왜 압축 방식 선택이 문제가 되는가

HTTP 응답 압축은 전송량을 줄여 응답 시간을 단축한다. 하지만 압축은 공짜가 아니다. 압축률을 높일수록 CPU 시간이 늘어나고, 트래픽이 몰리면 압축 자체가 서버의 병목이 된다. gzip은 오래 검증된 표준이고, Brotli는 정적 자산에서 더 높은 압축률을 낸다. 문제는 "어느 쪽이 좋은가"가 아니라 "정적/동적 응답에 각각 어떤 레벨을 쓸 것인가"이다. 이를 잘못 잡으면 대역폭은 아꼈지만 CPU가 포화되거나, CPU는 여유롭지만 트래픽 비용이 새는 상황이 생긴다.

gzip과 Brotli의 특성 비교

두 알고리즘은 압축률과 압축 속도의 곡선이 다르다. Brotli는 최고 레벨(11)에서 압축률이 우수하지만 인코딩이 매우 느려 동적 응답에는 부적합하다. 반대로 낮은 레벨에서는 gzip과 비슷한 속도로 약간 더 나은 결과를 낸다.

항목gzipBrotli
정적 자산 압축률기준약 15~25% 더 작음
높은 레벨 인코딩 속도빠름느림(레벨 10~11)
낮은 레벨 인코딩 속도빠름gzip과 유사(레벨 4~5)
브라우저 지원사실상 전부HTTPS 환경 대부분

핵심 전략: 정적은 사전 압축, 동적은 낮은 레벨

실무의 원칙은 단순하다. 빌드 시점에 정적 파일을 brotli 11과 gzip 9로 미리 압축해 두고, nginx는 요청 때마다 압축하지 않고 저장된 파일을 그대로 내보낸다. CPU를 한 번만 쓰고 이후에는 대역폭 이득만 취한다. 동적 API 응답은 미리 압축할 수 없으므로 실시간 압축을 쓰되 레벨을 낮게 잡는다.

정적 파일 사전 압축과 nginx 설정

빌드 단계에서 .br, .gz 파일을 함께 생성한다.

#!/usr/bin/env bash
# 배포 아티팩트를 사전 압축 (CI 빌드 단계)
find dist -type f \
  \( -name '*.js' -o -name '*.css' -o -name '*.svg' -o -name '*.json' \) \
  -print0 | while IFS= read -r -d '' f; do
    brotli -q 11 -f -k "$f"      # $f.br 생성
    gzip -9 -f -k "$f"           # $f.gz 생성
done

nginx는 ngx_brotli와 기본 gzip_static 모듈로 저장된 파일을 우선 사용한다.

http {
    # 정적 사전 압축 파일 우선 사용 (런타임 압축 없음)
    brotli_static on;
    gzip_static   on;

    # 동적 응답용 실시간 압축 (레벨을 낮게)
    gzip          on;
    gzip_comp_level 4;
    gzip_min_length 1024;
    gzip_types    application/json application/javascript text/css text/plain;

    brotli        on;
    brotli_comp_level 4;          # 동적은 4~5 권장
    brotli_min_length 1024;
    brotli_types  application/json application/javascript text/css;
}

CPU 영향 측정하기

레벨을 정하기 전에 실제 응답 크기와 인코딩 시간을 재봐야 한다. 대표 페이로드로 레벨별 비용을 비교하면 감이 아니라 숫자로 판단할 수 있다.

import gzip, time, brotli

data = open("sample_api_response.json", "rb").read()
orig = len(data)

for lvl in (1, 4, 6, 9):
    t = time.perf_counter()
    out = gzip.compress(data, lvl)
    ms = (time.perf_counter() - t) * 1000
    print(f"gzip {lvl}: {len(out)/orig:.2%}  {ms:.2f}ms")

for lvl in (4, 5, 9, 11):
    t = time.perf_counter()
    out = brotli.compress(data, quality=lvl)
    ms = (time.perf_counter() - t) * 1000
    print(f"brotli {lvl}: {len(out)/orig:.2%}  {ms:.2f}ms")

대개 동적 응답에서 레벨 4를 넘어서면 크기 감소는 미미한데 CPU 시간은 급격히 늘어난다. 이 손익분기점이 설정 근거가 된다.

흔한 실수와 주의점

  • 동적 응답에 brotli 11 사용: 응답마다 수십~수백 ms의 CPU를 소모해 부하 급증 시 지연이 폭발한다. 동적은 반드시 낮은 레벨.
  • 이미 압축된 콘텐츠 재압축: jpg, png, mp4, woff2는 압축 이득이 없고 CPU만 쓴다. *_types에서 제외한다.
  • 작은 응답 압축: 수백 바이트 응답은 압축 헤더 오버헤드로 오히려 커질 수 있어 min_length로 걸러야 한다.
  • 사전 압축 갱신 누락: 원본만 배포하고 .br/.gz를 갱신하지 않으면 오래된 압축본이 나갈 수 있으니 빌드 파이프라인에 함께 묶는다.

정리

압축은 CPU와 대역폭을 맞바꾸는 작업이다. 정적 자산은 빌드 시점에 최고 레벨로 사전 압축해 CPU를 한 번만 쓰고, 동적 응답은 낮은 레벨의 실시간 압축으로 지연과 부하를 통제한다. 레벨 선택은 대표 페이로드의 실측 손익분기점에 근거해야 하며, 이미 압축된 콘텐츠와 작은 응답은 대상에서 제외하는 것이 기본이다.