Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

npm shrinkwrap 으로 의존성 버전을 고정시키기

node.js로 프로그래밍을 하면 npm을 이용해서 모듈을 많이 사용하게 됩니다. 이는 node.js에서 무척 편리한 장점 중의 하나이지만 어느 환경에서나 그렇듯이 의존성에 대한 문제를 낳게 됩니다. 자바처럼 많은 사람들이 사용하고 오래된 환경에서도 메이븐에서 의존성 관리에 대한 곤란함을 느끼는 것을 보면 의존성은 쉽사리 해결하기 어려운 문제라고 생각합니다. 이와 비슷한 의존성이 속썩이는 문제가 npm에서도 존재합니다. 기본적으로 npm은 package.json으로 의존성을 지정해 줄 수 있어서 협업하는 개발자들이 같은 버전의 모듈을 사용할 수 있도록 공유할 수 있습니다. 하지만 여기서 해결되지 않는 문제가 중첩된 의존성에 대한 문제입니다.




의존성 모듈의 의존성 버전 제어의 문제점
여러 가지 의존성 모듈을 사용하는 데 그 모듈들이 가지고 있는 의존성의 버전은 관리할 수 없는 문제가 있습니다. 웹프레임워크인 express를 예를 들어 설명하겠습니다. 제 프로젝트에 express가 다음과 같이 설치되어 있다고 가정하겠습니다.


└─┬ express@2.5.8 
  ├─┬ connect@1.8.5 
  │ └── formidable@1.0.9 
  ├── mime@1.2.4 
  ├── mkdirp@0.3.0 
  └── qs@0.4.0 

express 2.5.8버전를 설치하면 connect, mime, mkdirp, qs 모듈도 설치되고 또 connectformidable라는 모듈에 대한 의존성을 가지고 있습니다. package.json에서는 의존성에 대한 버전 범위를 다양하게 지정할 수 있는데 express2.5.8로 지정해놨더라도 express가 의존하는 모듈에 대한 버전은 express의 package.json에 지정된 버전을 따르게 되어 있습니다. 다음은 express의 package.json의 의존성 부분입니다.


...
"dependencies": {
  "connect": "1.x",
  "mime": "1.2.4",
  "qs": "0.4.x",
  "mkdirp": "0.3.0"
},
...

connect는 mime과 mkdirp는 버전이 고정되어 있지만 connect와 qs는 각각 1.x버전과 0.4.x의 버전범위 내에서 가장 최신버전을 설치하게 됩니다.  이 상황에서 npm install express를 하는 시점에 따라 connect와 qs의 새로운 버전이 나온다면 새로운 버전이 설치되게 됩니다. 이는 다르게 말하면 제가 설치한 express와 제 동료 개발자가 설치한 express가 같은 세트가 아닐 수 있다는 것입니다. express를 만든 TJ Holowaychuk는 express를 전혀 변경하지 않았지만 connect와 qs의 개발자가 새로운 버전을 배포하면 express가 영향을 받을 수 있다는 의미입니다.

express가 모든 의존성을 정확한 버전으로 지정한다면 이런 문제가 없겠지만 대부분의 의존성은 약간의 유연함을 가지고 있고 express는 제가 만든 모듈도 아니기 때문에 이를 강요할 수도 없는 노릇입니다. 사실 이러한 유연함을 개발단계에서는 썩 나쁘지 않지만 프로덕션레벨로 서비스할 때는 빌드할 때마다 버전이 달라진 다는 것은 큰 문제를 낳을 수 있습니다. 이로 인한 버그가 생길 수도 있기 때문에 개발자 입장에서는 피곤한 문제입니다. 좀 더 문제를 구체적으로 설명하기 위해 현재 제 프로젝트의 package.json이 다음과 같다고 하겠습니다.

{
    "name": "application-name"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.8"
    , "jade": ">= 0.0.1"
  }

