HTTP/2 핵심 개념 — 프레임·스트림·HPACK·흐름 제어·SETTINGS

NOTE

HTTP/2(RFC 9113)를 직접 구현하거나 설정을 다루는 사람이 알아야 할 부분만 추렸다. 모든 항목에 검증 상태를 표시했다.

  • ✔ 원문 확인: RFC 9113(httpwg.org) 또는 IANA 레지스트리에서 직접 확인
  • ○ 자료 일치: 별도 교육 자료와 일치하고 다른 항목과 모순 없음
  • △ 기억/일반 지식: 원문 직접 대조는 못 함. 인용 전에 RFC 확인 권장

0. 한눈에 보기

flowchart TB
    subgraph Conn["Connection (TCP 연결 1개)"]
        direction TB
        S1["Stream 1\n요청1 ↔ 응답1"]
        S3["Stream 3\n요청2 ↔ 응답2"]
        S5["Stream 5 ..."]
        C0["Stream 0\n연결 전체 제어 (SETTINGS, PING, GOAWAY, WINDOW_UPDATE)"]
    end
    S1 --> F1["HEADERS 프레임 → DATA 프레임 ..."]
    S3 --> F2["HEADERS 프레임 → DATA 프레임 ..."]
개념정의
ConnectionTCP(또는 TLS over TCP) 연결 하나
Stream연결 안의 독립된 양방향 프레임 흐름. 요청·응답 한 쌍(트랜잭션) 에 해당. 정수 ID로 구분
Message요청 또는 응답 하나 (헤더 + 본문)
Frame전송의 최소 단위. 9바이트 헤더 + payload

한 연결에서 여러 스트림이 프레임 단위로 섞여(interleave) 동시에 오간다. 이것이 다중화(multiplexing) 이고, HTTP/1.1이 요청마다 연결을 여러 개 열던 문제를 푼다.

1. HTTP/1.1과 무엇이 다른가

항목HTTP/1.1HTTP/2
형식텍스트바이너리 프레임
연결당 동시 요청1개 (파이프라이닝은 HOL 블로킹으로 사실상 못 씀)여러 개 (스트림)
병렬성 확보 방법TCP 연결을 여러 개연결 1개로 충분
헤더매 요청마다 전체를 텍스트로 반복HPACK 압축 (이미 보낸 헤더는 인덱스로)
서버가 먼저 보내기불가서버 푸시(선택)

남아 있는 HOL 블로킹

HTTP/2는 응용 계층의 HOL 블로킹을 없앴지만 TCP 계층의 것은 남는다. TCP 패킷이 하나 유실되면 재전송될 때까지 연결 위의 모든 스트림이 같이 기다린다. 이를 피하려고 HTTP/3은 TCP 대신 QUIC(UDP 기반)을 쓴다 (RFC 9114). ○

2. 프레임 구조

2-1. 9바이트 프레임 헤더 ✔

+-----------------------------------------------+
|                 Length (24)                   |   payload 길이 (헤더 9바이트 제외)
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |   R = 예약 1비트
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+
  • Length는 24비트라 이론상 최대 16,777,215바이트. 실제 상한은 수신 측이 알린 SETTINGS_MAX_FRAME_SIZE다 (기본 16,384). ✔
  • Stream Identifier가 0이면 연결 전체에 대한 프레임이다 (SETTINGS, PING, GOAWAY, 연결 수준 WINDOW_UPDATE). ✔

2-2. 프레임 유형 ✔ (IANA 레지스트리로 코드 확인)

유형코드용도정의된 플래그
DATA0x00본문 전송END_STREAM(0x1), PADDED(0x8)
HEADERS0x01스트림을 열고 헤더 전송END_STREAM(0x1), END_HEADERS(0x4), PADDED(0x8), PRIORITY(0x20)
PRIORITY0x02우선순위(RFC 9113에서 폐기)–
RST_STREAM0x03스트림 하나를 즉시 종료–
SETTINGS0x04연결 설정 교환ACK(0x1)
PUSH_PROMISE0x05서버 푸시 예고END_HEADERS(0x4), PADDED(0x8)
PING0x06생존 확인 / RTT 측정ACK(0x1)
GOAWAY0x07연결을 종료하겠다는 통지–
WINDOW_UPDATE0x08흐름 제어 창 확대–
CONTINUATION0x09헤더 블록 이어서 전송END_HEADERS(0x4)

