HTTP/2
HTTP/2의 바이너리 프레이밍·스트림 다중화·HPACK을 익히고 HTTP/1.1 병목과 남은 TCP HOL 문제를 이해합니다.
9장에서 HTTP/1.1의 요청-응답 구조를 살펴보았습니다.
HTTP/1.1은 웹의 폭발적 성장을 이끌었지만, 현대 웹 페이지의 복잡도가 증가하면서 성능 한계가 드러났습니다.
하나의 웹 페이지가 수십 개의 CSS, JavaScript, 이미지 파일을 필요로 하는 상황에서, HTTP/1.1의 구조적 문제가 병목이 되기 시작했습니다.
HTTP/1.1의 한계
HTTP/1.1의 대표적인 성능 한계는 Head-of-Line(HOL) Blocking입니다.
HTTP/1.1 파이프라이닝에서는 여러 요청을 먼저 보내도 응답은 요청 순서대로 보내야 하므로, 느린 앞 응답이 뒤 응답을 막습니다. 연결을 유지하는 keep-alive 자체가 파이프라이닝을 뜻하지는 않습니다. 브라우저는 연결 단위의 병목을 줄이려고 여러 TCP 연결을 열었습니다.
하지만 연결 수가 늘면 TCP/TLS 핸드셰이크, 혼잡 제어, 서버 부하가 함께 증가합니다.
개발자들은 HTTP/1.1의 연결 단위 병목을 줄이기 위해 다양한 최적화 패턴을 사용했습니다.
| 기법 | 방식과 HTTP/2에서의 판단 |
|---|---|
| 스프라이트 | 여러 이미지를 합쳐 요청을 줄입니다. 개별 캐시·유지보수 비용도 비교합니다. |
| 도메인 샤딩 | 여러 도메인에 연결을 나누지만 DNS/TLS 비용과 연결 재사용 손실이 생길 수 있습니다. |
| 인라이닝 | CSS/JS를 HTML에 넣어 초기 렌더링을 돕지만 독립 캐시를 잃습니다. |
| 파일 합치기 | JS/CSS 요청 수를 줄입니다. HTTP/2에서도 압축·실행·캐시 비용에 따라 선택합니다. |
HTTP/2는 이런 패턴이 의존하던 “요청 수 자체를 줄여야 한다”는 압박을 프로토콜 레벨에서 완화합니다.
다만 번들링, 인라이닝, 프리로드 같은 최적화는 애플리케이션 특성에 따라 여전히 선택적으로 사용됩니다.
바이너리 프레이밍과 멀티플렉싱
HTTP/2의 핵심 변화는 바이너리 프레이밍 계층(Binary Framing Layer)의 도입입니다.
HTTP 메시지를 텍스트 줄 단위로 그대로 흘려보내지 않고, HEADERS와 DATA 같은 프레임으로 나누어 여러 스트림을 하나의 연결 위에서 동시에 진행합니다.
이미 요청을 받은 서버의 응답 예시이며, 각 HEADERS에서 헤더 블록이 끝났다고 가정합니다. 헤더 블록이 CONTINUATION으로 이어지는 동안에는 다른 스트림의 프레임을 끼워 넣을 수 없습니다.
한 연결에서 교차하는 HTTP/2 응답 프레임
각 스트림의 응답 헤더 블록을 끝냅니다. 스트림 1의 나머지 본문도 끝냅니다. 각 단계의 값과 전환 이유를 비교합니다.
HTTP/2 · 스트림 다중화
같은 연결에서 응답을 섞어 보내고, 스트림 3이 먼저 끝납니다
이미 요청을 받은 서버의 허용 전송 순서 예입니다. 각 HEADERS에서 헤더 블록이 끝났다고 가정하며, END_STREAM은 DATA에 붙인 플래그입니다. 프레임 경계는 TCP 패킷 경계와 다릅니다.
위에서 아래로 시간 순서입니다. 세로 점선은 서버·클라이언트의 흐름, 가로 화살표는 응답 프레임입니다. 청록색은 스트림 3의 먼저 끝나는 응답이며, 필수 스케줄이나 실제 시간 간격을 뜻하지 않습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 두 헤더부터 스트림 1 끝까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 시간은 위에서 아래로 향합니다. 서버가 HEADERS1, HEADERS3, DATA1, END_STREAM이 붙은 DATA3, END_STREAM이 붙은 DATA1을 차례로 보냅니다. 각 HEADERS에서 블록 종료를 가정한 허용 순서이며 스트림 3이 먼저 끝나도 1은 계속됩니다. 각 화살표는 응답 프레임이며 TCP 패킷 경계나 고정 시간을 뜻하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 헤더 각 스트림의 응답 헤더 블록을 끝냅니다. |
|
|
| 스트림 1 일부 스트림 1의 첫 본문 일부를 보냅니다. |
|
|
| 스트림 3 끝 스트림 3의 본문과 END_STREAM을 보냅니다. |
|
|
| 스트림 1 끝 스트림 1의 나머지 본문도 끝냅니다. |
|
|
한 연결에서 교차하는 HTTP/2 응답 프레임
각 스트림의 응답 헤더 블록을 끝냅니다. 스트림 1의 나머지 본문도 끝냅니다. 각 단계의 값과 전환 이유를 비교합니다.
HTTP/2 · 스트림 다중화
같은 연결에서 응답을 섞어 보내고, 스트림 3이 먼저 끝납니다
이미 요청을 받은 서버의 허용 전송 순서 예입니다. 각 HEADERS에서 헤더 블록이 끝났다고 가정하며, END_STREAM은 DATA에 붙인 플래그입니다. 프레임 경계는 TCP 패킷 경계와 다릅니다.
위에서 아래로 시간 순서입니다. 세로 점선은 서버·클라이언트의 흐름, 가로 화살표는 응답 프레임입니다. 청록색은 스트림 3의 먼저 끝나는 응답이며, 필수 스케줄이나 실제 시간 간격을 뜻하지 않습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 두 헤더부터 스트림 1 끝까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 시간은 위에서 아래로 향합니다. 서버가 HEADERS1, HEADERS3, DATA1, END_STREAM이 붙은 DATA3, END_STREAM이 붙은 DATA1을 차례로 보냅니다. 각 HEADERS에서 블록 종료를 가정한 허용 순서이며 스트림 3이 먼저 끝나도 1은 계속됩니다. 각 화살표는 응답 프레임이며 TCP 패킷 경계나 고정 시간을 뜻하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 헤더 각 스트림의 응답 헤더 블록을 끝냅니다. |
|
|
| 스트림 1 일부 스트림 1의 첫 본문 일부를 보냅니다. |
|
|
| 스트림 3 끝 스트림 3의 본문과 END_STREAM을 보냅니다. |
|
|
| 스트림 1 끝 스트림 1의 나머지 본문도 끝냅니다. |
|
|
END_STREAM은 별도 프레임이 아니라 해당 스트림의 송신 종료를 나타내는 플래그입니다. 그림의 프레임 경계는 TCP 패킷 경계와 같지 않습니다.
예시: 응답 하나의 끝과 연결 전체의 끝 그림의 스트림 1은 DATA 두 개로 본문을 보내고, 스트림 3은 DATA 하나로 먼저 응답을 끝냅니다. 스트림 3의 END_STREAM 뒤에도 스트림 1의 DATA를 보낼 수 있습니다. 응답을 스트림별로 나누었기 때문에 끝난 스트림 하나와 TCP 연결의 종료를 구분할 수 있습니다.
각 HEADERS의 압축된 헤더 블록은 20 B, DATA 본문은 그림 순서대로 100 B, 50 B, 150 B라고 정해 보겠습니다. 패딩·우선순위 필드·추가 제어 프레임 없이 이 다섯 프레임만 셉니다. HTTP/2 프레임 헤더는 각각 9 B이므로 프레임 헤더 5×9=45 B, 압축된 HTTP 헤더 2×20=40 B, 본문 100+50+150=300 B, 합계 385 B입니다. 이것은 HTTP/2 프레임 바이트 수이며 TCP·TLS·IP 헤더를 합친 전송량이나 실제 측정값이 아닙니다.
헤더 블록의 조건을 바꾸어 판단하기 서버가 HEADERS(스트림 1, END_HEADERS=0)를 보낸 직후 HEADERS(스트림 3)를 보내려 합니다. 각 스트림이 독립적이므로 허용될까요? 최소한 어떤 순서를 더 넣어야 하나요?
다중화의 예외를 적용한 풀이
스트림 1의 헤더 블록이 아직 끝나지 않았으므로 다른 스트림을 끼워 넣을 수 없습니다. 남은 블록을 같은 스트림 1의 CONTINUATION으로 이어 마지막에 END_HEADERS=1을 보낸 뒤 스트림 3의 HEADERS를 보냅니다. DATA 프레임을 사이에 넣는 것도 허용되지 않습니다. 헤더 블록의 연속 전송 규칙을 적용한 것이며, 그림은 처음부터 각 HEADERS에서 블록이 끝나는 별도 조건입니다.
헤더 압축 (HPACK)
HTTP/1.1에서는 매 요청마다 비슷한 헤더가 반복 전송되었습니다.
HTTP/2는 HPACK으로 헤더 필드를 인덱스와 동적 테이블 중심으로 표현해 반복 전송량을 줄입니다.
민감한 값은 동적 테이블에 넣지 않는 표현도 있어, 압축 효율과 보안 고려를 함께 다룹니다.
| 표현 | 역할 |
|---|---|
| 정적 테이블 | 명세에 미리 정의된 이름·값을 인덱스로 참조합니다. 첫 요청을 저장해서 만드는 표가 아닙니다. |
| 동적 테이블 | 인덱싱하는 리터럴 표현으로 등록한 필드를 연결의 후속 헤더 블록에서 재사용합니다. |
| 등록하지 않는 표현 | without indexing / never indexed 리터럴은 동적 테이블에 추가하지 않습니다. |
서버 푸시
HTTP/2의 서버 푸시(Server Push)는 서버가 클라이언트의 후속 요청을 예측하여 PUSH_PROMISE로 미리 자원을 예약하고 보내는 기능입니다.
프로토콜에 정의된 기능이어도 클라이언트 지원과 성능 이득이 보장되는 것은 아닙니다. 캐시 중복과 예측 실패가 생길 수 있으며, Chrome은 106부터 서버 푸시를 기본 비활성화했습니다. 103 Early Hints나 preload는 자원 자체를 푸시하는 대신 클라이언트에 요청 힌트를 줍니다.
HTTP/2의 남은 문제
HTTP/2는 HTTP 메시지 단위의 HOL Blocking을 크게 줄였지만, TCP 계층의 HOL Blocking은 여전히 남습니다.
하나의 TCP 연결 위에 여러 HTTP/2 스트림이 올라가기 때문에, 패킷 손실이 발생하면 TCP가 순서를 복구할 때까지 뒤쪽 바이트 전달이 함께 지연될 수 있습니다.
앞 바이트 100~199가 유실되고 스트림 3의 응답 프레임 바이트가 200 이후에 도착했다고 가정해 보세요. HTTP/2는 응답 순서를 바꿔 보낼 수 있지만 TCP는 이 빈 구간을 건너뛰어 뒤 바이트를 애플리케이션에 전달하지 않습니다. 스트림 3이 논리적으로 먼저 끝나는 순서와, 손실 뒤 클라이언트가 그 끝을 실제로 읽는 시점은 다른 질문입니다.
| 버전 | 전송·압축·남는 지연 |
|---|---|
| HTTP/1.1 | TCP 위 텍스트 메시지이며 TLS는 선택입니다. 헤더 압축·서버 푸시는 없고, 파이프라인 응답 순서와 TCP 바이트 순서의 대기가 있습니다. |
| HTTP/2 | TCP 위 바이너리 프레임·HPACK·여러 스트림을 사용합니다. 응답 간 순서 제약은 줄지만 TCP HOL은 남습니다. TLS에서는 ALPN h2를 사용하며 서버 푸시는 명세와 구현 지원을 구분합니다. |
| HTTP/3 | UDP 위 QUIC 스트림·바이너리 프레임·QPACK을 사용하고 TLS 1.3이 통합됩니다. TCP HOL은 없지만 각 스트림의 순서 대기와 공유 혼잡 제어가 남습니다. 푸시는 명세에 있으며 재개 조건에 맞으면 0-RTT 데이터가 가능합니다. |
RFC 9113은 평문 HTTP/2의 prior knowledge 연결을 정의하지만, 과거 h2c Upgrade 방식은 폐기했습니다. TLS 유무와 프로토콜 협상 결과를 구분해 확인해야 합니다.
이 문제를 줄이기 위해 QUIC 기반의 HTTP/3가 등장했습니다.
다음 절에서 QUIC이 연결 수립, 손실 복구, 스트림 처리 방식을 어떻게 바꾸는지 자세히 다루겠습니다.