infra
Platform

모듈 맵

[SW Eng] 시맨틱 버저닝 — 버전 숫자가 말하는 호환성

0 / 38 완료

펼치기
0 / 38 완료0%

PM·SRE를 위한 소프트웨어 엔지니어링 · 17 / 38

[SW Eng] 시맨틱 버저닝 — 버전 숫자가 말하는 호환성

MAJOR.MINOR.PATCH가 무엇을 약속하는지, breaking change·하위호환·버전 고정(lock)을 PM·인프라 관점에서 정리합니다

🚨INCIDENT ALERT
HIGH

의존 라이브러리를 업데이트했더니 빌드가 깨집니다. "버전을 1.4에서 2.0으로 올렸어요." 다른 개발자가 한숨 쉽니다. "MAJOR가 올랐으면 호환이 깨진 거예요, 마이그레이션 가이드 봤어야죠." 한편 인프라 담당은 "어제 분명 됐는데 오늘 같은 코드가 CI에서 깨져요"로 막막합니다 — 잠금 파일이 없어 그사이 패치 버전이 바뀐 것입니다. 버전 숫자는 장식이 아니라 '호환성에 대한 약속'입니다. 이 약속을 읽고 활용하면, 업데이트가 도박이 아니라 판단이 됩니다.

이번 챕터에서 배울 것
  • 1MAJOR.MINOR.PATCH가 각각 무엇을 약속하는지 설명할 수 있다
  • 2breaking change가 왜 MAJOR를 올리는지 설명할 수 있다
  • 3버전 범위(^, ~)와 잠금 파일의 역할을 구분할 수 있다
  • 4버전 정보로 업데이트 위험과 마이그레이션 필요성을 판단할 수 있다

SemVer — 숫자에 담긴 약속

💡개념

MAJOR.MINOR.PATCH: 무엇이 바뀌었는지를 숫자로

시맨틱 버저닝(SemVer)은 버전을 MAJOR.MINOR.PATCH로 매기고, 각 자리가 무엇을 약속하는지 정합니다.

시맨틱 버저닝 — MAJOR.MINOR.PATCH(예: 2.4.1)에서 MAJOR는 호환 깨지는 변경(마이그레이션 필요), MINOR는 하위호환 기능 추가, PATCH는 하위호환 버그 수정. 1.4.0→1.5.0은 OK, 1.4.0→1.4.1은 안심, 1.4.0→2.0.0은 가이드 확인 필수확대

핵심: 버전 숫자만 봐도 "내 코드를 고쳐야 하는지" 가늠할 수 있게 한 것이 SemVer의 가치입니다. MAJOR가 오르면 멈추고 변경 로그·마이그레이션 가이드를 봅니다. MINOR·PATCH면 릴리스 전략의 점진 배포로 안전하게 올립니다.

MAJOR.MINOR.PATCH 구조 — 버전 번호가 변경의 호환성을 약속: MAJOR는 하위호환 깨지는 변경(마이그레이션 필요), MINOR는 하위호환 기능 추가, PATCH는 하위호환 버그 수정. 숫자만 봐도 업그레이드 시 내 코드가 깨질지 가늠 — MAJOR가 오르면 변경 로그·마이그레이션 가이드를 확인확대

위 그림처럼 MAJOR는 빨간색(호환 파괴), MINOR는 파란색(기능 추가), PATCH는 초록색(버그 수정)으로 서로 다른 약속을 담고 있습니다.

breaking change와 버전 범위

💡개념

무엇이 '깨는' 변경이고, 어떻게 자동 업데이트를 통제하나

breaking change는 기존 사용자의 코드가 그대로면 깨지는 변경입니다.

TEXT
breaking (MAJOR↑):
  - API 응답 필드 제거 / 이름 변경
  - 필수 파라미터 추가
  - 함수 시그니처·동작 의미 변경
  - 기본값 변경(동작이 달라짐)

