본문 바로가기
IT 일반

About 유의적 버전 Semantic Version

by 직장인B 2022. 9. 2.

 

 어플리케이션 개발 과정엔 수많은 라이브러리가 사용된다. 하나의 라이브러리만 사용하는 어플리케이션은 단언컨대 없다. 그렇게 개발 과정에 포함되는 라이브러리들은 또 다른 라이브러리를 사용한다. 여기에서 의존성 문제가 생겨난다. 

 가령 덧셈 기능을 가진 라이브러리 Sum과 뺄셈 기능을 가진 라이브러리 Minus가 있다고 하자. 두 라이브러리를 이용해 여러 개의 덧셈 뺄셈을 한번에 수행할 수 있는 라이브러리 Math가 있고 Math 라이브러리를 이용해 개발된 어플리케이션 Calculator가 있다. 어느 날 획기적인 혁신을 통해 라이브러리 Sum에 곱하기 기능을 지원하는 multiplication 함수가 생겨났다. 그렇지만 누구보다 좋아해야할 것 같은 Calculator 개발진은 침울한 분위기다. 왜? 그들이 사용하는 Math 라이브러리에 Sum의 multiplication과 호환되는 api가 없기 때문. Math 라이브러리 자체가 업데이트되지 않은 이상 Sum 라이브러리 그리고 Minus 라이브러리의 비약적인 발전은 Calculator 개발진에게 있어 먼나라 꿈나라 이야기다. 

 이와는 반대로 Math 라이브러리 자체의 업데이트로 인해 그것을 사용하는 Calculator 어플리케이션이 Minus 라이브러리의 몇몇 기능을 사용하지 못하게되는 수도 있다. 무튼 이러한 라이브러리의 연쇄적이고 양가적인 결합은 어플리케이션의 생존에 치명적인 위협이 될 수 있는 의존성 문제를 내포한다. 

 

 의존성 이야기를 꺼낸 건 버전 체계의 중요성을 말하기 위함이다. 버전 체계는 라이브러리에 그것의 변화에 맞추어 변경되는 숫자 태그를 붙이는 체계다. 가령 막 개발된 라이브러리 Sum을 Sum v1로 표시하고, 추후 업데이트로 인해 내용이 변경된 Sum 라이브러리를 Sum v2로 표시하는 것이다. 버전의 명시는 다음과 같은 장점을 지닌다. 

 

  • 라이브러리의 업데이트 여부를 쉽게 체크할 수 있다.
  • 라이브러리의 업데이트 내용을 쉽게 추척할 수 있다.
  • 라이브러리들의 호환 관계를 라이브러리 단이 아닌 버전 단으로 매칭시킴으로써 훨씬 조밀한 의존 관계를 설정할 수 있다. 
  • 업데이트 이전의 라이브러리를 삭제하지 않고 유지시킬 수 있다. 

 주의할 점은 라이브러리의 버전 체계가 '의존성 문제의 해결책'은 아니라는 점이다. 의존성 문제를 해결하는 길은 오직 두 가지 밖에 없다. 모든 라이브러리를 업데이트하지 않는 것이 첫째고, 둘째는 개별적인 라이브러리의 업데이트에 맞추어 그와 연관된 다른 라이브러리의 업데이트를 진행하는 것이다. 다행히도 이 세계는 두 번째 방법에 따르고 있다. 버전 체계는 바로 이 두 번째 방법의 효율성을 높이기 위한 하위 시스템 정도로 파악해야 옳다. 그러니까 의존성 문제를 해결하는 일차적인 시스템은 '끊임없이 업데이트를 반복하는 IT 영역의 문화 그 자체'인 것이다. 

 서론이 길었다. 모쪼록 라이브러리 버전 체계가 중요하고 고마운 것이라는 것 정도만 이해하고 넘어가자. 이어지는 의문은 이렇다. 그럼 버전 관리는 어떻게 하면 될까? 변화가 이루어질 때마다 숫자를 올리면 되는걸까? 그렇게 해도 안될 것은 없지만 그보다는 더 효과적인 관리 체계를 고민하지 않을 수 없다. 라이브러리의 변화, 업데이트라는 것도 여러 의미를 가질 수 있고 그에 따라 분류될 수 있기 때문. 하지만 그렇다고 버전을 너무 복잡하게 구성할 수도 없겠다. 버그 패치를 한 경우는 +1을 올리고 API 추가를 한 경우엔 +3을 올리는 그런 식이라면 오히려 라이브러리 관리 효율을 심각하게 망칠 수 있다. 이 복잡 난감한 문제를 말끔하게 해결해준 것이 유의적 버전 Semntic Version이다. 유의적 버전이란 버전 체계의 한 규범이다. 


 통상 라이브러리의 버전이 세 개의 숫자와 두 개의 점으로 구성된 걸 보아왔을 것이다. 그것이 곧 유의적 버전이다. 가령 이런 식.

 

