Semantic Versioning
소프트웨어 관리라는 세상에는 ‘의존성 지옥’이라는 아주 두려운 곳이 있다. 시스템이 커 갈수록 더 많은 패키지를 소프트웨어에 통합하게 되고, 어느 날 자신을 돌아봤을 때는 절망의 구렁텅이에 있는 것을 보게 된다.
시스템이 많은 것에 의존할 때 새 패키지 버전을 재빨리 출시하려는 건 악몽이 된다. 의존성이 너무 강하면 버전 잠김(version lock. 패키지를 업그레이드 하려면 의존하는 모든 패키지에 대해 새 버전을 만들어야 하는 상태)이라는 위험에 빠진다. 그렇다고 의존성이 너무 약하면 결국엔 버전 섞임(version promiscuity. 필요 이상으로 많은 패키지와 호환되는 상태)이라는 골치 아픈 문제가 생긴다.
이를 해결할 방법으로 어떻게 버전 번호를 붙이고 올려야 하는지 간단한 규칙과 조건을 제안한다. 이 시스템이 잘 동작하려면 먼저 공용 API를 선언해야 하는데, 이는 문서로 만들 거나 코드로 강제할 수도 있다. 아무튼 이 API는 명확하고 정확해야 한다. 공용 API를 정한 후엔 버전 번호를 올리는 방법으로 이 API에 대한 변경을 다룬다. 버전 번호 형식이 X.Y.Z(주 버전.부 버전.패치 버전)라고 하자. API에 영향을 주지 않는 버그 수정은 패치 버전을 증가하고, 하위 호환성이 있는 API를 추가하거나 변경하면 부 버전을, 하위 호환되지 않는 API 변경은 주 버전을 증가시킨다.
이 시스템을 의미론적 버전 붙이기라 한다. 이 방식에 따르면 버전 번호와 이 번호를 바꾸는 방법을 통해 바탕이 되는 코드의 의미와 더불어 이전과 다음 버전 사이에 무엇이 바뀌었는지를 알 수 있다.
의미론적 버전 붙이기 사양 (SemVer)
- 의미론적 버전 번호 붙이기를 사용하는 소프트웨어는 반드시 공용 API를 선언해야 한다. 이 API는 코드로 선언하거나 문서에 엄격히 기술한다. 또한 정확하고 이해하기 쉬워야 한다.
- 일반 버전 번호는 반드시 X.Y.Z 형식이어야 하며 X, Y, Z는 정수이다. X는 주 버전, Y는 부 버전, Z는 패치 버전이다. 각각은 반드시 수치적으로 증가해야 한다. 예를 들면 1.9.0 < 1.10.0 < 1.11.0 이다.
- 패치 버전 바로 뒤에 임의 문자열을 추가해 특별한 버전 번호를 나타낼 수도 있다. 이 문자열은 반드시 영문자, 숫자, 대시 문자(0-9A-Za-z-)만으로 구성하며 반드시 영문자(A-Za-z)로 시작한다. 특별한 버전은 관련된 일반 버전보다 우선 순위가 낮다. 우선 순위는 사전 순을 따라야 한다. 예를 들면 1.0.0beta1 < 1.0.0beta2 < 1.0.0 이다.
- 버전을 붙인 패키지를 배포하면 그 버전의 내용은 절대 바꾸지 말아야 한다. 모든 변경은 새 버전으로 배포해야 한다.
- 개발을 시작할 때는 주 버전을 0으로 한다(0.y.z). 언제든지 모든 내용을 바꿀 수 있으며 공용 API는 안정적이지 않다고 생각한다.
- 버전 1.0.0에서는 공용 API를 정의한다. 버전 번호는 이 공용 API와 이 API가 바뀌는 방식에 따라 증가시킨다.
- 패치 버전 Z(x.y.Z | x > 0)는 반드시 하위 호환되는 버그 수정을 할 때만 증가시킨다. 버그 수정은 잘못된 동작을 고치는 내부 변경으로 정의한다.
- 부 버전(x.Y.z | x > 0)는 반드시 하위 호환되는 새 기능을 공용 API에 추가할 때 증가시킨다. 비공개 코드 부분에 새 기능을 추가하거나 기능 향상이 있어도 증가시킬 수 있으며 패치 수준 변경을 포함할 수 있다.
- 주 버전(X.y.z | X > 0)는 반드시 공용 API에 하위 호환성이 없는 변경을 할 때 증가시킨다. 부 버전과 패치 수준 변경을 포함할 수 있다.
태그 붙이기 사양 (SemVerTag)
이 하위 사양은 (Git, Mercurial, SVN 등) 버전 제어 시스템을 사용할 때 사용하길 권한다. 이 시스템을 사용할 때 패키지를 검사하고 SemVer 준수 여부와 출시한 버전인지 확인하는데 자동화한 툴을 사용할 수 있다.
- 버전 제어 시스템에서 배포 판에 태그를 붙일 때는 반드시 ‘vX.Y.Z’, 예를 들어 ‘v3.1.0’과 같은 형식이어야 한다.
- SemVer를 준수하는 첫 번째 리비전에는 태그를 ‘semver’로 붙인다. 이렇게 하면 프로젝트가 이미 있더라도 임의 시점부터 준수할 수 있으며 자동화한 툴로 이를 확인할 수 있다.
왜 의미론적 버전 붙이기를 사용하는가?
이는 새롭거나 혁명적인 생각이 아니다. 사실 이에 가까운 어떤 일을 이미 하고 있을지도 모른다. 문제는 ‘가깝다’는 걸로는 충분하지 않다는 거다. 형식적인 사양 중 어떤 것을 만족하지 않으면 의존성을 관리하는데 버전 번호는 전혀 쓸모 없어진다. 위에서 설명한 개념에 대해 이름을 짓고 명확히 정의함으로써 여러분의 의도를 여러분이 만든 소프트웨어의 사용자에게 전달하는 것이 더 쉬워진다. 일단 이런 의도를 명확히 하고 나면 마침내 의존성에 대한 사양을 유연하게 (그러나 너무 유연하지 않게) 만들 수 있다.
의미론적 버전 붙이기로 어떻게 의존성 지옥을 벗어날 수 있는지 간단한 예를 들어 보자. ‘Firetruck’이라는 라이브러리가 있다. 이 라이브러리는 ‘Ladder’라는 패키지가 필요한데 이 패키지에는 의미론으로 버전을 붙였다. Firetruck을 만들었을 때 Ladder는 3.1.0 버전이다. Firetruck은 3.1.0에 처음으로 추가한 기능을 사용하므로 Ladder에 대한 의존성을 3.1.0 이상 4.0.0 미만으로 안전하게 지정할 수 있다. 이제 Ladder 3.1.1과 3.2.0을 사용할 수 있을 때 이 것을 패키지 관리 시스템으로 배포할 수 있고 기존에 의존하는 소프트웨어와도 호환될 것이다.
물론 책임 개발자로서 여러분은 모든 패키지에서 공시한 대로 기능을 업그레이드했는지 확인하고 싶을 것이다. 실제 세상은 복잡한 곳이다. 이에 대해 주의하는 것 외에 우리가 할 수 있는 것은 아무 것도 없다. 여러분이 할 수 있는 것은 의미론적으로 버전을 붙여 의존하는 패키지의 새 버전을 만들지 않고 시간을 절약하며 혼란도 없이 패키지를 배포하고 업그레이드할 수 있도록 분별할 수 있는 방법을 만드는 것이다.
이 모든 것이 바람직하다는 생각이 든다면 의미론적 버전 번호 붙이기를 시작한다고 선언하고 그 규칙을 따르면 된다. README 파일에 이 웹사이트 주소를 적음으로써 다른 이도 이 규칙을 알고 그 이점을 얻을 수 있다.
FAQ
1.0.0을 배포해야 하는 시점을 어떻게 알 수 있나?
소프트웨어를 제품에 사용하고 있다면 분명히 이미 1.0.0일 것이다. 안정적인 API가 있고 이 API를 사용자들이 사용하고 있다면 1.0.0이어야 한다. 하위 호환성을 걱정한다면 이미 1.0.0일 거다.
이는 쾌속 개발(rapid development)과 빠른 반복 주기를 해치지 않는가?
주 버전이 0일 때는 모든 것이 쾌속 개발이다. 매일 API를 바꾼다면 0.x.x 버전으로 유지하거나 별도 개발 브랜치에서 다음 주 버전에 대한 개발을 해야 한다.
공용 API에 생긴 하위 호환되지 않는 변화가 아주 작은 것이라도 주 버전을 올려야 한다면 너무나 빨리 42.0.0 버전처럼 돼 버리지는 않나?
이는 책임 있는 개발과 예측에 대한 질문이다. 호환되지 않는 변화는 의존하는 코드가 많은 소프트웨어에 가볍게 추가해서는 안 된다. 업그레이드 비용이 심각하게 커질 수 있다. 주 버전을 올려 호환되지 않는 변화를 배포한다는 건 변화에 대한 충격을 고려하고 그에 대한 비용 대비 이익률을 평가해야 한다는 뜻이다.
공용 API를 모두 문서화 한다는 건 일이 너무 많다!
다른 이가 사용할 소프트웨어에 대해 적절히 문서화 하는 것은 전문 개발자로서 여러분이 가져야 할 책임이다. 소프트웨어 복잡성 관리는 프로젝트를 효율적으로 유지하는 매우 중요한 부분이지만 여러분이 만든 소프트웨어를 어떻게 사용해야 할지, 어떤 메소드가 호출하기에 안전한지 아무도 모른다면 이는 하기 어렵다. 오랜 시간 동안 의미론적으로 버전을 붙이고 공용 API를 일관성 있게 잘 정의하면 모든 이와 모든 것이 부드럽게 잘 흘러 가도록 할 수 있다.
실수로 하위 호환성이 없는 변화에 대해 부 번호를 바꿔 배포했을 때는 어떻게 하나?
의미론적 버전 붙이기 사양을 어긴 것을 아는 즉시 문제를 바로 잡고 하위 호환성을 복원한 올바른 부 버전을 배포한다. 기억할 것은, 이런 상황에서도 버전을 붙여 배포한 것은 변경할 수 없다는 점이다. 위반한 버전을 문서화 하고 사용자에게 문제를 알려 위반한 것을 알 수 있도록 한다.
공용 API를 바꾸지 않고 의존성을 갱신해야 할 때는 어떻게 해야 하나?
공용 API에 영향을 주는 것이 아니므로 호환성에 대해 생각한다. 여러분이 만든 패키지와 같은 것에 명시적으로 의존하는 소프트웨어에는 의존 사양이 있을 것이고 그 제작자는 충돌이 생기면 알 것이다. 변경이 패치 수준인지 부 버전 수준인지는 버그를 수정하려고 의존성을 갱신한 건지 아니면 새 기능을 추가하려 그렇게 한 건지에 따라 결정한다. 후자인 경우에는 흔히 코드를 추가하므로 부 버전 수준을 올리는 것이 자명하다.
이 문서에 대해
의미론적 버전 붙이기 사양은 Gravatars를 처음 만들었고 GitHub 공동 설립자인 톰 프레스턴 워너(Tom Preston-Werner)가 만들었다.
이에 대한 의견을 남기고 싶다면 GitHub에 이슈를 올려주길 바란다.