코드 번호를 기억에 의존하지 말 것

이 표를 만드는 중 문서 요약 도구가 GOAWAY=0x08, WINDOW_UPDATE=0x09, CONTINUATION=0x0A라고 잘못 요약한 적이 있다. IANA 레지스트리(iana.org/assignments/http2-parameters)로 0x07/0x08/0x09임을 확인했다. 자동 요약을 그대로 옮기면 틀릴 수 있다.

NOTE

CONTINUATION에 정의된 플래그는 END_HEADERS뿐이다. END_STREAM은 HEADERS/DATA에만 있다. 일부 교육 자료가 CONTINUATION에 END_STREAM을 같이 적는 경우가 있는데 규격과 다르다.

2-3. 메시지 → 프레임 변환 ○

HTTP 메시지 하나는 여러 프레임으로 나뉜다. 헤더는 HEADERS(+필요 시 CONTINUATION), 본문은 DATA다.

요청                                   응답
HEADERS  (END_HEADERS, END_STREAM)    HEADERS  (END_HEADERS)
  :method = GET                         :status = 200
  :scheme = http                        content-type = text/html
  :path   = /index.html               DATA
  :authority = example.com              "May The Force"
                                      DATA     (END_STREAM)
                                        "Be With You"
  • 메시지의 끝은 END_STREAM 플래그가 붙은 프레임이다. 요청에 본문이 없으면 HEADERS에 END_STREAM이 같이 붙는다.
  • 한 메시지를 이루는 프레임은 순서대로 보내야 한다. 다른 스트림의 프레임은 사이에 끼어들 수 있다 (헤더 블록은 예외, 아래 §7-3). ○
  • 본문이 MAX_FRAME_SIZE보다 크면 DATA 프레임을 여러 개로 쪼갠다. 헤더가 크면 CONTINUATION을 쓴다.

2-4. 의사 헤더(pseudo-header) ✔/△

:로 시작하는 헤더. 정의된 것만 쓸 수 있고, 일반 헤더보다 앞에 와야 한다. 일반 헤더 이름은 전부 소문자여야 한다. ✔(규칙 존재) / 세부 순서는 △

의사 헤더방향의미
:method요청GET, POST …
:scheme요청http / https
:authority요청HTTP/1.1의 Host에 해당 (호스트[:포트])
:path요청요청 경로와 쿼리
:status응답상태 코드

5G 코어에서 이 헤더가 중요한 이유

3GPP SBI는 URI가 {apiRoot}/{apiName}/{apiVersion}/{리소스}/{동작} 구조라서 :path의 앞부분만 봐도 어떤 서비스 요청인지 알 수 있고, :authority로 어느 NF로 보낼지 정한다. 그래서 스택이 이 두 헤더로 라우팅을 한다.

3. 연결 시작

3-1. 시작하는 방법 ✔

방식설명
h2 (TLS 위)TLS 핸드셰이크 중 ALPN으로 h2를 협상한다. (RFC 9113 §3.1)
prior knowledge (평문)상대가 HTTP/2를 지원한다는 걸 미리 알고 TCP 연결 직후 바로 preface를 보낸다. (§3.3)
h2c UpgradeHTTP/1.1 요청에 Upgrade: h2c를 실어 전환하던 방식. RFC 9113에서 폐기됐다(널리 배포된 적 없음). (§3.1)

5G 코어처럼 모든 노드가 HTTP/2를 쓴다고 정해진 망에서는 Upgrade 협상 없이 prior knowledge로 바로 시작한다. ○

3-2. 연결 preface ✔

클라이언트가 연결 직후 보내는 고정 24바이트:

PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
  • 뒤에 반드시 SETTINGS 프레임이 이어진다(비어 있어도 됨). 서버도 첫 프레임이 SETTINGS여야 한다. ✔
  • 서버는 처음 받은 24바이트가 이 문자열과 다르면 연결을 끊는다. HTTP/1.1 클라이언트가 잘못 접속한 경우를 걸러내는 장치다. ○
  • 일부러 HTTP/2.0이라는 존재하지 않는 형식을 쓴 것은 HTTP/1.x 서버가 이를 정상 요청으로 오인하지 않게 하려는 의도로 알려져 있다. △
sequenceDiagram
    participant C as 클라이언트
    participant S as 서버
    C->>S: 24바이트 preface "PRI * HTTP/2.0 ..."
    C->>S: SETTINGS (자기 설정)
    S->>C: SETTINGS (자기 설정)
    C->>S: SETTINGS ACK
    S->>C: SETTINGS ACK
    Note over C,S: WINDOW_UPDATE(stream 0)로 연결 수준 창을 넓힐 수 있다
    C->>S: HEADERS (stream 1)
    S->>C: HEADERS + DATA (stream 1)

4. SETTINGS — 내 한계를 상대에게 알리는 프레임 ✔

SETTINGS는 자기가 수신할 때의 한계를 상대에게 알린다(상대가 이를 지켜서 보낸다). 받으면 ACK 플래그를 단 빈 SETTINGS로 응답한다. ✔

이름코드기본값허용 범위 / 비고
SETTINGS_HEADER_TABLE_SIZE0x014,096HPACK 동적 테이블 크기
SETTINGS_ENABLE_PUSH0x0210 또는 1만 허용, 그 외는 PROTOCOL_ERROR. 클라이언트만 보냄
SETTINGS_MAX_CONCURRENT_STREAMS0x03제한 없음100 이상을 권장
SETTINGS_INITIAL_WINDOW_SIZE0x0465,535최대 2^31−1, 초과 시 FLOW_CONTROL_ERROR
SETTINGS_MAX_FRAME_SIZE0x0516,3842^14 ~ 2^24−1
SETTINGS_MAX_HEADER_LIST_SIZE0x06제한 없음수신할 헤더 목록의 크기 상한
  • MAX_FRAME_SIZE를 올리면 큰 본문을 보낼 때 프레임 수가 줄어 CPU가 절약된다. 하지만 TLS 레코드는 평문 최대 16,384바이트라서(TLS의 한계), 그보다 큰 HTTP/2 프레임은 여러 TLS 레코드로 쪼개져 암호화된다. 그래서 TLS 환경에서는 프레임을 크게 키워도 이득이 줄어든다. ○(교육자료) / TLS 레코드 크기는 △
  • INITIAL_WINDOW_SIZE는 스트림 창의 초기값이다. 연결 수준 창은 SETTINGS로 못 바꾸고 WINDOW_UPDATE(stream 0)로만 키운다. ✔(규칙) / ○

5. 스트림

5-1. 스트림 ID 규칙 ✔ (RFC 9113 §5.1.1 원문 확인)

규칙내용
개시자별 홀짝클라이언트가 연 스트림은 홀수, 서버가 연 스트림은 짝수 (MUST)
증가새 스트림 ID는 그 개시자가 이미 열었거나 예약한 모든 ID보다 커야 한다 (MUST)
재사용 불가”Stream identifiers cannot be reused.”
0번연결 전체 제어용
크기31비트 (최대 2^31−1)

ID가 바닥나면 (소진): 클라이언트는 새 연결을 연다. 서버는 GOAWAY를 보내 클라이언트가 새 연결을 열게 한다. ✔

→ 클라이언트가 쓰는 홀수 ID만 보면 한 연결에서 약 10억 개((2^31−1)/2)의 요청을 보낼 수 있다. 오래 유지되는 연결을 쓰는 서버 간 통신(5G 코어)에서는 이 한계에 도달할 수 있어서, 임계치를 정해 여러 연결에 분산하고 임계치를 넘으면 그 연결로의 새 요청을 막은 뒤 새 연결로 갈아타는 구현을 둔다. ○

