얼마전에, 회사일로 Git의 pack에서 object를 가져오는 기능을 node.js 기반으로 구현하다가 문제를 만났다.
pack은 대략 아래와 같은 포맷으로 구성되어있다. (엉성해서 죄송)
...hhzzzzzzzzzzzzhhzzzzzzzzzzzhhzzzzzzzzz...
(h는 헤더, z는 deflate로 압축된 영역)
pack파일은 git의 object들을 합쳐놓은 파일이므로, 크기가 굉장히 커질 수 있다. 저장소의 모든 object가 pack 파일 하나에 다 들어가는 경우도 얼마든지 있을 수 있다. 따라서 object하나만 얻으면 되는 상황에서 파일 전체를 읽는 것은 비효율적이므로, 원하는 부분만 정확히 읽어들일 필요가 있다.
근데 그게 참 쉽지 않은데, pack에서 object하나를 얻을 때, 어디서부터 읽어야 하는지는 쉽게 알 방법이 존재하지만(인덱스 파일이 있다) 어디까지 읽어야 하는지는 읽어보지 않고서는 모르기 때문이다. 파일을 읽고 압축을 풀어봐야 알 수 있다.
따라서 나는 이 문제를, ReadStream에서 파일을 읽고 InflateStream에게 넘겨준 뒤, 압축해제가 완료되면 파일 읽기를 중단하도록 하면 해결될 것으로 생각했다. 코드는 아래와 같다.
in = require('fs').createReadStream('pack-031a12679260de44c3e123e42d17e5f7803ab016.pack') inflate = require('zlib').createInflate(); out = process.stdout; in.pipe(inflate).pipe(out)
그러나 이 코드는 두 가지 이유때문에 의도대로 동작하지 않는다:
- InflateStream에서 압축해제가 완료되어도 아무런 이벤트가 발생하지 않는다. 그래서 끝났는지 알 길이 없다.
- 압축해제가 완료되었을 때 이벤트가 발생한다 해도, ReadStream에게 더 이상 파일을 읽을 필요가 없음을 알려줄 방법이 없다.
일단 첫번째 문제는 node.js의 zlib 구현에 문제가 있는 것이라 생각했다. 그래서 압축해제가 완료되면(Z_STREAM_END가 리턴되면) end 이벤트(아이작이 close가 아니라 end가 맞다고 조언해주었다)가 발생하도록 고치고 pull request를 보냈다.
두번째 문제는 깔끔한 해결책을 찾지 못했다. 그냥 inflate에서 end 이벤트가 발생하면 input.destory()를 호출해서 강제로 파일 읽기를 중단시켰다.
inflate.on('end', function() { input.destroy(); });
이 문제에 대해서는 Nodeconf 2012에서 Stream에 대한 주제로 발표를 했던 Yammer의 Marco Rogers에게도 도움을 요청한 상태이다. 뭔가 깔끔한 해결책이 나와주면 좋겠지만 그렇지 못한다면 그냥 이대로 해야할지도 모르겠다.