[PonsWarp 개념 교실 11] DEFLATE — zip64 속의 압축 (miniz_oxide)
쉽게 말하면, **DEFLATE는 "반복되는 부분을 '아까 그거'로 줄이고(LZ77), 자주 나오는 기호를 짧게 쓰는(허프만 코딩) 두 단계 압축"**이다.

ZIP64 편에서 압축이 옵션이라고 했는데, 그 옵션을 켰을 때 실제로 일어나는 일을 이번 글에서 본다. 특히 PonsWarp처럼 스트리밍으로 조각 단위 압축을 하는 경우의 설계를 중점적으로 다룬다.
이게 왜 필요한가 — "중복"이 곧 압축의 원료
파일에는 반복이 있다. 텍스트의 흔한 단어, 이미지의 같은 색, JSON의 반복되는 키. 압축의 본질은 이 중복을 찾아 짧게 표현하는 것이다.
DEFLATE는 이 중복을 두 단계로 처리한다.
1단계: LZ77 — "아까 그거"로 줄이기
LZ77은 이미 나온 데이터 안에서 반복을 찾아 "위치 + 길이"로 치환한다.
원본: ABCABCABC
↑↑↑ 반복 발견
압축: ABC (3,6)
└ 3번째 위치부터 6글자 = "ABCABC" 반복
"ABCABCABC"가 "ABC" + "6글자 더"로 줄었다. 이 방식은 압축된 데이터를 풀 때도 이미 풀린 앞부분만 보면 되므로 스트리밍에 적합하다. 압축 해제기가 "뒤를 기다릴" 필요가 없다.
2단계: 허프만 코딩 — 자주 나오는 기호를 짧게
LZ77로 줄인 결과도 여전히 바이트 단위다. 허프만 코딩은 자주 나오는 기호에는 짧은 비트, 드문 기호에는 긴 비트를 배정한다.
기호 빈도 코드
A 8회 0 ← 자주 → 짧게
B 2회 10
C 1회 110
D 1회 111
평균 비트 수가 바이트 단위(8비트)보다 줄어든다. DEFLATE는 LZ77 결과 + 허프만 코드 테이블을 합쳐 저장한다.
DEFLATE = LZ77 (반복 치환) + 허프만 (빈도 압축)
PonsWarp에선 어떻게 쓰나 — miniz_oxide 래퍼
PonsWarp의 WASM 코어는 자체 압축 구현 대신 miniz_oxide(Rust의 DEFLATE 구현)를 래핑해서 쓴다. 이 선택이 중요하다 — 암호화와 달리 압축은 검증된 라이브러리를 재사용했다.
rust// pons-core-wasm/src/zip64/deflate.rs use miniz_oxide::deflate::core::{ compress, create_comp_flags_from_zip_params, CompressorOxide, TDEFLFlush, TDEFLStatus, }; pub struct DeflateState { compressor: CompressorOxide, // miniz_oxide 압축기 crc_hasher: Crc32Hasher, // 압축과 무관한 원본 CRC도 함께 추적 total_in: u64, total_out: u64, output_buffer: Vec<u8>, }
핵심은 두 가지다.
1. 압축기 상태를 유지한다. CompressorOxide는 이전 청크의 상태(딕셔너리)를 기억한다. 그래서 조각을 하나씩 넣어도 전체 파일을 압축하는 것과 같은 결과를 낸다. 스트리밍 압축의 핵심이다.
2. 압축과 별개로 CRC도 추적한다. ZIP은 원본 데이터의 CRC를 요구한다. 압축된 조각을 내보내면서 동시에 Crc32Hasher로 원본 CRC를 갱신한다. 압축 여부와 무관하게 CRC는 항상 원본 기준이다.
스트리밍 압축 — Sync flush
조각 단위 압축에서 중요한 문제가 하나 있다. 압축기는 버퍼를 채우기 위해 입력을 모아두는 경향이 있다. 조각마다 출력을 즉시 내보내려면 flush를 해야 한다.
rust// pons-core-wasm/src/zip64/deflate.rs TDEFLFlush::Sync, // Sync flush로 즉시 출력
Sync flush는 "지금까지의 입력을 압축해서 바로 내보내되, 압축기 상태는 유지한다"는 의미다. 이 덕분에 수신측이 조각을 받는 대로 ZIP 바이트를 디스크에 쓸 수 있다. 마지막에 Finish flush로 남은 데이터를 모두 내보내고 종료한다.
청크1 → compress_chunk(Sync) → 출력1 → 디스크
청크2 → compress_chunk(Sync) → 출력2 → 디스크
...
마지막 → finish(Finish) → 남은 출력 → 디스크
PonsWarp 적용 지점
| 항목 | 값 | 증거 |
|---|---|---|
| 라이브러리 | miniz_oxide (자체 구현 아님) | deflate.rs:3 |
| 압축 상태 | CompressorOxide (청크 간 상태 유지) | deflate.rs:7-13 |
| 레벨 | 0~9, create_comp_flags_from_zip_params(level, -15, 0) | deflate.rs:16-18 |
| 조각 압축 | compress_chunk + TDEFLFlush::Sync | deflate.rs:30-77 |
| 종료 | finish + TDEFLFlush::Finish | deflate.rs:78-109 |
| CRC 병행 | Crc32Hasher로 원본 CRC 추적 | deflate.rs:111-114 |
| ZIP64 연결 | stream.rs가 DeflateState::new(level) 생성 | stream.rs:51-52 |
| STORE 선택 | level 0 = 압축 없음 (전송 경로 기본) | stream.rs:25 |
지금 돌아보는 판단
세 가지를 정리한다.
첫째, "어디서 직접 구현하고 어디서 라이브러리를 쓰는지"가 일관된 기준을 가져야 한다. 이 프로젝트는 AES/SHA-256/HKDF를 직접 구현했지만 DEFLATE는 miniz_oxide를 쓴다. 암호화는 "직접 구현 = 감사 부담"이 크고 압축은 "검증된 구현 재사용"이 합리적이지만, 이 기준이 코드에 명시적으로 남아 있지 않아 재구축할 때 혼란을 줄 수 있다. [INFERENCE] — 문서화된 기준은 없고 구현 선택만 보인다.
둘째, "상태 유지 + Sync flush"가 스트리밍 압축의 전부다. 상태를 유지하지 않으면 조각마다 별개 파일처럼 압축되어 압축률이 떨어지고, flush하지 않으면 출력이 늦어진다. 이 두 축이 맞물려 "받으면서 동시에 압축된 ZIP을 쓴다"가 가능해진다.
셋째, CRC는 압축과 무관하게 원본을 추적한다. ZIP을 푸는 쪽은 "원본 파일이 맞는지"를 CRC로 확인한다. 압축 여부와 관계없이 원본 기준이어야 하므로, 압축 경로에서도 별도 hasher로 원본 CRC를 관리하는 구조가 정확하다.
한 문장 정리
DEFLATE는 LZ77(반복 치환) + 허프만(빈도 압축)의 두 단계 압축이고, PonsWarp는 검증된 miniz_oxide를 래핑해 압축기 상태를 유지하며 Sync flush로 조각 단위 출력을 내보내 스트리밍 ZIP을 만든다.
관련 개념: [개념 10 — ZIP64 스트리밍] (압축이 얹히는 컨테이너)
다음 글: [개념 12 — LZ4 블록 압축: 속도를 위해 만든 비표준 포맷]