Library Name 3.6.9

 

 이런 질문을 해볼 수 있다. 버전이 3.6.9라면 라이브러리는 '몇 번'의 업데이트를 거쳤을까? 3*6*9 번의 변화일까? 아니면 3+6+9 번의 변화일까? 정답은 둘 다 아니다. 답은 '알 수 없다.'이다. 단 이렇게 말해볼 수는 있다. 

 해당 라이브러리는 3번의 주버전 업데이트를 거쳤고, 주버전이 3인 상태에서 6번의 부버전 업데이트를 거쳤고, 주버전이 3이고 부버전이 6인 상태에서 9번의 수버전 업데이트를 거쳤다. 즉 세 개의 숫자는 각각 다음의 의미를 지닌다. 

 

Library Name <주버전 업데이트 횟수>.<부버전 업데이트 횟수>.<수버전 업데이트 횟수>

 

 자연이 다음의 질문이 이어진다. 주, 부, 수버전 업데이트가 대체 뭔가? 무슨 차이가 있는가? 

 우선 각각의 의미를 파악하기 위해선 해당 라이브러리가 '사용가능한 것'임이 전제되어야 한다. 사용가능한 라이브러리란 그것의 API가 공개되어 다른 라이브러리 혹 어플리케이션 코드에 삽입될 수 있다는 걸 의미한다. Java로 따지자면 라이브러리의 인터페이스가 공개된 상태를 의미한다. 무튼 이렇게 라이브러리의 공개 API가 확정되면 라이브러리는 v1.0.0의 버전을 지닌다. 

 앞서의 전제로 v1.0.0의 라이브러리가 존재하는 상태다. 이때 라이브러리 API 에 기능 추가/변경 개발을 수행할 수 있다. 가령 특정 인터페이스에 새로운 함수를 추가하거나 기존에 있던 함수에 deprecated 선언을 내릴 수 있다. 이러한 상의 추가/변경을 수행한 경우 이를 부버전 업데이트라고 한다. 이후 라이브러리의 버전은 v1.1.0이 된다. 기능 추가/변경과는 별개로 특정 기능이 작동되는 과정에 버그가 있어서 이를 잡기 위해 코드를 수정한 경우 이를 수버전 업데이트라고 한다. 이후 라이브러리의 버전은 v1.1.1이 된다. 수버전 업데이트는 통상 패치라고 부른다. 새겨보아야 할 점은 부버전 업데이트나 수버전 업데이트의 경우 모두 기존의 공개 API와 호한 가능하다는 점이다. 호환 가능하다는 건, 업데이트 이전에 라이브러리가 제공하는 기능들이 업데이트 이후에도 정상적으로 제공/작동한다는 것이다. 만일 이러한 기존의 제공 기능들 자체가 변경되는 경우 이를 주버전 업데이트라고 한다. 이후 라이브러리의 버전은 v2.0.0이 된다. 주버전 업데이트가 이루어지는 경우 부버전, 수버전의 값은 0이 된다. 마찬가지로 부버전이 업데이트되는 경우 수버전의 값도 0이 된다. 

 예시를 들어 더 꼼꼼하게 알아보자. 서론에서 말했던 Sum 라이브러리와 Math 라이브러리를 그대로 가져와보기로 한다. Sum 라이브러리의 구조는 다음과 같다. 

 