비-breaking (MINOR/PATCH):
  - 새 선택적 필드·엔드포인트 추가(기존은 그대로)
  - 내부 리팩터링(외부 영향 없음)
  - 버그 수정(의도된 동작 복구)

버전 범위 지정으로 자동 업데이트를 통제합니다(package.json 등):

TEXT
"^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만 막고 나머지는 받겠다"는 뜻입니다. 하지만 범위만으론 설치 시점마다 패치가 달라질 수 있어, 잠금 파일로 정확한 버전을 박제합니다(아래).

버전 범위 지정 비교 — ^1.2.3은 MINOR·PATCH 업까지 허용(1.x.x, 가장 흔함), ~1.2.3은 PATCH만 허용(1.2.x), 1.2.3 고정은 정확히 그 버전만. 범위가 넓을수록 자동 업데이트로 보안패치를 받기 쉽지만 예기치 않은 변경 위험도 커짐 — 안정성과 최신성의 트레이드오프확대

위 그림처럼 캐럿(^)은 MAJOR를 고정해 가장 넓은 범위를, 틸드(~)는 PATCH만, 정확히 고정하면 해당 버전만 허용합니다.

잠금 파일과 버전 재현성 — 범위 지정(^1.2.3)만 있으면 설치 시점마다 다른 패치 버전이 깔려 "내 PC는 되는데 CI는 깨짐"이 발생. lock 파일(package-lock.json·yarn.lock)은 실제 설치된 정확한 버전을 고정 기록해, 모든 환경에서 동일 의존성 트리를 재현 — lock 파일은 반드시 커밋확대

위 그림처럼 잠금 파일 없이 범위만 지정하면 로컬·CI·Prod가 서로 다른 버전을 설치해 "로컬은 되는데 CI가 깨진다"가 발생하고, 잠금 파일은 이를 방지합니다.

버전·의존성 판단 — 무엇을 올리고 무엇을 잠그나
내 변경에 어떤 버전 번호를버그 수정=PATCH, 하위호환 기능 추가=MINOR, 깨는 변경=MAJOR. API 시그니처·동작이 바뀌면 무조건 MAJOR. '깨면 MAJOR, 더하면 MINOR, 고치면 PATCH'
의존성 범위(^ ~ 고정) 지정^1.2.3(MINOR·PATCH 자동)=기능 받되 깨짐 회피, ~1.2.3(PATCH만)=보수적, 1.2.3(고정)=완전 재현성. 라이브러리는 ^, 애플리케이션은 잠금 파일로 고정
잠금 파일(lock)을 커밋할까애플리케이션은 반드시 커밋 — 로컬·CI·Prod가 동일 버전. '로컬은 되는데 CI가 깨진다'의 90%가 잠금 미커밋. 라이브러리는 커밋 안 함(소비자가 결정)
자동 업데이트로 빌드가 깨짐범위가 MAJOR까지 열렸거나 잠금이 없던 것. 0.x 버전은 MINOR도 깨질 수 있음(SemVer 예외). 'npm outdated로 무엇이 MAJOR 점프인지 먼저 본다'
0.x 버전 의존성0.x는 '안정성 보장 없음' — MINOR도 breaking일 수 있다. 프로덕션 핵심 의존성이 0.x면 고정 + 업그레이드 시 신중히 테스트. '1.0 미만은 약속이 약하다'
내 라이브러리를 deprecate갑자기 제거 금지 — MINOR에서 deprecated 경고 + 마이그레이션 안내 → 다음 MAJOR에서 제거. '예고 없는 제거는 생태계를 깬다'

버전 안정성 점검 — 직접 확인

1의존성 업데이트가 breaking인지, 잠금이 걸렸는지 확인

업데이트 전, 어떤 의존성이 MAJOR를 올리는지(위험)와 잠금 파일이 버전을 고정하는지 확인합니다.

로컬 터미널
# 업데이트 가능한 의존성과 현재/최신 버전 비교
npm outdated

# 잠금 파일이 커밋돼 있는지(재현성의 핵심)
git ls-files | grep -E "package-lock.json|yarn.lock|go.sum|poetry.lock"

