250줄의 Rust로 Gzip 구현하기: 기술적 통찰과 커뮤니티의 냉혹한 리뷰
시니어 엔지니어로 일하다 보면 가끔 우리가 매일 공기처럼 사용하는 기반 기술의 내부가 궁금해질 때가 있다. Gzip은 인터넷 트래픽, 시스템 로그, 데이터베이스 백업 등 거의 모든 곳에 존재하지만, 정작 그 압축이 정확히 어떻게 풀리는지 바닥부터 코드로 설명할 수 있는 사람은 드물다.
최근 한 블로그에 올라온 “250줄의 Rust로 구현한 Gzip 압축 해제” 글은 바로 이런 엔지니어링적 호기심에서 출발했다. 수만 줄의 C 코드로 이루어진 레거시 프로젝트 대신, 핵심 알고리즘만 추려내어 작동 원리를 파악하겠다는 시도다. 오늘은 이 프로젝트의 기술적 디테일을 파헤쳐보고, Hacker News 커뮤니티에서 쏟아진 날카로운 비판들을 통해 우리가 배울 수 있는 점을 정리해 보겠다.
Gzip과 DEFLATE: 껍데기와 알맹이
Gzip 자체는 사실 아주 얇은 래퍼(Wrapper)에 불과하다. 실제 압축을 담당하는 핵심은 DEFLATE 알고리즘이다. Gzip 파일의 헤더는 매직 넘버(0x1F, 0x8B)로 시작하며, 약간의 메타데이터(예: 원본 파일명을 담는 FNAME 플래그)를 포함한다.
저자는 이 헤더를 파싱하는 아주 단순한 코드를 작성했다.
assert!(buf[0] == 0x1F && buf[1] == 0x8B, "not a gzip file");
let mut offset = 10; // skip the fixed header
if buf[3] == 8 { // FNAME flag
offset += buf[10..].iter().position(|&b| b == 0).unwrap() + 1;
}
여기서 실무적인 관점의 첫 번째 태클이 들어온다. 사양서에 따르면 파일 이름의 인코딩이 명확히 정의되어 있지 않다. 파일 시스템마다 허용하는 문자가 다르기 때문에 보안이나 호환성을 고려한다면 FNAME을 맹신하고 파싱하는 것은 위험할 수 있다. 하지만 교육용 토이 프로젝트라는 점을 감안하면 납득할 수 있는 생략이다.
DEFLATE의 두 기둥: Huffman Coding과 LZ77
DEFLATE는 두 가지 강력한 개념을 결합하여 압축률을 극대화한다.
첫째는 Huffman Coding 이다. 자주 등장하는 기호에는 짧은 비트를, 드물게 등장하는 기호에는 긴 비트를 할당하는 방식이다. DEFLATE는 비트 길이만으로 트리를 재구성할 수 있는 Canonical Huffman 코드를 사용한다. 둘째는 LZ77 이다. 데이터 스트림에서 이전에 등장했던 문자열 패턴을 찾아 “X바이트 전에서 Y바이트만큼 복사해라”라는 참조(Back-reference) 형태로 변환한다.
저자의 코드에서 가장 인상 깊은 부분은 비트 단위로 데이터를 읽어오는 로직이다. DEFLATE는 바이트 내에서 LSB(Least Significant Bit)부터 MSB(Most Significant Bit) 순서로 비트를 팩킹하기 때문에, 이를 처리하는 코드는 필연적으로 지저분해지기 마련이다.
fn bits(&mut self, need: i32) -> i32 {
let mut val = self.bit_buffer;
while self.bit_count < need {
val |= (self.nextbyte() as i32) << self.bit_count;
self.bit_count += 8;
}
self.bit_buffer = val >> need;
self.bit_count -= need;
val & ((1i32 << need) - 1)
}
과거 내가 어셈블리로 비트 조작을 하던 시절의 패턴과 거의 동일하다. Hacker News의 한 유저는 이를 Rust의 이터레이터 패턴으로 우아하게 풀려다 결국 컴파일된 어셈블리가 위와 같은 단순 시프트 연산과 동일하게 나온 것을 보고 감탄했다는 경험담을 공유하기도 했다. 고수준 추상화가 제로 코스트로 최적화되는 것은 언제 봐도 즐거운 일이다.
25,000줄의 C 코드 vs 250줄의 Rust 코드
저자는 zlib이 25,000줄의 C 코드(CVE의 온상이라며 농담까지 곁들인)라며 자신의 250줄 코드를 자랑스러워했다. 하지만 시니어 엔지니어들이 모인 Hacker News 커뮤니티의 반응은 냉혹했다.
- Production Ready의 무게: 프로덕션급 라이브러리가 수만 줄인 이유는 단순히 레거시 크러프트(Cruft) 때문이 아니다. CRC32 무결성 검증, 하드웨어 가속(AVX, SIMD 등), 멀티 플랫폼 호환성, 그리고 수십 년간 발견된 엣지 케이스들을 처리하기 위한 방어적 코드가 포함되어 있기 때문이다. 이를 250줄의 해피 패스(Happy path) 코드와 비교하는 것은 기만적이다.
- 숨겨진 원본의 존재: 누군가 이 코드가 사실
zlib저장소에 포함된 교육용 C 구현체인puff.c를 거의 1:1로 포팅한 수준이라는 것을 밝혀냈다. 함수 구조와 식별자 이름이 너무나 유사하여 LLM의 도움을 받았을 것이라는 강한 의심도 제기되었다. 저자가 이 사실을 글에서 명시하지 않은 것은 엔지니어링 윤리 측면에서 꽤 아쉬운 대목이다.
나의 생각: Error Handling과 토이 프로젝트의 한계
내 경험상, 코어 알고리즘을 짜는 것보다 예외 상황을 우아하게 처리하는 데 훨씬 더 많은 시간과 코드가 소요된다. 저자의 코드는 잘못된 입력이 들어오면 가차 없이 panic!을 발생시킨다. Rust 커뮤니티 일각에서 자바의 “Throw and forget” 문화를 답습하는 것 아니냐는 비판이 나온 것도 이 때문이다. 실무에서 Result<T, E>를 제대로 활용하지 않고 패닉을 남발하는 코드는 절대 PR(Pull Request)을 통과할 수 없다.
하지만 이 모든 비판에도 불구하고, 나는 이 프로젝트를 높게 평가한다.
위 사진처럼 직접 만든 250줄의 코드로 압축을 풀고, 심지어 curl로 구글의 압축된 응답을 받아 직접 해제해 보는 경험은 그 어떤 튜토리얼 문서보다 값지다.
결론
이 250줄의 Rust 코드를 당장 프로덕션에 도입할 생각은 버려라. CRC 체크도 없고 엣지 케이스에서 서버를 패닉 상태로 몰고 갈 것이다.
하지만 주말에 시간을 내어 DEFLATE 스펙 문서를 펴놓고, 바퀴를 다시 발명해 보는 것은 어떨까? 수천 줄의 최적화된 코드를 보며 압도당하기보다는, 동작하는 가장 단순한 형태를 직접 만들어 보는 것. 그것이 바로 주니어에서 시니어로, 그리고 그 이상으로 성장하는 가장 확실한 방법이다.
References
- Original Article: https://iev.ee/blog/gzip-decompression-in-250-lines-of-rust/
- Hacker News Thread: https://news.ycombinator.com/item?id=47499262