Sum 1.0.0

Interface SumCalculation
Class SumCalculationImpl
Method int Plus(int a) 입력된 변수에 1을 더한 값을 반환

 

 SumCalculation이라는 인터페이스에 Plus라는 method가 있고 인터페이스의 구현 클래스 SumCalculationImpl 가 있다. Math 라이브러리는 SumCalculationImpl의 plus 함수를 사용하여 사칙연산을 수행하는 기능을 갖는다. 사칙연산의 효율성을 위하여 Sum 라이브러리에 입력된 변수에 10을 더한 값을 반환하는 method를 추가했다고 해보자. 

 

Sum 1.1.0

Interface SumCalculation
Class SumCalculationImpl
Method int Plus(int num) 입력된 변수에 1을 더한 값을 반환
Method int PlusHigh(int num) 입력된 변수에 10을 더한 값을 반환

 Sum 라이브러리의 API 구조가 변경되었다. 하지만 Math 라이브러리는 굳이 이에 맞추어 업데이트를 진행할 필요가 없다. 기존에 사용하던 Plus 함수는 그대로 제공되고 있기 때문이다. 이는 기존에 Sum v1.0.0을 사용하는 다른 모든 라이브러리에게 있어서도 마찬가지겠다. 

 한편 어느 날 Plus 함수 작동 중에 num이 음수인 경우 -1을 더한 값을 반환하는 버그가 발견되었다. 이에 대한 패치를 진행한 Sum 라이브러리의 구조는 다음과 같다. 

 

Sum 1.1.1

Interface SumCalculation
Class SumCalculationImpl
Method int Plus(int num) 입력된 변수에 1을 더한 값을 반환(패치)
Method int PlusHigh(int num) 입력된 변수에 10을 더한 값을 반환

 Sum 라이브러리의 API 구조는 변경되지 않았다. 그렇기에 Sum v1.0.0을 사용하는 Math 라이브러리나 Sum v1.1.0을 사용하는 다른 라이브러리의 경우 이에 맞춘 코드 수정이 필요하지 않다. 

 그런데 어느 날 대대적인 시스템 혁신으로 인해 Sum 라이브러리의 모든 Method가 반환값을 십진수 정수값이 아닌 이진수 문자열 형식에 맞춰야할 필요가 생겼다고 하자. 이때 Sum 라이브러리 Method의 내부 로직이 변경되는 것과 함께 interface 정의 자체가 변하게 된다. 다음처럼 말이다. 

 

Sum 2.0.0

Interface SumCalculation
Class SumCalculationImpl
Method String Plus(int num) 입력된 변수에 1을 더한 값을 이진수로 반환(패치)
Method String PlusHigh(int num) 입력된 변수에 10을 더한 값을 이진수로 반환

 API의 내용 자체가 변화된다. 이 경우 Sum 라이브러리를 사용하는 Math 라이브러리는 변화된 API 내용에 맞추어 자신의 내부 로직을 업데이트시켜야한다.

 주버전 업데이트의 경우는 방금의 사례처럼 라이브러리를 사용하는 다른 개체들로 하여금 그에 맞춘 업데이트를 요구한다. 언제나 그런건 아니지만 대부분 그러하다. 반대로 부버전 업데이트나 수버전 업데이트의 경우 그에 맞춘 업데이트가 반드시 필요한 것은 아니다. 다만 기능적인 개선을 위해서 업데이트를 수행할 수는 있겠다. 

 요점은 Math 라이브러리 입장에서 Sum 라이브러리의 버전 변화를 추적함으로써 자신의 업데이트 필요성을 체크해볼 수 있다. 버전이 변화할 때마다 Sum 라이브러리의 모든 코드를 살펴볼 필요가 없는 것이다! 바로 이 점이 유의성 버전의 핵심 의의라고 하겠다. 

 


  앞서의 내용은 사용가능한 라이브러리에 한정된 내용이다. 그렇다면 사용 이전인 라이브러리, 즉 개발 중인 라이브러리는 위의 버전 체계의 적용을 받지 못하는 것일까? 아니다. 이에 대한 해결책이 있다. 

 단순하다. 주버전을 0 으로 설정하면 된다. 다음처럼 말이다.

 

