압축 알고리즘 — 허프만에서 gzip까지, HTTP 압축의 원리
브라우저 개발자 도구에서 응답 크기를 보면 "transferred" 크기가 "resource" 크기보다 훨씬 작습니다. 어떻게 줄인 걸까요?
개념 정의
데이터 압축 은 정보를 더 적은 비트로 표현하는 기술입니다. 웹에서는 서버 응답을 압축하여 전송량을 줄이고, 페이지 로딩을 빠르게 합니다.
압축은 크게 두 종류입니다.
- **무손실 압축 **: 원본을 완벽히 복원 (gzip, zstd)
- ** 손실 압축 **: 약간의 정보 손실 허용 (JPEG, MP3)
여기서는 백엔드에서 주로 사용하는 무손실 압축을 다룹니다.
왜 필요한가
- ** 네트워크 대역폭 절약 **: API 응답을 60~80% 줄일 수 있습니다
- ** 페이지 로딩 속도 **: 전송 데이터가 적으면 빠릅니다
- ** 비용 절감 **: 클라우드에서 아웃바운드 트래픽은 비용이 됩니다
- ** 로그 저장 **: 대량 로그 파일의 저장 공간 절약
허프만 코딩 (Huffman Coding)
핵심 아이디어
자주 나오는 문자에는 ** 짧은 코드 **, 드문 문자에는 ** 긴 코드 **를 부여합니다. 전체 비트 수가 최소화됩니다.
문자열: "AABABCAAB"
빈도: A=5, B=3, C=1
고정 길이: A=00, B=01, C=10 → 9 × 2 = 18비트
허프만: A=0, B=10, C=11 → 5×1 + 3×2 + 1×2 = 13비트
절약: 약 28%
허프만 트리 구성
public class HuffmanCoding {
static class Node implements Comparable<Node> {
char ch;
int freq;
Node left, right;
Node(char ch, int freq) { this.ch = ch; this.freq = freq; }
@Override
public int compareTo(Node o) { return this.freq - o.freq; }
}
public Map<Character, String> buildCodes(String text) {
// 1. 빈도 계산
Map<Character, Integer> freq = new HashMap<>();
for (char c : text.toCharArray()) {
freq.merge(c, 1, Integer::sum);
}
// 2. 우선순위 큐로 허프만 트리 구성
PriorityQueue<Node> pq = new PriorityQueue<>();
freq.forEach((ch, f) -> pq.offer(new Node(ch, f)));
while (pq.size() > 1) {
Node left = pq.poll(); // 빈도 가장 낮은 두 개를
Node right = pq.poll();
Node parent = new Node('\0', left.freq + right.freq);
parent.left = left; // 합쳐서 새 노드 생성
parent.right = right;
pq.offer(parent);
}
// 3. 트리를 순회하며 코드 생성
Map<Character, String> codes = new HashMap<>();
buildCodesRecursive(pq.poll(), "", codes);
return codes;
}
private void buildCodesRecursive(Node node, String code, Map<Character, String> codes) {
if (node == null) return;
if (node.left == null && node.right == null) {
codes.put(node.ch, code.isEmpty() ? "0" : code);
return;
}
buildCodesRecursive(node.left, code + "0", codes);
buildCodesRecursive(node.right, code + "1", codes);
}
}
접두사 코드 특성
허프만 코드는 ** 접두사 코드(prefix code)**입니다. 어떤 코드도 다른 코드의 접두사가 아니므로, 구분자 없이 연속으로 붙여도 유일하게 디코딩할 수 있습니다.
A=0, B=10, C=11
인코딩: 01010110 → 0|10|10|11|0 → ABBCA
모호함 없음!
LZ77 알고리즘
핵심 아이디어
이전에 나왔던 문자열을 ** 참조 **합니다. "앞에서 5글자 전에 3글자 복사"처럼 (거리, 길이, 다음 문자) 트리플로 표현합니다.
텍스트: "ABCABCABC"
처리:
위치 0: A → (0, 0, 'A') 새 문자
위치 1: B → (0, 0, 'B') 새 문자
위치 2: C → (0, 0, 'C') 새 문자
위치 3: ABCABC → (3, 6, ?) 3글자 전에서 6글자 복사!
슬라이딩 윈도우
LZ77은 ** 슬라이딩 윈도우 **를 사용합니다. 이미 처리한 부분(탐색 버퍼)에서 현재 위치의 문자열과 일치하는 가장 긴 부분을 찾습니다.
// LZ77 개념적 인코딩
public List<int[]> compress(String text) {
List<int[]> result = new ArrayList<>();
int i = 0;
int windowSize = 4096; // 탐색 윈도우 크기
while (i < text.length()) {
int bestDist = 0, bestLen = 0;
// 슬라이딩 윈도우에서 가장 긴 매칭 찾기
int searchStart = Math.max(0, i - windowSize);
for (int j = searchStart; j < i; j++) {
int len = 0;
while (i + len < text.length() &&
text.charAt(j + len) == text.charAt(i + len)) {
len++;
}
if (len > bestLen) {
bestLen = len;
bestDist = i - j;
}
}
if (bestLen > 2) { // 매칭이 충분히 길면 참조
result.add(new int[]{bestDist, bestLen});
i += bestLen;
} else { // 리터럴(원래 문자 그대로)
result.add(new int[]{0, text.charAt(i)});
i++;
}
}
return result;
}
Deflate = LZ77 + 허프만
Deflate 는 gzip의 핵심 알고리즘으로, LZ77과 허프만 코딩을 결합합니다.
1단계: LZ77으로 중복 문자열을 (거리, 길이) 쌍으로 치환
2단계: 허프만 코딩으로 결과를 최적 인코딩
원본 → [LZ77] → 중간 표현 → [허프만] → 압축 데이터
gzip은 Deflate + 헤더/체크섬으로 구성됩니다.
HTTP 압축
요청/응답 흐름
클라이언트 → 서버:
Accept-Encoding: gzip, deflate, br
서버 → 클라이언트:
Content-Encoding: gzip
(압축된 응답 본문)
주요 압축 형식
| 형식 | 알고리즘 | 압축률 | 속도 | 지원 |
|---|---|---|---|---|
| gzip | Deflate | 좋음 | 빠름 | 거의 모든 브라우저 |
| br (Brotli) | LZ77 + 허프만 변형 | 매우 좋음 | 중간 | 모던 브라우저 |
| zstd | Zstandard | 매우 좋음 | 매우 빠름 | 신규 지원 확대 중 |
Brotli는 gzip보다 20~30% 더 압축하지만, 압축 시간이 더 걸립니다. 정적 파일은 미리 압축해두고(pre-compression), 동적 응답은 gzip을 쓰는 전략이 많습니다.
Spring 응답 압축 설정
# application.yml
server:
compression:
enabled: true
mime-types: application/json,application/xml,text/html,text/plain
min-response-size: 1024 # 1KB 이상인 응답만 압축
// 프로그래밍 방식
@Bean
public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> customizer() {
return factory -> {
Compression compression = new Compression();
compression.setEnabled(true);
compression.setMimeTypes(new String[]{
"application/json", "text/html", "text/plain"
});
compression.setMinResponseSize(DataSize.ofKilobytes(1));
factory.setCompression(compression);
};
}
압축 대상 선택
✓ 압축해야 할 것: JSON, HTML, CSS, JS, XML, SVG
✗ 압축하면 안 되는 것: JPEG, PNG, MP4 (이미 압축됨 → 오히려 커질 수 있음)
압축률 비교
| 데이터 유형 | gzip 압축률 | Brotli 압축률 |
|---|---|---|
| JSON API 응답 | 60~80% | 65~85% |
| HTML 페이지 | 70~85% | 75~90% |
| JavaScript | 60~75% | 65~80% |
| 이미 압축된 이미지 | 0~5% | 0~5% |
백엔드/실무 연결
로그 압축
대량의 로그를 저장할 때 gzip으로 압축하면 10배 이상 줄어들기도 합니다.
// Java에서 GZip 압축
try (GZIPOutputStream gzip = new GZIPOutputStream(new FileOutputStream("app.log.gz"))) {
gzip.write(logData.getBytes());
}
CDN에서의 압축
CloudFlare, AWS CloudFront 같은 CDN은 자동으로 응답을 압축합니다. Origin 서버에서 압축하지 않아도 CDN 엣지에서 gzip/Brotli를 적용할 수 있습니다.
gRPC와 Protocol Buffers
gRPC는 JSON 대신 Protocol Buffers(바이너리 직렬화)를 사용하여 데이터 크기를 줄입니다. 여기에 gzip 압축까지 적용하면 전송량이 극적으로 줄어듭니다.
주의할 점
이미 압축된 데이터를 다시 압축하여 오히려 크기 증가
JPEG, PNG 등 이미 압축된 파일을 gzip으로 다시 압축하면 오히려 크기가 커질 수 있습니다. HTTP 압축 설정 시 이미지, 동영상 등은 제외해야 합니다.
Spring에서 작은 응답까지 압축하여 CPU 낭비
Spring Boot의 server.compression.min-response-size를 너무 작게 설정하면, 수십 바이트 응답까지 압축하여 CPU를 낭비합니다. 기본값(2KB) 이상으로 유지하는 것이 좋습니다.
정리
- **허프만 코딩 **: 빈도 기반 가변 길이 코드로, 자주 나오는 데이터에 짧은 코드를 부여합니다
- LZ77: 이전에 나온 문자열을 (거리, 길이)로 참조하여 중복을 제거합니다
- Deflate (gzip): LZ77 + 허프만의 조합으로, 웹에서 가장 많이 사용됩니다
- Brotli: gzip보다 20~30% 더 압축하며, 모던 브라우저가 지원합니다
- Spring에서
server.compression.enabled=true로 쉽게 활성화할 수 있습니다 - JSON, HTML 등 텍스트 데이터는 60~85% 압축되지만, 이미 압축된 미디어는 효과가 없습니다