package.json으로 npm install을 해서 다음과 같은 버전의 express와 jade가 설치되었습니다.


$ npm ls
application-name@0.0.1 /Users/outsider/projects/nodejs/test/
├─┬ express@2.5.8 
│ ├─┬ connect@1.8.5 
│ │ └── formidable@1.0.9 
│ ├── mime@1.2.4 
│ ├── mkdirp@0.3.0 
│ └── qs@0.4.0 
└─┬ jade@0.21.0 
  ├── commander@0.5.2 
  └── mkdirp@0.3.0 

마지막에 npm oudated로 확인을 해보면 다음과 같이 qs와 connect의 새버전이 배포된 것을 확인할 수 있습니다.


$ npm outdated --loglevel=silent
qs@0.4.2 ./node_modules/express/node_modules/qs current=0.4.0
connect@1.8.6 ./node_modules/express/node_modules/connect current=1.8.5


이러한 상황에서 다른 개발자가 프로젝트를 다운받아서 설치를 하면 제가 가진 것과는 다른 세트의 expressjade가 설치될 것입니다. 즉 1.8.6버전의 connect와 0.4.2 버전의 qs가 설치됩니다. 아무런 문제도 없을수도 있지만 버전차이로 인한 오류가 발생할 수도 있습니다. 그래서 개발때 충분히 테스트했을 때도 배포할 때 새로운 버전의 의존성이 배포되면서 오류가 발생할 수도 있습니다.(--loglevel을 지정한 것은 버전확인을 위해서 HTTP 요청 로그가 남지 않도록 하기 위함입니다.)

이 문제를 해결하기 위해서 프로젝트 하위의 node_modules를 VCS에 추가하는 방법을 생각해 볼 수 있습니다. 물론 이는 간단하게 의존성의 버전을 완전히 고정하는 하나의 방법이 될 수 있습니다. 하지만 의존성 모듈중에는 자바스크립트로만 이뤄진 것도 있지만 node-waf를 통해서 빌드가 필요한 바이너리 파일도 있는데 이러한 바이너리 파일은 시스템 의존적이기 때문에 VCS에서 다운받은 후에 다시 빌드해야 하는 문제가 있습니다. 또한 형상관리는 소스코드에서 생성해 낼 수 있는 것은 형상관리에 넣지 않는 것이 일반적입니다. 이는 중복이기 때문에 트래픽이나 관리의 낭비가 될 뿐입니다. 예를 들어 컴파일된 파일이나 자바에서 jar파일은 의존성에 넣지 않는 것이 그 예입니다. 더군다나 바이너리는 버전의 비교도 하기 어려울 뿐만 아니라 의존성을 업데이트했을 때 버전 비교에서 쓸데없는 비교결과만 나오기 때문에 그다지 좋은 방법이라고 할 수는 없습니다.




npm shrinkwrap
npm에서는 이 문제를 해결하기 위해서 1.1.2 버전부터 shrinkwrap라는 명령어를 추가했습니다. 1.1.2버전 미만의 npm을 사용한다면 업그래이드를 해야 사용할 수 있습니다. 이 내용은 처음에는 Dave Pacheco가 작성한 Managing Node.js dependencies with shrinkwrap을 번역해서 올릴려고 했지만 여러면 Dave한테 번역해도 되는지를 물었지만 대답이 없어서 그냥 직접 정리합니다. 더 자세한 내용은 Dave의 글을 참조하시면 됩니다. npm 개발팀에서는 앞에서 얘기한 중첩된 의존성 버전의 문제를 어떻게 관리할 수 있을 것인가를 고민한 끝에 shrinkwrap를 추가했습니다.

