icon

안동민 개발노트

12장 : HTTP/2, HTTP/3, WebSocket

HTTP/2


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 HOL Blocking
하나의 TCP 연결
  요청1 ────→ 응답1 (3초 대기)
                    요청2 ────→ 응답2 (0.1초)
                                     요청3 ────→ 응답3
  └── 요청2는 0.1초면 되지만, 요청1 때문에 3초 대기 ──┘

브라우저의 우회: 도메인당 6개 TCP 연결
  연결1: 요청1 ──→ 응답1
  연결2: 요청2 ──→ 응답2
  연결3: 요청3 ──→ 응답3
  ...
  하지만 각 연결마다 TCP + TLS 핸드셰이크 비용 발생

개발자들은 성능 최적화를 위해 다양한 핵(hack)을 사용했습니다.

최적화 기법방식문제점
CSS 스프라이트여러 이미지를 하나로 합침유지보수 어려움
도메인 샤딩리소스를 여러 도메인에 분산DNS 조회 추가
인라이닝CSS/JS를 HTML에 직접 삽입캐시 불가
파일 합치기JS/CSS 파일 결합변경 시 전체 재다운로드

HTTP/2는 이런 핵이 필요 없도록 프로토콜 레벨에서 문제를 해결합니다.


바이너리 프레이밍과 멀티플렉싱

HTTP/2의 핵심 변화는 바이너리 프레이밍 계층(Binary Framing Layer)의 도입입니다.

HTTP/1.1 vs HTTP/2 메시지 형식
HTTP/1.1 (텍스트)
  GET / HTTP/1.1\r\n
  Host: example.com\r\n
  Accept: text/html\r\n
  \r\n
  → 사람이 읽을 수 있지만, 파싱 비용 높음

HTTP/2 (바이너리 프레임)
  ┌─────────────────────────────────┐
  │ Length (24비트) │ Type │ Flags  │
  ├─────────────────────────────────┤
  │ Stream ID (31비트)              │
  ├─────────────────────────────────┤
  │ Frame Payload                   │
  └─────────────────────────────────┘
  → 효율적 파싱, 멀티플렉싱 가능
멀티플렉싱 동작
HTTP/1.1: 하나씩 순서대로
  ────[요청A]────[응답A]────[요청B]────[응답B]────→ 시간

HTTP/2: 하나의 연결에서 동시 전송
  ────[A프레임][B프레임][C프레임][A프레임][B프레임]────→
       Stream1  Stream2  Stream3  Stream1  Stream2

  TCP 연결 1개 안에
    Stream 1: GET /index.html
    Stream 3: GET /style.css
    Stream 5: GET /app.js
    Stream 7: GET /image.png

  프레임이 뒤섞여 전송되고, 수신 측에서 Stream ID로 재조립
  → HOL Blocking 해결!
  → TCP 연결 1개로 충분 (핸드셰이크 비용 절감)

헤더 압축 (HPACK)

HTTP/1.1에서 매 요청마다 비슷한 헤더를 반복 전송했습니다.

HPACK 압축 원리
첫번째 요청
  :method: GET
  :path: /index.html
  user-agent: Chrome/120
  accept: text/html
  cookie: session=abc123
  → 전체 전송 (~200 bytes)

두번째 요청
  :method: GET
  :path: /style.css      ← 이것만 다름!
  user-agent: Chrome/120  ← 동일 (인덱스만 전송)
  accept: text/css        ← 이것만 다름!
  cookie: session=abc123  ← 동일 (인덱스만 전송)
  → 변경된 필드만 전송 (~20 bytes)

압축률: ~90% 감소!

정적 테이블 (미리 정의된 61개)
  인덱스 2: :method GET
  인덱스 3: :method POST
  인덱스 8: :status 200

동적 테이블 (연결 중 학습)
  연결에서 사용된 헤더를 추가
  같은 헤더 재사용 시 인덱스만 전송

서버 푸시

HTTP/2의 서버 푸시(Server Push)는 서버가 클라이언트의 요청을 예측하여 미리 자원을 보내는 기능입니다.

서버 푸시 vs 일반 요청
일반 HTTP
  클라이언트 ──GET /──→ 서버
  클라이언트 ←─ index.html ─ 서버
  (HTML 파싱 후)
  클라이언트 ──GET /style.css──→ 서버
  클라이언트 ←─ style.css ─ 서버
  → 2번의 왕복

서버 푸시
  클라이언트 ──GET /──→ 서버
  클라이언트 ←─ index.html ─ 서버
  클라이언트 ←─ style.css ─ 서버 (미리 전송!)
  → 1번의 왕복

하지만 실패한 이유
  * 브라우저 캐시에 이미 있어도 불필요하게 전송
  * 구현 복잡성 높음
  * Chrome 2022년 지원 제거
  → 대안: 103 Early Hints

HTTP/2의 남은 문제

HTTP/2는 HTTP 계층의 HOL Blocking을 해결했지만, TCP 계층의 HOL Blocking은 여전합니다.

TCP HOL Blocking (HTTP/2의 아킬레스건)
하나의 TCP 연결
  [Stream1][Stream2][Stream3][Stream1][Stream2]
                ↑ 이 패킷 유실!

  TCP는 스트림을 모름 → 전체 스트림이 대기:
  Stream1: ■■■ 대기 ■■■
  Stream2: ■■■ 대기 ■■■  ← 자기 패킷은 도착했는데!
  Stream3: ■■■ 대기 ■■■  ← 자기 패킷은 도착했는데!

  패킷 유실률이 높으면 (무선 네트워크):
  HTTP/2 (TCP 1개) < HTTP/1.1 (TCP 6개)
  오히려 느려지는 역설!
항목HTTP/1.1HTTP/2HTTP/3
프로토콜텍스트바이너리바이너리
전송 계층TCPTCPQUIC (UDP)
멀티플렉싱불가가능가능
HOL BlockingHTTP + TCPTCP만없음
헤더 압축없음HPACKQPACK
서버 푸시없음있음 (폐기)있음
연결 수립1-3 RTT1-3 RTT0-1 RTT
암호화선택사실상 필수필수

이 문제를 근본적으로 해결하기 위해 QUIC이 등장했습니다. 다음 절에서 자세히 다루겠습니다.

목차