5-2. 스트림 상태 ○/△

stateDiagram-v2
    [*] --> idle
    idle --> open: HEADERS 송신/수신
    idle --> reserved_local: PUSH_PROMISE 송신
    idle --> reserved_remote: PUSH_PROMISE 수신
    reserved_local --> half_closed_remote: HEADERS 송신
    reserved_remote --> half_closed_local: HEADERS 수신
    open --> half_closed_local: END_STREAM 송신
    open --> half_closed_remote: END_STREAM 수신
    half_closed_local --> closed: END_STREAM 수신 / RST_STREAM
    half_closed_remote --> closed: END_STREAM 송신 / RST_STREAM
    open --> closed: RST_STREAM
    closed --> [*]
  • 요청에 본문이 없으면: open → (END_STREAM 송신) → half_closed_local → (응답 END_STREAM 수신) → closed.
  • MAX_CONCURRENT_STREAMS는 동시에 열려 있는 스트림 수의 상한이다. open과 half_closed가 셈에 들어간다. △

5-3. 동시 스트림 제한이 걸리는 모습 ○

MAX_CONCURRENT_STREAMS = 10일 때:

순서사용 중 스트림 수동작
11ID 1 요청 전송
20ID 1 응답 수신 → 스트림 종료, 수 감소
310요청 10개를 응답 없이 연속 전송
41011번째 요청은 보낼 수 없다 (제한). 대기해야 한다
59응답 하나가 도착해 스트림 종료
610대기하던 요청 1개를 보낼 수 있다

제한은 송신자 측에서 지켜야 한다. 보내는 쪽이 자기가 열어 둔 스트림 수를 세어야 하고, 한도에 도달하면 신규 요청을 큐에 쌓아야 한다.

6. 흐름 제어 (Flow Control)

6-1. 규칙 ✔

  • DATA 프레임만 흐름 제어 대상이다. 헤더 프레임과 제어 프레임은 대상이 아니다. ✔
  • 창(window)은 두 개다: 스트림별 창과 연결 전체 창. DATA 하나를 보내면 둘 다 줄어든다. 둘 중 하나라도 0이면 못 보낸다. ✔
  • 초기값은 둘 다 65,535바이트. 최대 2^31−1. ✔
  • 수신 측이 WINDOW_UPDATE로 창을 늘려 준다(스트림 번호 0이면 연결 창). ✔
  • 연결 창은 SETTINGS로 못 바꾸고 WINDOW_UPDATE(stream 0)로만 바꾼다. ○

왜 필요한가: 송신자가 수신자가 처리할 수 있는 속도보다 빠르게 보내서 수신 버퍼가 터지는 것을 막기 위해서다. TCP에도 흐름 제어가 있지만 TCP는 연결 전체만 본다. HTTP/2는 한 연결 안에 스트림이 여럿이므로 느린 스트림 하나가 연결 전체를 막지 않도록 스트림별 창이 따로 필요하다.

6-2. 숫자로 따라가 보기 ○

연결 창 = 200,000바이트(WINDOW_UPDATE로 확대), 스트림 창 초기값 = 100,000바이트, 응답 DATA = 150,000바이트인 경우:

순서사건연결 창스트림 창
1요청 전송, 스트림 창 100,000으로 시작200,000100,000
2응답 150,000 중 100,000만 전송 (스트림 창 한도)100,0000
3나머지 50,000은 대기100,0000
4수신 측이 WINDOW_UPDATE(stream) 50,000100,00050,000
5나머지 50,000 전송 후 스트림 종료50,000–

반대로 연결 창이 먼저 바닥나면 모든 스트림이 같이 멈추고, WINDOW_UPDATE(stream 0)가 와야 풀린다.

흐름 제어 창이 작으면 느려진다

기본값 65,535는 큰 응답에서 WINDOW_UPDATE 왕복이 잦아 처리량이 떨어진다. 서버 간 통신처럼 지연이 낮고 대역폭이 큰 환경에서는 INITIAL_WINDOW_SIZE를 수 MB로 키운다. 대신 연결·스트림마다 그만큼의 수신 버퍼가 필요하다(메모리 비용). △

