안동민 개발노트

본문 시작

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과 3의 응답 프레임을 교차해 전송하는 허용 순서의 예시입니다. 스트림 3이 먼저 끝나며 END_STREAM은 DATA 프레임에 붙인 플래그입니다. TCP 패킷 경계나 필수 전송 순서를 뜻하지 않습니다.

한 연결에서 교차하는 HTTP/2 응답 프레임이미 요청을 받은 서버가 스트림 1과 3의 응답 프레임을 교차해 전송하는 허용 순서의 예시입니다. 스트림 3이 먼저 끝나며 END_STREAM은 DATA 프레임에 붙인 플래그입니다. TCP 패킷 경계나 필수 전송 순서를 뜻하지 않습니다.HEADERS · 스트림 1응답 헤더HEADERS · 스트림 3응답 헤더DATA · 스트림 1본문의 일부DATA · 스트림 3END_STREAM = 1 · 응답 끝DATA · 스트림 1END_STREAM = 1 · 응답 끝서버클라이언트
한 연결에서 교차하는 HTTP/2 응답 프레임이미 요청을 받은 서버가 스트림 1과 3의 응답 프레임을 교차해 전송하는 허용 순서의 예시입니다. 스트림 3이 먼저 끝나며 END_STREAM은 DATA 프레임에 붙인 플래그입니다. TCP 패킷 경계나 필수 전송 순서를 뜻하지 않습니다.HEADERS · 스트림 1응답 헤더HEADERS · 스트림 3응답 헤더DATA · 스트림 1본문의 일부DATA · 스트림 3END_STREAM = 1 · 응답 끝DATA · 스트림 1END_STREAM = 1 · 응답 끝서버클라이언트

END_STREAM은 별도 프레임이 아니라 해당 스트림의 송신 종료를 나타내는 플래그입니다. 그림의 프레임 경계는 TCP 패킷 경계와 같지 않습니다.


헤더 압축 (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가 순서를 복구할 때까지 뒤쪽 바이트 전달이 함께 지연될 수 있습니다.

버전전송·압축·남는 지연
HTTP/1.1TCP 위 텍스트 메시지이며 TLS는 선택입니다. 헤더 압축·서버 푸시는 없고, 파이프라인 응답 순서와 TCP 바이트 순서의 대기가 있습니다.
HTTP/2TCP 위 바이너리 프레임·HPACK·여러 스트림을 사용합니다. 응답 간 순서 제약은 줄지만 TCP HOL은 남습니다. TLS에서는 ALPN h2를 사용하며 서버 푸시는 명세와 구현 지원을 구분합니다.
HTTP/3UDP 위 QUIC 스트림·바이너리 프레임·QPACK을 사용하고 TLS 1.3이 통합됩니다. TCP HOL은 없지만 각 스트림의 순서 대기와 공유 혼잡 제어가 남습니다. 푸시는 명세에 있으며 재개 조건에 맞으면 0-RTT 데이터가 가능합니다.

RFC 9113은 평문 HTTP/2의 prior knowledge 연결을 정의하지만, 과거 h2c Upgrade 방식은 폐기했습니다. TLS 유무와 프로토콜 협상 결과를 구분해 확인해야 합니다.

이 문제를 줄이기 위해 QUIC 기반의 HTTP/3가 등장했습니다.

다음 절에서 QUIC이 연결 수립, 손실 복구, 스트림 처리 방식을 어떻게 바꾸는지 자세히 다루겠습니다.