npm shrinkwrap의 문서를 보면 Lock down dependency versions라고 설명하고 있습니다. 즉, 의존성의 버전을 고정시키는 명령어입니다. 기존의 package.json의 장점은 개발자가 원하는 범위내의 의존성을 허용할 수 있는 장점이 있지만 상황에 따라 모든 의존성의 완전한 버전을 관리하기 위한 요구사항을 해결하기 위해서 추가로 shrinkwrap라는 명령어를 추가한 것입니다.(자주 사용하는 명령어는 아니라서인지 명령어가 입력하기 좋은 단어는 아니네요.) shrinkwarp의 사용법을 살펴보겠습니다. 위와 같이 개발에 사용하는 모듈이 로컬에 이미 설치된 상황에서 다음과 같이 명령어를 실행합니다.


$ npm shrinkwrap
wrote npm-shrinkwrap.json



프로젝트 디렉토리에서 npm shrinkwrap를 실행하면 프로젝트 루트에 npm-shrinkwrap.json이라는 파일이 생성됩니다.(물론 package.json도 여전히 존재합니다.) 앞에서 본 npm 모듈이 설치된 상황에서 생성한 npm-shrinkwrap.json 파일은 다음과 같이 작성됩니다.


{
  "name": "application-name",
  "version": "0.0.1",
  "dependencies": {
    "express": {
      "version": "2.5.8",
      "dependencies": {
        "connect": {
          "version": "1.8.5",
          "dependencies": {
            "formidable": {
              "version": "1.0.9"
            }
          }
        },
        "mime": {
          "version": "1.2.4"
        },
        "qs": {
          "version": "0.4.0"
        },
        "mkdirp": {
          "version": "0.3.0"
        }
      }
    },
    "jade": {
      "version": "0.21.0",
      "dependencies": {
        "commander": {
          "version": "0.5.2"
        },
        "mkdirp": {
          "version": "0.3.0"
        }
      }
    }
  }
}



package.json처럼 의존성에 대한 정보가 담겨있지만 package.json와는 다르게 의존성 모듈이 의존하고 있는 모듈의 버전정보까지도 담고 있습니다. npm shrinkwrappackage.json을 기반으로 하는 것이 아니라 현재 node_modules에 설치된 모듈의 버전에 기반해서 만들어집니다. node_modules에 설치된 모듈안에 node_modules를 계속 추적해 들어가서 모든 의존성 트리의 버전을 이용해서 npm-shrinkwrap.json을 만들어냅니다.

이렇게 프로젝트 루트에 package.json뿐만 아니라 npm-shrinkwrap.json이 같이 있는 경우에 npm install로 의존성을 설치하면 package.json을 따라 최신 모듈을 설치하는 것이 아니라 npm-shrinkwrap.json에 명시된 기준의 버전으로 설치가 됩니다.(여기서는 connect@1.8.6대신에 1.8.5가 설치되고 qs@4.2대신에 4.0버전이 설치됩니다.) 그리고 의존성에 대한 버전을 완벽하게 제어할 수 있습니다. npm-shrinkwrap.json가 있을 때도 npm update를 실행하면 package.json에 따라서 새로운 버전의 모듈이 있으면 업데이트를 합니다.(npm install에만 영향을 줍니다.) npm-shrinkwrap.json를 업데이트 하려면 npm update로 새로운 버전을 설치한 뒤에 다시 npm shrinkwrap를 실행해주면 됩니다. 이제 npm-shrinkwrap.json를 공유하면 언제나 완전히 같은 버전의 의존성을 가질 수 있습니다.

저는 회사에서 node.js를 사용한 것도 아니고 개인적인 데모페이지같은 것만 있기 때문에 사실 의존성에 대한 문제가 생기면 버그리포팅을 할 수 있는 장점(?)도 있기는 한데 이 중첩의존성은 사실 진지하게 node.js를 서비스하는데 사용한다면 충분히 고민될 만한 문제입니다. 적절한 타이밍에 npm개발팀에서 내놓은 이 shrinkwrap라는 해결책은 상당히 유연하면서도 괜찮은 접근이라고 생각합니다.
2012/03/17 04:02 2012/03/17 04:02