7. 헤더 압축 — HPACK (RFC 7541)

7-1. 왜 필요한가

HTTP 요청은 헤더가 매번 거의 같다(같은 :authority, user-agent, 쿠키 …). 본문은 작은데 헤더가 큰 API 호출에서 헤더 전송 비용이 크다. HPACK은 이미 보낸 헤더를 인덱스로 대체해서 줄인다.

7-2. 구조 ○/△

요소설명
정적 테이블규격에 미리 정의된 61개 항목 (RFC 7541 부록 A). 예: 인덱스 2 = :method GET, 3 = :method POST, 4 = :path /, 7 = :scheme https, 8 = :status 200 △
동적 테이블연결마다, 방향마다 있는 표. 처음 보낸 헤더가 여기에 추가되고, 이후에는 인덱스로 참조한다. 크기 상한이 SETTINGS_HEADER_TABLE_SIZE(기본 4,096)
Huffman 부호화문자열 값을 정적 Huffman 코드로 줄인다

동작 (○):

  1. 첫 요청: 정적 테이블에 있는 헤더는 인덱스로, 나머지는 Huffman으로 압축. 양쪽 동적 테이블에 저장.
  2. 같은 요청을 다시 보내면: 정적·동적 테이블의 인덱스(1바이트 안팎) 로 대체.

7-3. 이 구조가 만드는 제약 △

  • 동적 테이블은 상태를 가진다. 양쪽이 같은 순서로 갱신해야 같은 표를 유지한다. 그래서 헤더 블록은 순서대로 처리해야 하고, HEADERS와 그 뒤 CONTINUATION들은 다른 프레임이 끼어들지 않고 연속으로 와야 한다.
  • 그래서 HPACK 압축·해제는 연결 단위로 직렬화된다. 스레드로 병렬 처리하려면 연결(세션)을 스레드에 고정해야 한다.
  • 이 때문에 구현들은 한 세션(연결)을 한 스레드에 배정하는 경우가 많다. △ 대표적인 C 라이브러리 nghttp2는 공식 가이드가 “단일 nghttp2_session 객체는 한 시점에 한 스레드만 사용해야 한다” 고 명시한다. ✔ (nghttp2 API 모델 §10)

8. 우선순위와 서버 푸시 — 지금은 거의 쓰이지 않는다

기능상태
스트림 우선순위(가중치 1~256, 종속성 트리)RFC 9113에서 폐기(“deprecates the priority signaling defined in RFC 7540”). 복잡하고 구현이 제각각이었다고 명시 ✔
대안RFC 9218(PRIORITY_UPDATE, priority 헤더) 등 새 방식 △
서버 푸시규격에는 남아 있다(선택 기능). SETTINGS_ENABLE_PUSH=0으로 끌 수 있다 ✔

5G 코어의 "우선순위"는 HTTP/2 우선순위가 아니다

HTTP/2의 스트림 우선순위는 홉 단위(연결 양 끝)로만 유효하다. 5G SBI는 종단 간 우선순위를 위해 3gpp-sbi-message-priority 헤더(0~31, 낮을수록 높음)를 쓴다. 과부하 상황에서 “이 우선순위 이상은 제한을 무시한다” 같은 정책도 이 헤더 값을 기준으로 구현된다. ○

9. 생존 확인과 종료

9-1. PING ✔

  • payload 8바이트(의미 없는 데이터). 받으면 같은 payload에 ACK를 달아 반드시 응답해야 한다 (MUST). ✔
  • 용도: ① 연결이 살아 있는지 확인 ② RTT 측정.
  • 왜 필요한가: TCP는 상대 장비 전원이 나가거나 중간 장비가 연결 상태를 지워도 한참 동안 모른다(half-open). 응용 계층 PING으로 죽은 연결을 직접 감지한다. ○
  • 구현에서는 보통 일정 간격으로 보내고, 응답이 N번 연속 없으면 연결을 끊는 정책을 둔다 ○

