본문으로 건너뛰기
김신건의 로그

HPACK

· 수정 · 📖 약 4분 · 1,585자/단어 #network #http #http2 #compression #security
HPACK 헤더 압축, hpack, HTTP/2 헤더 압축, HTTP header compression

정의

HPACK (RFC 7541)은 HTTP/2에서 요청/응답 헤더를 압축하는 방식이다.

HTTP/1.1에서 헤더는 매 요청마다 평문 텍스트로 반복 전송되었다. 한 페이지 로드에 수십 건의 요청이 발생하면 헤더만으로 수십 KB가 낭비된다. HPACK은 정적 테이블 + 동적 테이블 + Huffman 인코딩 세 가지를 조합해 헤더 중복을 제거한다.

문제 상황: HTTP/1.1 헤더의 낭비

GET /api/users HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: application/json
Accept-Language: ko-KR,ko;q=0.9
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Cookie: session_id=abc123; _ga=GA1.2.xxx

동일한 호스트에 50개 병렬 요청을 보내면, 위 헤더가 50번 그대로 반복된다. Authorization, Cookie처럼 큰 헤더가 매번 전송되어 모바일처럼 대역폭이 제한된 환경에서 특히 비효율적이다.

압축 원리

Static Table (정적 테이블)

자주 쓰이는 헤더 이름/값 조합 61개를 사전 정의한 고정 인덱스.

Index | Header Name          | Header Value
------+----------------------+-------------------
1     | :authority           |
2     | :method              | GET
3     | :method              | POST
4     | :path                | /
5     | :path                | /index.html
6     | :scheme              | http
7     | :scheme              | https
8     | :status              | 200
...
61    | www-authenticate     |

:method GET 이 필요하면 인덱스 2 한 바이트만 전송하면 된다.

Dynamic Table (동적 테이블)

연결 단위로 양쪽(클라이언트/서버)이 공유하는 헤더 캐시.

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C,S: 첫 번째 요청
    C->>S: "Authorization: Bearer TOKEN123" (리터럴로 전송 + 동적 테이블 인덱스 62 등록)
    S-->>C: 200 OK

    Note over C,S: 두 번째 요청
    C->>S: 인덱스 62 (1 바이트) - 동일 헤더
    S-->>C: 200 OK

동적 테이블은 FIFO 큐로 구성된다. 오래된 항목은 새 항목이 추가될 때 밀려나며, 테이블 최대 크기는 SETTINGS_HEADER_TABLE_SIZE 프레임으로 협상한다 (기본값 4096 bytes).

Huffman 인코딩

문자열로 전송해야 하는 헤더 값을 Huffman 코드로 추가 압축한다. HTTP 트래픽에서 자주 등장하는 문자에 짧은 코드를 배정해 ASCII 대비 약 30% 추가 절약을 달성한다.

'e' (0x65) → ASCII 8bit, Huffman 4bit (0b0000)
'a' (0x61) → ASCII 8bit, Huffman 5bit (0b00001)
'q' (0x71) → ASCII 8bit, Huffman 8bit (0b11101000)

인코딩 플로우

flowchart TD
    H["헤더 필드\n(name: value)"]
    Q1{"Static Table에\n존재하는가?"}
    Q2{"Dynamic Table에\n존재하는가?"}
    I1["인덱스 하나만 전송\n(1-2 바이트)"]
    I2["Dynamic Table 인덱스\n전송 (1-2 바이트)"]
    Q3{"Dynamic Table에\n추가할까?"}
    L1["리터럴 + 동적 등록\n(Huffman 압축 후)"]
    L2["리터럴만 전송\n(인덱싱 안 함)"]

    H --> Q1
    Q1 -->|"Yes"| I1
    Q1 -->|"No"| Q2
    Q2 -->|"Yes"| I2
    Q2 -->|"No"| Q3
    Q3 -->|"Yes"| L1
    Q3 -->|"No"| L2

헤더 값이 자주 바뀌는 경우 (예: Date, Content-Length) 는 동적 테이블 등록을 건너뛰어 테이블 공간 낭비를 방지한다. Cache-Control: no-cache처럼 민감 정보가 포함된 헤더는 Never Indexed 플래그로 중간 프록시의 캐시 저장을 금지할 수 있다.

압축 효과

