[PonsWarp 개념 교실 12] LZ4 블록 압축 — 속도를 위해 만든 비표준 포맷
쉽게 말하면, **LZ4는 "압축률은 포기하되 속도에 올인한 블록 압축"**이다. DEFLATE가 정밀한 두 단계 압축이라면, LZ4는 "반복 찾기" 한 가지만 빠르게 한다.

이번 글에서는 PonsWarp가 LZ4를 표준 프레임이 아닌 자체 블록 포맷으로 구현한 이유와, 그 선택이 가져온 대가를 본다. 이 이야기는 "구현했지만 쓰지 않은 것들" 편의 사전 단계이기도 하다.
이게 왜 필요한가 — "빠른 압축"이 따로 필요한 이유
DEFLATE는 압축률이 좋지만 CPU를 쓴다. 실시간 전송처럼 압축에 쓸 시간이 아까운 상황에서는 다른 균형이 필요하다.
- 압축 속도 ~400MB/s
- 해제 속도 ~1.5GB/s
- 압축률 ~50-60%
이 수치는 PonsWarp의 LZ4 모듈 주석에 적혀 있는 목표치다. DEFLATE보다 압축률은 낮지만, 해제가 매우 빨라서 전송받는 쪽에서의 부담이 적다. (참고: 이 수치는 코드 주석이며, 측정으로 검증된 값은 아니다.)
LZ4의 원리 — 반복 찾기 하나만
LZ4는 DEFLATE의 1단계(LZ77)만 가져온 것에 가깝다. 이미 나온 데이터에서 반복되는 부분을 "위치 + 길이"로 치환한다.
원본: ABCD ABCD ABCD
↑ 반복 발견
압축: ABCD [offset=4, len=8] ← "4칸 전부터 8글자 반복"
DEFLATE와의 차이는 허프만 코딩(2단계)이 없다는 점이다. 허프만은 압축률을 올리지만 그 자체가 비용이다. LZ4는 이 단계를 빼서 속도를 확보한다.
PonsWarp의 구현 상수는 이 "빠른 반복 찾기"를 위한 것들이다.
rust// pons-core-wasm/src/compression/lz4.rs const MIN_MATCH: usize = 4; // 4바이트 이상 반복만 매치 const MAX_DISTANCE: usize = 65535; // 64KB 전까지만 참조 (해시 테이블 한계) const HASH_LOG: usize = 14; // 2^14 = 16,384칸 해시 테이블 const ACCELERATION: usize = 1; // 매치 실패 시 스킵 정도
MIN_MATCH=4: 4바이트 미만의 반복은 압축 이득이 없어 무시MAX_DISTANCE=65535: 64KB 앞까지만 "아까 그거"로 참조 — 해시 테이블 크기와 상관HASH_LOG=14: 16,384칸 해시 테이블 — "지금 이 4바이트가 이전에 어디서 나왔는지"를 빠르게 찾음
비표준 포맷 — 4바이트 원본 크기 prefix
여기서 이 글의 핵심이 나온다. 표준 LZ4는 LZ4 프레임 포맷을 쓰는데, PonsWarp의 구현은 자체 블록 포맷이다.
rust// pons-core-wasm/src/compression/lz4.rs // 원본 크기 저장 (4 bytes, little endian) output.extend_from_slice(&(input.len() as u32).to_le_bytes());
압축 결과물은 4바이트 원본 크기 + 토큰 스트림이다. 이 포맷은:
- 장점: 구현이 단순하고 오버헤드가 작다. WASM용 경량 구현에 맞다.
- 대가: 표준 LZ4 도구와 호환되지 않는다. 다른 프로그램이 이 데이터를 LZ4로 해제하려면 실패한다.
표준 프레임 포맷은 헤더·플래그·체크섬 등 규격을 지켜야 해서 구현이 복잡하다. PonsWarp는 그 복잡성을 피하고 자체 포맷을 골랐다. "외부 호환성"보다 "내부 전송용 경량성"을 우선한 선택이다.
해제 안전장치 — 256MiB 상한
자체 포맷의 위험 중 하나는 원본 크기 필드를 신뢰하면 안 된다는 것이다. 압축 데이터가 조작되면 "원본이 4GB"라고 적혀 있을 수 있고, 그대로 Vec을 할당하면 메모리 폭발이다. 그래서 해제 시 상한을 둔다.
rust// pons-core-wasm/src/compression/lz4.rs const MAX_DECOMPRESS_SIZE: usize = 256 * 1024 * 1024; if original_size > MAX_DECOMPRESS_SIZE { return Err(JsValue::from_str("Decompressed size exceeds limit")); }
256MiB를 넘는 해제 요청은 거부한다. 이 상한은 진단 과정에서 추가된 안전장치로, 원래는 원본 크기 필드를 그대로 믿고 할당하는 구조였다. (진단 리포트 WC-07: "Lz4Decompress가 original_size: u32 신뢰 → 4GiB Vec 할당")
그런데 실제 전송 경로에서는?
정직하게 말하면, LZ4는 WASM 코어에 완전히 구현·export되어 있지만, 프론트엔드 전송 경로에서 호출하는 증거는 찾지 못했다. DEFLATE(ZIP64)는 실제로 쓰이지만, LZ4는 "구현해 두고 안 쓰는" 상태다.
이유는 설계상 추정 가능하다. 전송 경로는 STORE(ZIP64 압축 없음)를 쓰고, 암호화는 AES-GCM이 담당한다. 압축을 또 얹으면 CPU가 들고, 이미 압축된 파일(영상·이미지)은 이득이 없다. "있으면 좋을 것 같은" 기능이 실제로는 필요 없었던 경우다. 이 이야기는 "구현했지만 쓰지 않은 것들" 편에서 FEC·Merkle과 함께 깊게 다룬다.
PonsWarp 적용 지점
| 항목 | 값 | 증거 |
|---|---|---|
| 매치 최소 길이 | MIN_MATCH=4 | lz4.rs:10 |
| 참조 거리 상한 | MAX_DISTANCE=65535 (64KB) | lz4.rs:11 |
| 해시 테이블 | HASH_LOG=14 → 16,384칸 | lz4.rs:12-13 |
| 가속 | ACCELERATION=1 | lz4.rs:14 |
| 자체 포맷 | 4B 원본 크기(LE) + 토큰 스트림 | lz4.rs:41 |
| 해제 상한 | 256MiB 초과 거부 | lz4.rs:106-108 |
| 목표 성능(주석) | 압축 ~400MB/s, 해제 ~1.5GB/s | lz4.rs:6-8 |
| 전송 경로 | 프론트 호출 없음 (미사용) | 프론트 grep 무결과 |
지금 돌아보는 판단
세 가지를 정리한다.
첫째, "표준을 안 따르는" 선택은 호환성이라는 가격표가 붙는다. 자체 블록 포맷은 구현을 단순하게 만들었지만, 외부 도구와의 상호운용성을 포기했다. 재구축한다면 "이 데이터가 이 앱 밖으로 나갈 일이 있는가?"를 먼저 물어야 한다. 내부 전송 전용이면 자체 포맷이 합리적이고, 외부로 나갈 수 있으면 표준 포맷이 안전하다.
둘째, 크기 필드를 신뢰하지 않는 것이 보안의 기본이다. 원본 크기 필드는 "힌트"일 뿐, "명령"이 아니다. 256MiB 상한처럼 해제 전에 검증하는 안전장치는 필수다. 이건 LZ4만의 문제가 아니라 모든 압축·역직렬화에 적용되는 원칙이다.
셋째, "구현했다"와 "필요했다"는 다르다. LZ4는 완전히 구현됐지만 전송 경로에 안 쓰인다. 기능을 추가하기 전에 "정말 이 기능이 사용자에게 가치를 주는가"를 묻는 절차가 있었다면 아꼈을 작업이다. 이 교훈은 "구현했지만 쓰지 않은 것들" 편에서 FEC·Merkle과 함께 정리한다.
한 문장 정리
LZ4는 반복 찾기 하나로 속도에 올인한 블록 압축이고, PonsWarp는 4바이트 원본 크기 + 토큰 스트림의 자체 포맷으로 구현하되 256MiB 해제 상한으로 안전장치를 두었고, 실제 전송 경로에는 쓰지 않는다.
관련 개념: [개념 11 — DEFLATE] (속도 대비 압축률의 대척점) · [구현 12 — 구현했지만 쓰지 않은 것들] (다음 시리즈)
다음 글: [개념 13 — Reed-Solomon: GF(2^8) 이레이저 코딩]