9-2. GOAWAY와 RST_STREAM

RST_STREAMGOAWAY
범위스트림 하나 종료연결 전체 종료 예고
담는 정보에러 코드마지막으로 처리한 스트림 ID + 에러 코드 ✔
용도요청 취소, 에러서버 점검·재시작 시 정상 종료, 스트림 ID 소진 시 새 연결 유도 ✔

GOAWAY의 “마지막 스트림 ID”는 그 ID 이하의 요청은 처리했거나 처리 중이고, 그보다 큰 요청은 처리하지 않았으니 재전송해도 안전하다는 정보를 준다. △

9-3. 에러 코드 △

코드이름코드이름
0x0NO_ERROR0x7REFUSED_STREAM
0x1PROTOCOL_ERROR0x8CANCEL
0x2INTERNAL_ERROR0x9COMPRESSION_ERROR
0x3FLOW_CONTROL_ERROR0xaCONNECT_ERROR
0x4SETTINGS_TIMEOUT0xbENHANCE_YOUR_CALM
0x5STREAM_CLOSED0xcINADEQUATE_SECURITY
0x6FRAME_SIZE_ERROR0xdHTTP_1_1_REQUIRED

(RFC 9113 §7. 이 표는 기억 기준이라 인용 전에 IANA 레지스트리 확인.)

10. TLS와의 관계

항목내용
협상TLS 핸드셰이크 중 ALPN h2 ✔
최소 버전HTTP/2 over TLS는 TLS 1.2 이상을 요구한다 △ (RFC 9113 §9.2)
레코드TLS 한 레코드의 평문 최대는 16,384바이트 → 큰 HTTP/2 프레임은 여러 레코드로 쪼개져 암호화 ○
상호 인증mTLS: 서버가 클라이언트 인증서도 검증. 인증·인가 방식 비교는 인증·인가 방식 비교

11. 자주 헷갈리는 점

질문답
스트림 ID는 요청이 끝나면 재사용되나?아니다. 연결 안에서 한 번 쓴 ID는 다시 못 쓴다 ✔
MAX_CONCURRENT_STREAMS는 누가 지키나?보내는 쪽이 상대가 알린 값을 지킨다
연결 창을 SETTINGS로 키울 수 있나?없다. WINDOW_UPDATE(stream 0)
헤더도 흐름 제어를 받나?아니다. DATA만 ✔
한 연결로 충분하면 왜 여러 연결을 두나?① 스트림 ID 소진 대비 ② TCP HOL 블로킹 완화 ③ 한 연결의 처리량 한계 ○
HTTP/2에서 Host 헤더는?:authority가 대신한다
헤더 이름 대소문자는?전부 소문자여야 한다 ✔
압축 오류가 나면?HPACK 상태가 어긋난 것이라 연결 전체가 COMPRESSION_ERROR로 종료된다 △

12. 검증 상태 요약

항목상태근거
프레임 헤더 구조✔RFC 9113 §4.1 (httpwg.org)
프레임 유형 코드 10종✔IANA HTTP/2 Frame Type Registry
SETTINGS 코드·기본값·범위✔RFC 9113 §6.5.2, IANA Settings Registry
preface 24바이트, SETTINGS 뒤따름✔RFC 9113 §3.4
ALPN h2, prior knowledge, h2c Upgrade 폐기✔RFC 9113 §3.1, §3.3
스트림 ID 규칙·소진 처리✔RFC 9113 §5.1.1
흐름 제어 규칙(DATA만, 초기 65,535, 최대 2^31−1)✔RFC 9113 §5.2
우선순위 폐기, 서버 푸시 선택✔RFC 9113 §5.3, §8.4
PING 8바이트·ACK, GOAWAY 필드✔RFC 9113 §6.7, §6.8
스트림 상태 전이, HPACK 세부, 에러 코드 표, TLS 1.2 이상△기억 기준. RFC 9113 §5.1, RFC 7541, §7, §9.2 확인 필요

관련 문서