시나리오HTTP/1.1HTTP/2 + HPACK
페이지 로드 50개 요청 (각 ~700 B)~35 KB 헤더~3-5 KB
모바일 네트워크 Slow Start 구간큰 영향작은 영향
동일 호스트 반복 API 호출매번 전체 전송첫 번째 이후 수 바이트

HTTP/2의 멀티플렉싱과 함께 HPACK은 HTTP/2의 두 번째 큰 성능 이득원이다.

보안 이슈: CRIME / HPACK Bomb

CRIME 공격

CRIME (Compression Ratio Info-leak Made Easy)은 TLS 압축 + HTTP 응답 압축을 함께 쓸 때 발생하는 사이드 채널 공격이다.

공격자가 임의 문자열을 요청에 주입할 수 있는 상황에서, 압축된 패킷 크기 변화를 관찰하여 헤더 값(쿠키, 세션 토큰 등)을 추측한다.

압축 전: "Cookie: secret=a..." → 크기 100
     +  "secret=a"          → 압축 시 중복 제거, 크기 95 (추측 성공)
          
압축 전: "Cookie: secret=a..." → 크기 100
     +  "secret=b"          → 다른 문자열, 중복 없음, 크기 102 (추측 실패)

HPACK은 이를 완전히 해결하지는 않는다. 공격자가 동일 HTTP/2 연결에서 요청을 보낼 수 있다면 동적 테이블 크기 변화를 관찰하는 비슷한 공격이 이론적으로 가능하다. RFC 7541 §7 보안 고려사항 참조.

WARNING

Never Indexed 플래그: Authorization, Cookie 같은 민감 헤더는 Literal Header Field Never Indexed 표현을 사용해 중간 노드(프록시)의 인덱싱을 막아야 한다. HTTP/2 라이브러리 대부분이 이를 자동 처리하지만, 커스텀 구현 시 반드시 확인.

HPACK Bomb

동적 테이블에 최대 크기(4 KB)까지 헤더를 채운 뒤, 그 모든 항목을 인덱스 참조로만 이루어진 헤더 블록을 수십만 개 전송한다. 수신측은 작은 바이트를 수백 MB 메모리로 팽창시키며 디코딩해야 한다.

# 공격자 전송: 9 바이트 헤더 블록
# 수신측 메모리 요구: 수 MB

- 동적 테이블 가득 채움 (4 KB)
- 참조만 있는 헤더 블록 10만 개 전송 → 테이블 전체 팽창 반복

CAUTION

서버 구현 시 SETTINGS_MAX_HEADER_LIST_SIZE와 최대 헤더 블록 크기를 반드시 제한해야 한다. HTTP/2 라이브러리의 기본 설정을 확인하고 적절한 상한을 설정할 것.

QPACK: HTTP/3에서의 개선

QUIC 위에서는 HPACK의 순서 의존성 문제가 발생한다. HTTP/2는 TCP 위에서 동작해 헤더 블록이 순서대로 도착하지만, HTTP/3의 QUIC은 스트림이 독립적이다.

문제: 동적 테이블 업데이트가 스트림 A에서 도착했는데, 스트림 B의 헤더가 그 업데이트를 참조한다면? QUIC에서는 스트림 B가 먼저 도착할 수 있다.

QPACK 해결책 (RFC 9204):

flowchart LR
    E["Encoder\n(클라이언트)"]
    D["Decoder\n(서버)"]
    ES["Encoder Stream\n(단방향, 테이블 업데이트)"]
    DS["Decoder Stream\n(단방향, ACK)"]
    RS["Request Stream\n(각 HTTP 요청)"]

    E -->|"동적 테이블 업데이트"| ES --> D
    D -->|"테이블 업데이트 확인"| DS --> E
    E -->|"헤더 블록 (인덱스 참조)"| RS --> D

인코더 스트림과 디코더 스트림이 분리되어 동적 테이블 업데이트가 독립적으로 전달된다. 요청 스트림은 해당 업데이트가 도착했다는 확인을 받은 뒤에만 인덱스를 참조한다.

항목HPACKQPACK
대상 프로토콜HTTP/2 (TCP)HTTP/3 (QUIC)
동적 테이블단일 스트림 순서 의존독립 인코더/디코더 스트림
순서 보장 필요Yes (TCP가 보장)No (QUIC 스트림 독립)
HOL 블로킹TCP 계층에서 처리QPACK 레벨 동기화로 해결