# 컨테이너 베이스 이미지가 가변 태그(latest)인지 불변인지
grep -E "^FROM" Dockerfile
OUTPUT
$ 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) 업데이트 후 CI/CD 파이프라인의 자동 테스트가 회귀를 잡도록. 버전 숫자를 '읽을 줄 아는' 것만으로 이런 사고의 상당수를 예방합니다.

심화 — 약속의 한계와 잠그기의 청구서

💡개념

심화: SemVer는 검증되는 규칙이 아니라 자기 신고다

버전 숫자를 읽는 법을 익혔다면, 다음 단계는 그 숫자를 어디까지 믿을지 아는 것입니다. SemVer는 도구가 검증해 주는 규격이 아니라 메인테이너가 스스로 매기는 신고이고, 규모가 커질수록 그 틈이 드러납니다.

  • PATCH도 깨질 수 있습니다: 메인테이너가 버그 수정이라 판단한 변경이, 그 버그 동작에 기대던 코드에겐 breaking입니다. 사용자가 충분히 많으면 관찰 가능한 모든 동작에 누군가는 의존하게 된다(하이럼의 법칙)는 말처럼, breaking 여부는 배포자의 의도가 아니라 소비자의 사용 방식이 결정합니다. 그래서 MINOR·PATCH 자동 업데이트도 결국 테스트가 받쳐줘야 안전합니다.
  • 잠금 파일이 잠그는 건 트리 전체입니다: package.json에 30개를 적어도 잠금 파일엔 수백~수천 개가 기록됩니다 — 직접 의존성이 끌고 오는 의존성(전이 의존성)까지 전부. 빌드를 깨는 범인은 대개 내가 이름도 모르는 전이 의존성입니다. 그리고 잠금 파일이 있어도 npm install은 package.json과 어긋나면 잠금을 고쳐 쓰므로, CI에선 잠금 그대로만 설치하고 어긋나면 실패하는 npm ci를 써야 재현성이 완성됩니다.
  • 잠그기에도 비용이 있습니다: 완전 고정은 재현성을 주는 대신 보안 패치의 유입도 끊습니다. 잠근 채 방치된 의존성은 조용히 낡아가고(업그레이드 부채), 몇 년 뒤 취약점 대응 때 한꺼번에 청구됩니다. 잠금은 갱신 파이프라인(자동 PR + 테스트)과 짝일 때만 안전장치입니다.
  • SemVer가 유일한 체계도 아닙니다: Ubuntu 22.04처럼 날짜를 버전으로 쓰는 CalVer는 호환성 대신 시점을 약속합니다. 버전 문자열을 읽기 전에 그 프로젝트가 어떤 체계를 쓰는지부터 확인해야, 22.04를 MAJOR 22로 오독하지 않습니다.

그래서 성숙한 팀은 버전 숫자를 신뢰의 근거가 아니라 위험 분류의 힌트로 씁니다 — 최종 판정은 언제나 테스트가 합니다.

상황: 운영 서비스의 핵심 의존성에서 심각(Critical) 취약점이 보고됩니다. 대응은 간단해 보였습니다 — 패치된 버전으로 올리면 끝. 그런데 현재 버전은 2.3인데 패치는 5.x에만 반영돼 있습니다. 2년 전 '안정성을 위해' 전 의존성을 정확 버전으로 고정한 뒤 한 번도 올리지 않았던 것입니다.

원인: 완전 고정 전략의 숨은 비용입니다. 고정은 그 시점의 재현성을 주지만, MINOR·PATCH로 흘러들어왔을 보안 수정까지 함께 차단합니다. 업그레이드를 미룬 기간만큼 breaking change가 누적되고(2.x→3.0→4.0→5.0), 취약점이 터진 날 그 부채가 한꺼번에 만기됩니다. 보안 대응은 일정을 기다려 주지 않아, 최악의 타이밍에 최대 규모의 마이그레이션을 강요당합니다.