Library Name 0.5.0

 

 이는 개발중인 해당 라이브러리에 5번의 기능 추가/변경이 진행되었다는 것을 암시한다. 

 종종 주버전이 0인 라이브러리가 상용 어플리케이션 내에 적용되고 있는 것을 볼 수 있다. 주버전 0인 경우에도 API를 공개할 수 있다. 하지만 이 경우 개발자의 맘대로 API를 변경될 수 있다. 기본적인 안정성이 보장되지 않는 셈이다. 

 


 종종 버전에 식별자라는 것이 붙는 경우가 있다. 식별자의 형식 자체는 그에 대한 깔끔한 정의가 없는 듯하다. 통상 alpha나 beta 같은 식별자들이 통용된다. 식별자는 버전에 막대기 하나를 붙인 그 다음에 위치한다. 다음처럼 말이다. 

 

Libaray Name 1.2.8-alpha

 

 주버전이 0으로 표시된 개발 라이브러리에는 이러한 식별자를 붙이면 안된다고 한다. 식별자는 대게 정식 배포 이전에 테스트용으로 배포한 라이브러리임을 표시하기 위해 사용한다. 정식 배포하려고 준비 중인데 이것 먼저 써보세요 라는 식으로 임시 배포해볼 수 있는 것. 

 그러한 점에서 식별자가 붙은 라이브러리 배포판은 식별자가 없는 라이브러리 배포판보다 우선순위가 낮다! 우선순위라 함은 간단하게 말해 배포 순서라고 볼 수 있다. 최근에 배포할수록 우선순위는 높다. 

 식별자 사이에서도 우선순위를 가를 수 있다. 가령 alpha 식별자는 beta 식별자보다 우선순위가 높다. 하지만 이는 통상적인 관례에 의해서 정의되는 것이지 정확히 규범화된 체계는 아닌 듯하다. 

 


 Node JS 기반의 개발 작업을 하는 경우, package.json 파일의 dependency 목록을 보면 라이브러리 버전 앞에 ~ 나 ^ 의 기호가 붙여진 걸 종종 볼 수 있다. 이건 버전 체계와는 별개의 표기임을 알아두어야 한다. 그러니까 ^가 붙든 붙지 않든 해당 프로젝트에 설치된 라이브러리의 버전은 동일하다! 

 갈매기와 물결 표시는 라이브러리 버전을 명시하는 것이 아닌 프로젝트와 해당 라이브러리의 의존 관계를 명시한다. 다르게 말하자면 호환의 범위를 나타낸다. 물결 ~ 의 경우 프로젝트가 해당 라이브러리의 주버전, 부버전에 의존한다는 걸 말해주고, 갈매기 ^의 경우 프로젝트가 해당 라이브러리의 주버전에만 의존한다는 걸 말해준다. 추가로 * 이나 x 를 쓰는 경우 가장 최신 주버전 라이브러리를 명시한다. 

 

``` package.json

 

"library Name" : "~3.6.9"

 이 경우 프로젝트의 정상적인 작동을 위해선 해당 라이브러리는 3.6.x 버전으로 설치되어야한다.

 

"library Name" : "^3.6.9"

 이 경우 프로젝트의 정상적인 작동을 위해선 해당 라이브러리는 3.x.x 버전으로 설치되어야한다. 

 

"library Name" : "*"

 이 경우 가장 최신의 라이브러리를 사용하면 된다. 

 

```

 

 쉽게 말해 해당 프로젝트를 사용하려는 사람들에게 라이브러리 설치 가이드를 알려주는 셈이다. 

 

 

 

 

참고 : semver.org/lang/ko/