실전 예시

헤더 인코딩 직접 관찰 (Python)

# hpack 패키지로 HPACK 인코딩/디코딩 실습
# pip install hpack

import hpack

encoder = hpack.Encoder()
decoder = hpack.Decoder()

headers = [
    (":method", "GET"),
    (":path", "/api/users"),
    (":scheme", "https"),
    (":authority", "example.com"),
    ("authorization", "Bearer my-token"),
    ("accept", "application/json"),
]

# 인코딩
encoded = encoder.encode(headers)
print(f"인코딩 전 헤더 길이: {sum(len(k)+len(v)+2 for k,v in headers)} bytes")
print(f"HPACK 인코딩 후: {len(encoded)} bytes")

# 디코딩
decoded = decoder.decode(encoded)
print(f"디코딩 결과: {decoded}")

nginx에서 HPACK 확인

# HTTP/2 + HPACK 요청 전송
curl -v --http2 https://example.com/api/users -H "Authorization: Bearer token123"

# 응답 헤더에서 HTTP/2 확인
# < HTTP/2 200
# < content-type: application/json
# < content-length: 512

# Wireshark에서 HTTP/2 헤더 관찰:
# 필터: http2.header.name == "authorization"

동적 테이블 크기 협상

# Python h2 라이브러리로 SETTINGS 프레임 제어
# pip install h2

import h2.connection
import h2.config

config = h2.config.H2Configuration(client_side=True)
conn = h2.connection.H2Connection(config=config)

# 동적 테이블 최대 크기를 8192 bytes로 설정
conn.initiate_connection()
conn.update_settings({
    h2.settings.SettingCodes.HEADER_TABLE_SIZE: 8192,
    h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536,
})

함정

WARNING

  1. 동적 테이블은 연결 단위: 연결이 끊기면 테이블도 초기화된다. HTTP/2 연결 풀링이 끊기는 상황 (재배포, 로드 밸런서 교체 등)에서 첫 몇 개 요청은 압축 효율이 낮다.
  2. 프록시가 테이블 공유 안 함: 클라이언트-프록시, 프록시-서버 각각 별개 HPACK 컨텍스트. 프록시를 거치면 클라이언트 테이블이 서버에 전달되지 않는다.
  3. 헤더 순서가 성능에 영향: 자주 등장하는 헤더를 먼저 보내면 동적 테이블 효율이 높아진다.
  4. HPACK Bomb 방어 필수: SETTINGS_MAX_HEADER_LIST_SIZE로 헤더 목록 최대 크기 제한. 기본값이 없는 구현도 있으므로 명시적 설정 필요.

관련 위키

  • HTTP/2 - HPACK이 사용되는 프로토콜
  • HTTP/3 - QPACK을 사용하는 후속 버전
  • QUIC - HTTP/3의 전송 계층
  • TLS - HPACK과 함께 동작하는 암호화 레이어
  • HTTP/1.1 - HPACK 도입 이전 버전
이 글의 용어 (5개)
[Network] HTTP/1.1: keep-alive, pipelining, chunked transfernetwork
정의 HTTP/1.1 (1997, RFC 9112 으로 재정리) 는 텍스트 기반 stateless request/response 프로토콜. 2026 시점에도 대부분의 트래픽이 H…
[Network] HTTP/2: multiplexing, HPACK, server push의 종말network
정의 HTTP/2 (2015, RFC 9113) 는 HTTP/1.1 의 문법은 유지하되 전송을 바이너리 + 멀티플렉싱 으로 바꾼다. Head-of-Line Blocking (HT…
[Network] HTTP/3: QUIC 위의 HTTP, 0-RTT, connection migrationnetwork
정의 HTTP/3 (2022, RFC 9114)는 HTTP의 전송 레이어를 TCP 에서 QUIC 으로 교체한 버전이다. UDP 위에 QUIC, QUIC 위에 HTTP 구조다. 핵…
QUICnetwork
정의 QUIC (Quick UDP Internet Connections)은 Google이 2012년 처음 제안하고 2021년 RFC 9000으로 표준화된 UDP 기반 전송 프로토…
TLS / SSLnetwork
정의 TLS (Transport Layer Security) 는 (또는 위 ) 위에서 동작하는 암호화·인증 프로토콜이다. SSL 은 TLS 의 전신 (Netscape 1995, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기