진단: npm outdated(또는 각 생태계의 동등 도구)로 Current와 Latest의 MAJOR 격차를 정기적으로 봅니다 — 격차가 2 이상 벌어진 핵심 의존성이 곧 잠재 부채 목록입니다. 취약점이 이미 터진 상황이라면, 패치가 구버전 라인에 백포트됐는지(예: 2.3.9) 릴리스 노트부터 확인합니다.

해결: 급한 불은 백포트 패치나 임시 완화(취약 기능 비활성화, 경계에서의 요청 차단)로 끄고, MAJOR 마이그레이션은 별도 작업으로 계획합니다. 재발 방지는 고정을 푸는 게 아니라 갱신을 상시화하는 것입니다 — 자동 PR(Renovate 등)로 MINOR·PATCH를 작게 자주 올리고(CI/CD 파이프라인의 테스트가 게이트), MAJOR는 분기마다 하나씩 계획적으로 소화합니다. 업그레이드는 미룰수록 싸지는 게 아니라 비싸지는 작업입니다.

💼
실무 맥락
현업 패턴

인프라/SRE로서 버저닝은 재현성과 롤백의 토대입니다 — 컨테이너 이미지를 불변 태그(v1.2.0)로 빌드해 릴리스 전략의 롤백 기준점을 만들고, 의존성 잠금 파일을 강제해 "어느 환경에서나 같은 빌드"를 보장합니다. 의존성 자동 업데이트(Renovate 등)를 도입하되 MAJOR는 게이트로 막아, 통제되지 않은 breaking change가 파이프라인에 흘러들지 않게 합니다. PM은 자사 API의 버전을 SemVer로 관리해 외부 연동사에 "이번 변경이 깨지는가(MAJOR)"를 명확히 약속하고, breaking 변경 시 마이그레이션 기간·deprecated 공지를 일정에 넣습니다. 버전 숫자는 팀과 외부가 호환성을 두고 나누는 공용 언어입니다.

이것으로 Phase 4(빌드·배포·릴리스)를 마칩니다. 다음 Phase에서는 이렇게 배포되는 시스템의 구조 자체 — 소프트웨어 아키텍처를 다룹니다.

실전 랩으로 손에 익히기: 시맨틱 버저닝 실습 — 변경을 major/minor/patch로 분류하고 버전·호환성을 관리합니다.

지식 확인

퀴즈 — 8문제

Q1

시맨틱 버저닝 2.4.1에서 각 숫자의 의미는?

Q2

'breaking change(파괴적 변경)'의 예로 적절한 것은?

Q3

버전 범위 지정 '^1.2.3'(캐럿)이 허용하는 업데이트는?

Q4

잠금 파일(package-lock.json, go.sum 등)을 커밋하는 이유는?

Q5

의존성의 MAJOR 버전이 올라갔다(예: 2.x → 3.0). 인프라/운영이 주의할 점은?

Q6

package.json에 '^1.2.3'으로 두면 어떤 위험과 이점이 있나?

Q7

[심화] 의존성의 PATCH 버전만 올렸는데도 우리 코드가 깨졌다. SemVer 관점에서 이를 가장 잘 설명하는 것은?

Q8

[심화] 심각 취약점 패치가 세 MAJOR 앞 버전에만 있어 하루짜리 대응이 몇 주 마이그레이션이 됐다. 2년 전 전 의존성을 정확 버전으로 고정한 뒤 한 번도 안 올렸다. 원인과 처방은?

0 / 8 답변

🧪 실습으로 확인하기

시맨틱 버저닝 — 버전 숫자가 말하는 호환성

중급

MAJOR.MINOR.PATCH가 무엇을 약속하는지 정하고, 변경을 등급으로 분류하는 규칙·의존성 범위(캐럿/틸드)와 lockfile 정책·배포 태그와 체인지로그 연결까지, 버전 숫자가 호환성을 신뢰성 있게 말하도록 버저닝 규칙을 설계한다.

45📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요