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입니다.
하나의 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 (텍스트)
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에서 매 요청마다 비슷한 헤더를 반복 전송했습니다.
첫번째 요청
: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)는 서버가 클라이언트의 요청을 예측하여 미리 자원을 보내는 기능입니다.
일반 HTTP
클라이언트 ──GET /──→ 서버
클라이언트 ←─ index.html ─ 서버
(HTML 파싱 후)
클라이언트 ──GET /style.css──→ 서버
클라이언트 ←─ style.css ─ 서버
→ 2번의 왕복
서버 푸시
클라이언트 ──GET /──→ 서버
클라이언트 ←─ index.html ─ 서버
클라이언트 ←─ style.css ─ 서버 (미리 전송!)
→ 1번의 왕복
하지만 실패한 이유
* 브라우저 캐시에 이미 있어도 불필요하게 전송
* 구현 복잡성 높음
* Chrome 2022년 지원 제거
→ 대안: 103 Early HintsHTTP/2의 남은 문제
HTTP/2는 HTTP 계층의 HOL Blocking을 해결했지만, TCP 계층의 HOL Blocking은 여전합니다.
하나의 TCP 연결
[Stream1][Stream2][Stream3][Stream1][Stream2]
↑ 이 패킷 유실!
TCP는 스트림을 모름 → 전체 스트림이 대기:
Stream1: ■■■ 대기 ■■■
Stream2: ■■■ 대기 ■■■ ← 자기 패킷은 도착했는데!
Stream3: ■■■ 대기 ■■■ ← 자기 패킷은 도착했는데!
패킷 유실률이 높으면 (무선 네트워크):
HTTP/2 (TCP 1개) < HTTP/1.1 (TCP 6개)
오히려 느려지는 역설!| 항목 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 프로토콜 | 텍스트 | 바이너리 | 바이너리 |
| 전송 계층 | TCP | TCP | QUIC (UDP) |
| 멀티플렉싱 | 불가 | 가능 | 가능 |
| HOL Blocking | HTTP + TCP | TCP만 | 없음 |
| 헤더 압축 | 없음 | HPACK | QPACK |
| 서버 푸시 | 없음 | 있음 (폐기) | 있음 |
| 연결 수립 | 1-3 RTT | 1-3 RTT | 0-1 RTT |
| 암호화 | 선택 | 사실상 필수 | 필수 |
이 문제를 근본적으로 해결하기 위해 QUIC이 등장했습니다. 다음 절에서 자세히 다루겠습니다.