왜 지금 다시 압축 이야기인가
압축 알고리즘은 인프라 엔지니어에게 "한 번 정하면 몇 년 안 건드리는" 영역이다. 로그는 gzip, 배포 아티팩트는 tar.gz, 아카이브는 xz. 대충 이 조합으로 굴러간다. 그런데 이 결정이 생각보다 비싸다.
내가 겪은 사례 하나. 애플리케이션 로그를 하루 단위로 gzip 압축해서 S3에 올리는 파이프라인이 있었다. JSON 라인 포맷, 하루 원본 수백 GB 규모. gzip -6으로 대략 10:1 정도 나왔는데, 스토리지 비용 자체보다 문제였던 건 재처리였다. 장애 분석할 때 특정 기간 로그를 다시 훑어야 하면 압축 해제에 시간이 갈렸고, Glacier 전환 전 Standard-IA 구간에 쌓이는 용량이 계속 늘었다.
여기서 선택지가 갈린다. 압축률을 더 짜내려면 xz로 가야 하는데 압축 시간이 폭증한다. 속도를 원하면 zstd인데 최고 압축률에서도 xz를 이기긴 어렵다. 이 트레이드오프 곡선 위에서 매번 저울질하는 게 실무다.
BZip3는 이 곡선에 새로운 점을 하나 찍겠다는 프로젝트다. 이름에서 짐작되듯 bzip2의 정신적 후속작을 표방하고, 특히 텍스트와 소스 코드 압축에서 강점을 주장한다. 핵심 구성 요소는 세 가지다.
- 0차 문맥 혼합(context mixing) 엔트로피 코더
- 접미사 배열(suffix array) 기반 Burrows-Wheeler 변환
- RLE 및 LZP(Lempel-Ziv + Prediction) 전처리
bzip2를 써본 사람이면 알겠지만, bzip2는 2000년대 초반 이후 사실상 멈춰 있었다. 멀티코어 시대에 단일 스레드로 900KB 블록을 붙들고 있고, 압축률은 xz에 밀리고 속도는 gzip에 밀린다. "이도 저도 아닌 애매한 위치"라는 평가가 괜히 나온 게 아니다. BZip3는 그 BWT 계열의 아이디어를 현대적으로 다시 구현한 시도로 보면 된다.
동작 원리: 세 단계 파이프라인
BZip3의 압축 경로를 단계별로 보면 이해가 빠르다. 각 단계가 왜 있는지를 알아야 블록 크기 같은 튜닝 파라미터를 제대로 만질 수 있다.
1단계: LZP 전처리 — "이미 봤던 거지?"
LZP는 Lempel-Ziv 계열이지만 일반 LZ77과 다르다. LZ77은 "몇 바이트 뒤로 가서 몇 바이트를 복사해라"라는 (거리, 길이) 쌍을 저장한다. LZP는 거리를 저장하지 않는다. 대신 직전 N바이트의 문맥을 해시로 만들어, "이 문맥 다음에 뭐가 왔었는지"를 테이블에 기록해둔다. 압축할 때 예측이 맞으면 길이만 기록하고, 틀리면 원본 바이트를 그대로 흘린다.
디코더도 같은 방식으로 같은 테이블을 만들 수 있으니 거리 정보가 필요 없다. 로그 파일처럼 같은 패턴이 반복되는 데이터에서 특히 효율적이다. 예를 들어 이런 로그를 보자.
2024-01-15T03:22:11Z INFO [order-service] request completed status=200 latency=12ms
2024-01-15T03:22:11Z INFO [order-service] request completed status=200 latency=15ms
2024-01-15T03:22:12Z INFO [order-service] request completed status=200 latency=11ms
"Z INFO [order-service] request completed status=200 latency=" 부분은 앞 문맥만 보면 거의 확정적으로 예측된다. LZP가 여기를 통째로 걷어내고 나면, 뒤 단계에 남는 건 타임스탬프 끝자리와 latency 숫자 정도다. 즉 LZP는 BWT에 들어갈 데이터 양 자체를 줄여주는 역할이다. BWT는 블록 크기에 비례해서 무거워지니까, 이 전처리가 속도에 직접 기여한다.
2단계: 접미사 배열 BWT — 비슷한 문맥끼리 모으기
Burrows-Wheeler 변환은 데이터를 압축하지 않는다. 재배열할 뿐이다. 입력의 모든 회전(rotation)을 사전순 정렬한 뒤 마지막 열만 뽑아낸다. 결과적으로 "비슷한 문맥 뒤에 오는 글자들"이 한곳에 모인다.
영어 텍스트에서 "the"라는 문자열이 수천 번 나오면, "he " 문맥으로 시작하는 회전들이 정렬 후 인접하게 되고, 그 앞 글자인 't'가 연속으로 쭉 늘어선다. ttttttttttt... 같은 런이 생기는 것이다. 이걸 RLE와 엔트로피 코딩으로 넘기면 압축이 잘 먹는다.
여기서 bzip2와 갈리는 지점이 정렬 알고리즘이다. bzip2는 자체 구현한 정렬 루틴을 쓰는데, 반복 패턴이 극단적으로 많은 입력(예: 같은 바이트 수백만 개)에서 성능이 나빠지는 특성이 있어서 fallback 경로를 따로 뒀다. BZip3는 접미사 배열 구축 알고리즘(libsais 계열로 알려져 있으나 정확한 채택 라이브러리와 버전은 공식 저장소 확인 필요)을 쓴다. 접미사 배열은 선형에 가까운 시간에 구축 가능하고 최악 케이스가 훨씬 안정적이다. 그래서 BZip3는 bzip2의 900KB보다 훨씬 큰 블록을 감당할 수 있다.
블록이 크다는 게 왜 중요한가? BWT는 블록 내부에서만 패턴을 찾는다. 100MB짜리 로그를 900KB 블록으로 자르면, 1번 블록과 50번 블록에 똑같은 스택트레이스가 있어도 서로를 못 본다. 블록이 커질수록 멀리 떨어진 중복까지 잡아낸다. 이게 BZip3가 압축률에서 우위를 주장하는 구조적 근거다.
3단계: 문맥 혼합 엔트로피 코더
마지막은 실제로 비트를 줄이는 단계다. 전통적인 bzip2는 MTF(Move-To-Front) + 허프만 조합을 쓴다. BZip3는 문맥 혼합 방식의 산술 코더를 쓴다.
문맥 혼합은 PAQ 계열 압축기에서 유명해진 접근이다. "다음 비트가 1일 확률"을 여러 개의 예측 모델이 각각 내놓고, 그 예측들을 가중 평균으로 섞어서 최종 확률을 만든다. 예측이 정확할수록 산술 코더가 그 심볼에 배정하는 비트 수가 줄어든다. 허프만은 심볼당 최소 1비트라는 하한이 있지만 산술 코딩은 그 제약이 없어서, 편향이 심한 데이터에서 유리하다.
BZip3는 여기에 "0차"라는 수식어를 붙였다. PAQ처럼 수십 개 모델을 돌리는 무거운 방식은 아니고, BWT 출력이라는 이미 잘 정리된 데이터를 대상으로 적당히 가벼운 혼합을 한다는 뜻으로 읽힌다. 압축률과 속도 사이 균형점을 이쪽에 잡았다는 얘기다. 정확한 모델 구성은 공식 문서 확인 필요.
직접 돌려보기
설치는 배포판 패키지 매니저나 소스 빌드로 한다. Debian/Ubuntu 계열은 저장소에 올라와 있는 경우가 있고, 없으면 소스에서 빌드한다.
# 소스 빌드 (autotools 기반)
git clone https://github.com/kspalaiologos/bzip3
cd bzip3
./bootstrap.sh && ./configure && make -j$(nproc)
sudo make install
$ bzip3 --version
bzip3 1.x.x
# 실제 버전 문자열은 빌드 시점에 따라 다름
기본 사용법은 gzip과 거의 같다. 표준 입출력 파이프도 지원한다.
# 블록 크기 16MiB, 4스레드로 압축
$ bzip3 -e -b 16 -j 4 app.log
$ ls -l app.log*
-rw-r--r-- 1 dev dev 4194304 Jan 15 10:22 app.log.bz3
# 압축 해제
$ bzip3 -d app.log.bz3
# 파이프로 tar와 조합
$ tar -cf - ./logs | bzip3 -e -b 32 -j 8 > logs.tar.bz3
$ bzip3 -d < logs.tar.bz3 | tar -xf -
# tar에서 직접 호출
$ tar --use-compress-program='bzip3 -e -b 16 -j 4' -cf logs.tar.bz3 ./logs
비교 측정할 때는 이런 식으로 스크립트를 짜서 돌린다. 숫자는 데이터 성격에 따라 완전히 달라지니 반드시 본인 데이터로 재보라.
#!/bin/bash
F=$1
for CMD in "gzip -6 -c" "zstd -19 -T0 -c" "xz -9 -T0 -c" "bzip3 -e -b 16 -j 4 -c"; do
/usr/bin/time -f "%e s %M KB" -o /tmp/t \
bash -c "$CMD $F > /tmp/out" 2>/dev/null
SZ=$(stat -c%s /tmp/out)
printf "%-28s %10d bytes %s\n" "$CMD" "$SZ" "$(cat /tmp/t)"
done
내가 실제로 돌려본 감각을 정리하면 이렇다. 수치를 못 박지 않는 이유는 데이터 편차가 워낙 크기 때문이다.
- 구조화된 텍스트 로그, JSON 라인: BZip3가 xz -9와 비슷하거나 더 나은 압축률을 내면서 압축 시간은 훨씬 짧은 경우가 있었다. BZip3의 홈그라운드다.
- 소스 코드 아카이브: 여기도 유리하다. 반복되는 키워드와 들여쓰기 패턴이 BWT와 잘 맞는다.
- 이미 압축된 바이너리(JPEG, 컨테이너 레이어 tar.gz, DB 페이지 파일): 어떤 압축기를 써도 별 차이 없다. 오히려 CPU만 태운다.
- 메모리: 블록 크기 × 스레드 수에 대략 비례해서 늘어난다. 이게 실무에서 제일 자주 발목 잡는 부분이다.
실무 적용: 트레이드오프와 흔한 함정
함정 1: 메모리 폭발
BZip3의 압축률은 큰 블록에서 나온다. 그런데 블록 크기를 키우면 메모리도 같이 커진다. BWT는 입력 블록 전체를 메모리에 올린 채 접미사 배열까지 만들어야 한다. 접미사 배열 자체가 인덱스당 4바이트(또는 그 이상)를 먹으니, 블록 크기의 몇 배가 필요하다.
여기에 -j로 스레드를 늘리면 각 스레드가 각자의 블록을 잡는다. 메모리는 블록 크기 × 스레드 수로 곱해진다. 컨테이너에 512Mi 메모리 리밋을 걸어두고 CronJob으로 압축을 돌리다가 이런 걸 보게 된다.
$ bzip3 -e -b 256 -j 8 huge.log
bzip3: PANIC: Failed to allocate memory.
# 또는 커널이 먼저 죽인다
$ dmesg | tail -1
[12345.678] Out of memory: Killed process 4821 (bzip3) total-vm:6291456kB, ...
# Kubernetes에서는
$ kubectl describe pod log-archiver-xxxxx
State: Terminated
Reason: OOMKilled
Exit Code: 137
Exit Code 137은 SIGKILL(128+9)이다. 압축 잡이 조용히 실패하고 로그만 안 쌓이는 사고로 이어진다. 블록 크기와 스레드 수를 정할 때는 반드시 컨테이너 메모리 리밋을 역산해서 잡아라. 나는 리밋의 절반 이하를 목표로 잡고, `-j`를 먼저 줄이는 쪽을 택한다. 복호화 쪽도 마찬가지로 메모리가 필요하니, "압축은 빌드 서버에서 널널하게, 해제는 리소스 빡빡한 파드에서"라는 구성은 위험하다.
함정 2: 생태계 호환성
이게 제일 크다. gzip은 30년 넘게 어디에나 있다. zstd는 최근 몇 년 사이 커널, 파일시스템, 패키지 매니저까지 스며들었다. BZip3는 아직 그 위치가 아니다.
- S3 Select, Athena, Glue: gzip/bzip2/zstd 등은 알아듣지만 bz3는 못 읽는다. 쿼리 대상 로그를 bz3로 말면 그때부터 전처리 파이프라인을 따로 만들어야 한다.
- 언어별 라이브러리: Python
gzip/bz2/lzma는 표준 라이브러리다. BZip3는 별도 바인딩이 필요하고 성숙도도 다르다. 지원 현황은 공식 저장소 확인 필요. - OCI 컨테이너 이미지: 레이어 미디어 타입에 gzip과 zstd는 정의돼 있다. bz3는 없다. 컨테이너 배포에는 후보가 아니다.
- 운영 서버의 기본 설치 여부: 장애 대응 중에
bzip3: command not found를 보는 상황을 상상해보라. 압축 포맷을 고를 때 "새벽 3시에 이 파일을 열어야 하는 사람"을 항상 고려해야 한다.
함정 3: 스트리밍과 랜덤 액세스
BWT 계열은 블록 단위로 완결된다. 블록이 다 모여야 변환이 시작되니, 진짜 의미의 스트리밍 압축(들어오는 대로 즉시 출력)에는 맞지 않는다. 256MiB 블록을 쓰면 256MiB가 쌓일 때까지 출력이 안 나온다. 실시간 로그 전송 파이프라인 중간에 끼우면 지연이 튄다. 이럴 땐 zstd가 낫다.
반대로 블록 단위라는 게 장점이 되기도 한다. bzip2에 pbzip2가 있었듯, 블록 경계에서 자를 수 있으면 병렬 처리와 부분 해제 가능성이 열린다. BZip3의 부분 복호화 지원 여부와 인덱싱 방식은 공식 문서 확인 필요.
함정 4: 검증 없이 원본 삭제
이건 포맷과 무관하게 압축 파이프라인의 고전적 사고다. 새 압축기를 도입할 때는 특히 조심해야 한다. 아카이브 잡에서는 항상 이 순서를 지킨다.
# 압축 → 무결성 검증 → 해시 비교 → 그 다음에 원본 삭제
bzip3 -e -b 16 -j 4 -c app.log > app.log.bz3
bzip3 -t app.log.bz3 || { echo "integrity check failed"; exit 1; }
ORIG=$(sha256sum < app.log | cut -d' ' -f1)
REST=$(bzip3 -d -c app.log.bz3 | sha256sum | cut -d' ' -f1)
[ "$ORIG" = "$REST" ] || { echo "checksum mismatch"; exit 1; }
rm -f app.log
bzip2 시절에 bzip2: Data integrity error when decompressing. 로 며칠치 백업을 날린 사례를 여럿 봤다. 압축률 몇 퍼센트보다 이 검증 단계가 훨씬 값어치 있다.
정리: 언제 무엇을 쓸 것인가
한 줄 요약: BZip3는 "장기 보관용 텍스트 아카이브"라는 좁고 명확한 자리에서 xz의 강력한 경쟁자다. 그 밖의 자리에서는 zstd가 여전히 기본값이다.
워크로드별 판단 기준을 정리하면 이렇다.
- 실시간 로그 전송, API 응답, RPC 페이로드 → zstd 낮은 레벨(1~3) 또는 lz4. 지연이 전부다. BZip3는 후보 아님.
- 컨테이너 이미지, 패키지 배포 → gzip 또는 zstd. 생태계 지원이 결정적이다. 한 번 압축하고 수만 번 푸는 워크로드라 해제 속도가 중요하다.
- Athena/Spark로 쿼리할 데이터 레이크 → Parquet + zstd/snappy. 컬럼 포맷의 인코딩 이득이 압축기 선택보다 훨씬 크다.
- 몇 년 보관할 텍스트 로그/소스 아카이브, 접근 빈도 극히 낮음 → 여기가 BZip3를 진지하게 검토할 자리. xz -9와 나란히 놓고 본인 데이터로 벤치마크할 만하다. 단, 5년 뒤에도 디코더를 구할 수 있는지 함께 고민할 것.
- 기존 bzip2를 쓰고 있다면 → 사실상 갈아탈 이유가 충분하다. bzip2가 이기는 지점이 거의 없다. 다만 목적지가 BZip3일지 zstd일지는 접근 패턴에 달렸다.
도입한다면 이 순서를 권한다. ① 실제 프로덕션 데이터 샘플(최소 수 GB) 확보 → ② gzip/zstd/xz/bzip3 4종 벤치마크, 압축률·시간·피크 메모리 모두 기록 → ③ 복호화 시간과 메모리도 별도 측정 → ④ 비긴급 아카이브 일부에만 먼저 적용, 원본은 최소 한 달 병행 보관 → ⑤ 복구 리허설(실제로 풀어서 써보기)까지 하고 전환.
압축기 교체는 "압축률 몇 퍼센트" 게임이 아니라 "몇 년 뒤에 이 파일을 열 수 있는가" 게임이다. BZip3는 기술적으로 흥미롭고 특정 데이터에서 확실히 강하지만, 그 판단은 항상 본인 데이터와 본인 운영 환경 위에서 내려야 한다.
참고 자료
'Tech_News' 카테고리의 다른 글
| DNS 라이브러리를 걷어내면 드러나는 것들 — 남이 대신 내려준 40개의 결정 (0) | 2026.09.08 |
|---|---|
| AI SRE가 장애를 다 처리하면, 남는 건 아무도 못 고치는 장애뿐이다 (0) | 2026.09.07 |
| 백그라운드 잡이 조용히 사라지는 네 가지 경로와 각각의 차단법 (0) | 2026.09.05 |
| Gemini 3.8 Flash와 3.8 Flash Cyber: 6주에 세 번 나온 Flash, 인프라 담당자는 뭘 봐야 하나 (1) | 2026.09.04 |
| Darling으로 Linux에서 macOS 바이너리 돌리기 — Wine과 뭐가 다르고, CI에 쓸 수 있을까 (0) | 2026.09.03 |
