의존 라이브러리를 업데이트했더니 빌드가 깨집니다. "버전을 1.4에서 2.0으로 올렸어요." 다른 개발자가 한숨 쉽니다. "MAJOR가 올랐으면 호환이 깨진 거예요, 마이그레이션 가이드 봤어야죠." 한편 인프라 담당은 "어제 분명 됐는데 오늘 같은 코드가 CI에서 깨져요"로 막막합니다 — 잠금 파일이 없어 그사이 패치 버전이 바뀐 것입니다. 버전 숫자는 장식이 아니라 '호환성에 대한 약속'입니다. 이 약속을 읽고 활용하면, 업데이트가 도박이 아니라 판단이 됩니다.
- 1MAJOR.MINOR.PATCH가 각각 무엇을 약속하는지 설명할 수 있다
- 2breaking change가 왜 MAJOR를 올리는지 설명할 수 있다
- 3버전 범위(^, ~)와 잠금 파일의 역할을 구분할 수 있다
- 4버전 정보로 업데이트 위험과 마이그레이션 필요성을 판단할 수 있다
SemVer — 숫자에 담긴 약속
MAJOR.MINOR.PATCH: 무엇이 바뀌었는지를 숫자로
시맨틱 버저닝(SemVer)은 버전을 MAJOR.MINOR.PATCH로 매기고, 각 자리가 무엇을 약속하는지 정합니다.
2 . 4 . 1
│ │ └ PATCH: 하위호환 버그 수정 (안심하고 올려도 됨)
│ └────── MINOR: 하위호환 기능 추가 (기존 코드 안 깨짐)
└──────────── MAJOR: 호환 깨지는 변경 (마이그레이션 필요!)
1.4.0 → 1.5.0 : 기능 추가, 내 코드 그대로 OK
1.4.0 → 1.4.1 : 버그 수정, 안심
1.4.0 → 2.0.0 : ⚠ 호환 깨짐 — 마이그레이션 가이드 확인 필수
핵심: 버전 숫자만 봐도 "내 코드를 고쳐야 하는지" 가늠할 수 있게 한 것이 SemVer의 가치입니다. MAJOR가 오르면 멈추고 변경 로그·마이그레이션 가이드를 봅니다. MINOR·PATCH면 [[release-strategy]]의 점진 배포로 안전하게 올립니다.
breaking change와 버전 범위
무엇이 '깨는' 변경이고, 어떻게 자동 업데이트를 통제하나
breaking change는 기존 사용자의 코드가 그대로면 깨지는 변경입니다.
breaking (MAJOR↑):
- API 응답 필드 제거 / 이름 변경
- 필수 파라미터 추가
- 함수 시그니처·동작 의미 변경
- 기본값 변경(동작이 달라짐)
비-breaking (MINOR/PATCH):
- 새 선택적 필드·엔드포인트 추가(기존은 그대로)
- 내부 리팩터링(외부 영향 없음)
- 버그 수정(의도된 동작 복구)
버전 범위 지정으로 자동 업데이트를 통제합니다(package.json 등):
"^1.2.3" → MAJOR 고정(1.x), MINOR·PATCH 자동 (1.5.0 OK, 2.0.0 ✗) ← 가장 흔함
"~1.2.3" → MINOR도 고정, PATCH만 자동 (1.2.9 OK, 1.3.0 ✗) ← 보수적
"1.2.3" → 정확히 그 버전만 ← 완전 고정
캐럿(^)은 "SemVer 약속을 신뢰해 MAJOR만 막고 나머지는 받겠다"는 뜻입니다. 하지만 범위만으론 설치 시점마다 패치가 달라질 수 있어, 잠금 파일로 정확한 버전을 박제합니다(아래).
버전 안정성 점검 — 직접 확인
업데이트 전, 어떤 의존성이 MAJOR를 올리는지(위험)와 잠금 파일이 버전을 고정하는지 확인합니다.
# 업데이트 가능한 의존성과 현재/최신 버전 비교
npm outdated
# 잠금 파일이 커밋돼 있는지(재현성의 핵심)
git ls-files | grep -E "package-lock.json|yarn.lock|go.sum|poetry.lock"
# 컨테이너 베이스 이미지가 가변 태그(latest)인지 불변인지
grep -E "^FROM" Dockerfile
$ npm outdated
Package Current Wanted Latest
axios 1.6.2 1.6.8 1.7.0 ← Wanted(1.6.8)는 범위 내 안전, Latest(1.7.0)는 MINOR↑
left-pad 1.3.0 1.3.0 2.0.0 ← Latest가 MAJOR(2.0.0)! 올리면 breaking 가능
$ git ls-files | grep lock
package-lock.json ← 잠금 파일 커밋됨(재현성 OK)
$ grep FROM Dockerfile
FROM node:20.11.1-slim ← 불변 태그(OK). node:latest였다면 위험
npm outdated- npm outdated에서 Latest가 Current보다 MAJOR가 높으면(예: 1.x→2.x) → breaking 가능. 그 패키지는 자동 업데이트 말고 변경로그 확인 후 별도 마이그레이션
- Wanted == Current인데 Latest가 더 높으면 → 범위 지정(^)이 MAJOR를 막고 있는 것(정상). 의도적으로 올리려면 범위를 수정
- 잠금 파일(package-lock 등)이 git에 없으면 → 환경마다 다른 버전 설치 위험("로컬은 되는데 CI는 깨진다"의 주원인). 커밋하도록 요청
- Dockerfile FROM이 :latest나 태그 없음이면 → 빌드 시점마다 베이스가 달라져 재현 불가·롤백 무의미. 불변 태그(node:20.11.1)로 고정
상황: CI가 의존성을 자동 갱신하거나, 잠금 파일 없이 재설치하는 과정에서 한 라이브러리의 MAJOR 버전이 올라가(1.x→2.x) 빌드가 깨지거나, 더 나쁘게는 빌드는 되지만 동작이 미묘하게 달라져 운영에서 버그가 터집니다.
원인: SemVer 약속과 버전 고정의 부재입니다. 범위가 너무 느슨하거나(>=1.0.0) 잠금 파일이 없어, 통제되지 않은 MAJOR 업데이트가 들어왔습니다. MAJOR는 정의상 호환이 깨질 수 있는 변경입니다.
진단:
# 잠금 파일 변경으로 어떤 버전이 점프했는지(PR diff에서)
git diff package-lock.json | grep -E '"version"' | head
# MAJOR가 바뀐 항목이 있으면 그게 범인
해결: (1) 잠금 파일을 커밋해 모든 환경의 버전을 고정. (2) 범위는 ^(MAJOR 고정)을 기본으로, 의존성 업데이트는 자동 PR(Dependabot/Renovate)로 받되 MAJOR는 자동 머지 금지하고 사람이 변경로그를 검토. (3) 업데이트 후 [[cicd-pipeline]]의 자동 테스트가 회귀를 잡도록. 버전 숫자를 '읽을 줄 아는' 것만으로 이런 사고의 상당수를 예방합니다.
인프라/SRE로서 버저닝은 재현성과 롤백의 토대입니다 — 컨테이너 이미지를 불변 태그(v1.2.0)로 빌드해 [[release-strategy]]의 롤백 기준점을 만들고, 의존성 잠금 파일을 강제해 "어느 환경에서나 같은 빌드"를 보장합니다. 의존성 자동 업데이트(Renovate 등)를 도입하되 MAJOR는 게이트로 막아, 통제되지 않은 breaking change가 파이프라인에 흘러들지 않게 합니다. PM은 자사 API의 버전을 SemVer로 관리해 외부 연동사에 "이번 변경이 깨지는가(MAJOR)"를 명확히 약속하고, breaking 변경 시 마이그레이션 기간·deprecated 공지를 일정에 넣습니다. 버전 숫자는 팀과 외부가 호환성을 두고 나누는 공용 언어입니다.
이것으로 Phase 4(빌드·배포·릴리스)를 마칩니다. 다음 Phase에서는 이렇게 배포되는 시스템의 구조 자체 — 소프트웨어 아키텍처를 다룹니다.