- Published on
HTTP/3와 QUIC 내부 완전 해부 — 0-RTT, Stream Multiplexing, Connection Migration, BBR, WebTransport까지
- Authors

- Name
- Youngju Kim
- @fjvbn20031
들어가며 — "TCP는 왜 못 끝냈나"
30년 된 질문: TCP가 1974년 설계 이후 이렇게 오래 살아남은 이유는? 답: 작동하니까.
그런데 왜 Google, Cloudflare, Meta가 2012년부터 TCP를 UDP로 대체하려고 수억 달러를 썼을까? TCP에 근본적 문제가 있기 때문이다. 이 글에서는:
- HTTP/2의 HOL(Head-of-Line) 블로킹이 왜 TCP 레벨에선 해결 불가능한가
- QUIC이 어떻게 TCP+TLS+HTTP/2의 기능을 UDP 위에서 재설계했나
- 0-RTT의 원리와 리플레이 공격 위험
- Connection Migration — IP가 바뀌어도 연결이 유지되는 마법
- HTTP/3의 프레임 구조와 HPACK → QPACK 진화
- BBR 혼잡제어가 QUIC에 잘 어울리는 이유
- WebTransport — QUIC 위에 쓰는 새 메시징
- 실측 채택률과 현실의 함정
이전 글 DNS 내부 구조에서 UDP 기반 53번 포트의 세계를 봤다. QUIC은 같은 UDP를 쓰지만 TLS + 신뢰성 + 멀티스트림까지 다 얹는다.
1. HTTP/1.1 → HTTP/2 → HTTP/3의 흐름
HTTP/1.1 (1997) — 한 번에 하나
- Keep-Alive로 TCP 재사용
- 하지만 순차적 요청: 한 응답이 끝나야 다음 요청
- 해결책: 브라우저가 도메인당 6개 TCP 연결 병렬로 사용
- 문제: 6개 각각 TCP 슬로우 스타트, 6배 CPU
HTTP/2 (2015) — Binary + Multiplexing
- 한 TCP 연결 안에 여러 요청 병렬 처리 (Streams)
- 헤더 압축 HPACK
- 서버 푸시, 프레임 기반
하지만 결정적 문제가 남는다.
HTTP/2의 치명적 약점: TCP HOL Blocking
한 TCP 연결 위에 스트림 10개. 그런데 1번 스트림의 패킷 하나가 손실되면? TCP는 순서 보장을 위해 뒤의 모든 패킷을 버퍼에 쌓아두고 기다린다. 다른 9개 스트림도 멈춘다.
HTTP/2 멀티플렉싱의 철학("하나가 느려도 나머진 가")이 TCP 계층에서 깨진다.
HTTP/3 (2022 RFC 9114) — UDP로 내려가다
TCP를 버리고 UDP 위에 직접 QUIC이라는 새 전송 프로토콜을 설계. HTTP/3는 QUIC 위의 HTTP다.
2. QUIC의 계층 구조 — "프로토콜 Shift Left"
┌──────────────────────────┐ ┌───────────────────────────┐
│ HTTP/2 │ │ HTTP/3 │
├──────────────────────────┤ ├───────────────────────────┤
│ TLS 1.2/1.3 │ │ QUIC (TLS 1.3 내장) │
├──────────────────────────┤ │ + 스트림 │
│ TCP │ │ + 신뢰성 │
├──────────────────────────┤ │ + 혼잡제어 │
│ IP │ ├───────────────────────────┤
└──────────────────────────┘ │ UDP │
├───────────────────────────┤
│ IP │
└───────────────────────────┘
QUIC의 핵심 선택:
- UDP 위에서 구현 — 커널 수정 없이 배포 가능
- TLS 1.3 통합 — 별도 핸드셰이크 X, 암호화 기본
- 스트림이 각자 독립 — 한 스트림 손실이 다른 스트림을 막지 않음
3. QUIC 핸드셰이크 — 1-RTT와 0-RTT
TCP+TLS 1.2의 3-RTT 지옥
- TCP SYN/SYN-ACK — 1 RTT
- TLS ClientHello/ServerHello — 1 RTT
- TLS Key exchange — 1 RTT
= 3 RTT. 아시아↔미국 왕복 약 600ms. 3번이면 1.8초.
TCP+TLS 1.3 — 2-RTT
TLS 1.3이 1-RTT로 줄였지만, TCP 1-RTT가 남음. 총 2-RTT.
QUIC — 1-RTT (첫 연결)
클라이언트 서버
│ │
│── Initial + ClientHello ──>│
│ │
│<── Initial + ServerHello ─│
│<── Handshake + Cert ──────│
│<── Handshake Finished ────│
│ │
│── Handshake Finished ────>│
│── HTTP Request (1-RTT) ──>│
│ │
│<── HTTP Response ─────────│
TLS 핸드셰이크가 QUIC 전송 핸드셰이크와 결합되어 1 RTT.
0-RTT — 두 번째부터는 공짜
이전에 한 번 연결했던 서버라면:
- 클라이언트가 이전 세션의 **PSK(Pre-Shared Key)**를 기억
- 첫 패킷에 암호화된 데이터를 바로 붙여 보냄
- RTT 없이 HTTP 요청이 서버에 도달
즉 첫 바이트 지연 = 거의 0 (UDP 지연만).
0-RTT의 대가: Replay Attack
- 공격자가 0-RTT 패킷을 캡처
- 나중에 다시 보내면 서버가 똑같이 처리할 가능성
- 결제 같은 멱등하지 않은 요청이면 위험
해결:
- 서버가 0-RTT에는 GET만 허용
- Single-use 토큰 활용
- 애플리케이션 계층에서 멱등성 설계
4. Stream Multiplexing — HOL 없는 진짜 병렬
QUIC 스트림의 독립성
각 스트림이 자기만의 전송 상태 머신을 가진다.
- Stream 3의 패킷 손실 → Stream 3만 재전송 대기
- Stream 5, 7은 정상 처리
TCP에서는 OS 커널의 TCP 스택이 한 시퀀스 번호 공간을 관리하므로 불가능. QUIC은 유저스페이스라 자유롭다.
Stream ID 규칙
양방향 스트림 (Client Init): 0, 4, 8, 12, ...
양방향 스트림 (Server Init): 1, 5, 9, 13, ...
단방향 스트림 (Client Init): 2, 6, 10, 14, ...
단방향 스트림 (Server Init): 3, 7, 11, 15, ...
각 방향이 독립된 2비트 식별자를 가진다.
Flow Control 이중 구조
- 스트림 레벨 — 각 스트림별 window
- 연결 레벨 — 전체 연결의 window
둘 다 넘지 않아야 전송 가능. 메모리 사용 제어의 핵심.
5. Connection Migration — 와이파이에서 LTE로 매끄럽게
TCP의 제약
TCP 연결은 (src IP, src port, dst IP, dst port) 4튜플로 정의. 클라이언트 IP가 바뀌면(와이파이 → 4G) 연결 끊김. 재연결에 RTT × N 소비.
QUIC의 해법: Connection ID
QUIC은 패킷에 Connection ID를 심는다. IP/포트가 바뀌어도 같은 Connection ID면 같은 연결.
플로우:
- 와이파이에서 QUIC 연결 성립 (Conn ID = X)
- 와이파이 끊김 → 모바일 LTE로 전환, 클라이언트 IP 변경
- 클라이언트가 같은 Conn ID로 패킷 발송
- 서버가 "아, 이건 기존 연결. 새 IP로 응답하겠음"
- 사용자는 스트리밍이 끊기지 않음
Path Validation
위장된 IP로 다른 사용자 연결을 훔치지 못하게, 서버가 새 경로에 PATH_CHALLENGE/PATH_RESPONSE로 왕복 테스트. 응답 정상이면 경로 수용.
이 기능 덕에 모바일에서 YouTube/Meet 같은 실시간 스트림이 끊김 없이 유지된다.
6. 패킷 구조와 암호화
QUIC 패킷의 두 형태
Long Header (핸드셰이크)
+-+-+-+-+-+-+-+-+
|1|1| Type |...|
+-+-+-+-+-+-+-+-+
| Version (32) |
+---------------+
| DCID Length |
| DCID | (목적지 Connection ID)
+---------------+
| SCID Length |
| SCID | (출발지 Connection ID)
+---------------+
| Packet Number |
| Payload (암호화)|
+---------------+
Short Header (데이터)
+-+-+-+-+-+-+-+-+
|0|1|S|R|R|K|PP| (Spin, Reserved, Key phase, PN length)
+---------------+
| DCID |
+---------------+
| Packet Number |
| Payload (암호화)|
+---------------+
"모든 것이 암호화된다"
QUIC은 Connection ID를 제외한 거의 모든 헤더 필드까지 암호화. 네트워크 중간 장비가 QUIC 내부를 뜯어볼 수 없다. 장점: 프라이버시, 프로토콜 진화의 자유. 단점: 기존 DPI(Deep Packet Inspection)가 먹통.
Header Protection
Packet Number 필드까지 암호화된다. 이를 위해 AEAD(AES-GCM, ChaCha20-Poly1305) 기반의 별도 header protection 키. 이 덕에 중간에 패킷 번호를 추적해 fingerprint도 못 만듦.
7. HTTP/3 프레임과 QPACK
HTTP/3 프레임
HTTP/3는 QUIC 위에서 8개 정도의 프레임만 정의.
DATA - 본문
HEADERS - HTTP 헤더
CANCEL_PUSH - 서버 푸시 취소
SETTINGS - 설정 교환
PUSH_PROMISE - 푸시 안내
GOAWAY - 연결 종료 통지
MAX_PUSH_ID - 푸시 ID 한계
HTTP/2의 WINDOW_UPDATE, PRIORITY, PING 등은 QUIC으로 이동(QUIC이 이미 처리).
QPACK — HPACK의 QUIC 버전
HPACK(HTTP/2)은 동적 헤더 테이블을 쌍방 동기화로 관리. 문제: 순서 의존성. 한 프레임이 늦게 오면 다음 프레임 압축 해제 불가 → HOL 재출현.
QPACK은 동적 테이블 업데이트와 헤더 참조를 분리. 별도 Encoder Stream / Decoder Stream으로 동기화. 다른 요청 스트림의 지연 없이 압축 효과 유지.
8. 혼잡 제어 — BBR이 왜 QUIC과 궁합인가
전통 Loss-based CC (Cubic)
"패킷 손실 = 혼잡 신호". 모뎀 시대엔 합리적이었다. 현대 무선망에선:
- 손실이 혼잡과 무관한 경우 많음 (RF 잡음)
- 버퍼블로트(bufferbloat)로 지연만 증가, 손실은 늦게
BBR (Bottleneck Bandwidth + RTT) — 2016 Google
"혼잡의 본질은 bottleneck 대역폭과 왕복 지연의 곱". BBR은:
- 지속적으로 max bandwidth 추정
- 지속적으로 min RTT 추정
BDP = bandwidth × RTT만큼만 in-flight 유지
손실이 아닌 지연 증가를 혼잡 신호로 사용. 버퍼블로트 환경에서 월등한 throughput.
QUIC이 BBR과 잘 맞는 이유
- QUIC은 유저스페이스에서 CC 알고리즘 교체 자유
- QUIC은 패킷마다 정확한 RTT 샘플 가능 (별도 ACK 없음)
- TCP는 커널 재컴파일 필요하지만 QUIC은 라이브러리 업데이트면 끝
Google은 YouTube/Search에서 QUIC+BBRv3로 throughput +10~15%, rebuffering -18% 보고.
9. WebTransport — QUIC 위의 새 메시징
WebSocket의 한계
- TCP 기반 → HOL blocking
- 한 연결이 멀티 스트림 아님
- 재연결 시 전체 상태 복원
WebTransport (2023 W3C Draft)
브라우저가 QUIC 스트림과 데이터그램에 직접 접근.
const transport = new WebTransport("https://example.com/chat");
await transport.ready;
// 신뢰성 스트림
const stream = await transport.createUnidirectionalStream();
const writer = stream.getWriter();
writer.write(new TextEncoder().encode("hello"));
// 비신뢰성 데이터그램 (실시간 음성/게임)
transport.datagrams.writable.getWriter().write(packet);
용도:
- 게임(순서 중요 X, 속도 중요)
- 실시간 공동작업
- 미디어 스트리밍
WebRTC가 P2P에 강점이라면 WebTransport는 서버-클라이언트에 최적.
10. HTTP/3 채택률과 현실
실측 데이터 (2025 기준)
- Cloudflare 트래픽: HTTP/3 35~40%
- Google 자체 서비스 (YouTube 등): 75%+
- Meta (Facebook/Instagram): 대부분 QUIC
- 상위 1000 사이트 중 HTTP/3 지원: 약 30%
브라우저 지원:
- Chrome 87+, Firefox 88+, Safari 14+, Edge 87+ 모두 지원
- 모바일에서 채택이 빠름 (네트워크 스위칭 이득 큼)
서버 구현:
- nginx — 1.25+ 실험적 지원
- Envoy — 완전 지원
- Caddy — 기본 ON
- Cloudflare — 완전 운영
- HAProxy — 2.6+ 지원
"왜 아직 100%가 아닌가"
- UDP 방화벽 차단 — 사내 네트워크가 UDP/443을 막음
- 중간 장비 (NAT, 로드밸런서)의 QUIC 미지원
- CPU 오버헤드 — 유저스페이스 처리 비용 (TCP는 커널에서 최적화된 지) ~+30% CPU
- 관측성 도구 부족 — 암호화 때문에 기존 pcap 분석 어려움
11. QUIC 구현체들
Go
- quic-go — 가장 활발, Cloudflare 기여
- Caddy가 이걸 씀
Rust
- quinn — 프로덕션급, Cloudflare pingora
- quiche — Cloudflare 자체, FFI로 C에서도 사용
C/C++
- lsquic — LiteSpeed, 매우 빠름
- msquic — Microsoft, Windows 커널 지원
- ngtcp2 — 라이브러리, nginx가 사용 중
성능 경쟁
벤치마크마다 다르지만 msquic, quiche, lsquic이 상위권. 단일 코어 1M req/s 달성 사례도.
12. 운영 이슈들 — 현장에서 마주치는 문제
이슈 1: UDP 버퍼 튜닝
리눅스 기본 net.core.rmem_max 212KB로는 QUIC 부하에 부족. 운영 권장:
net.core.rmem_max = 7500000
net.core.wmem_max = 7500000
이슈 2: 세그멘테이션 오프로드
GSO (Generic Segmentation Offload) / GRO (Receive) 활성화로 QUIC 처리량 2~3배 향상. 커널 5.0+, NIC 지원 필수.
이슈 3: 방화벽 정책
- UDP 플러드로 오인 → 드롭
- 해결: QUIC 트래픽 별도 레이트 리밋 정책
- Cloudflare/Fastly는 스캐닝 공격 패턴 탐지로 구분
이슈 4: 관측성
- qlog — QUIC 전용 구조화 로그 포맷
- qvis — qlog 시각화 도구
- Wireshark 4.0+ QUIC 파싱 지원 (TLS 키 로그 필요)
이슈 5: Anycast + Connection ID
- Anycast로 가까운 노드로 라우팅되지만, BGP 변화 시 다른 노드로 갈 수 있음
- 이때 Connection ID 기반 stateless reset 또는 로드밸런서의 routing consistent hash로 해결
13. QUIC vs WebSocket vs SSE 선택 가이드
| 요구사항 | 추천 |
|---|---|
| 단방향 서버→클라이언트 실시간 알림 | SSE (간단, HTTP/2 이상) |
| 양방향 메시징, 낮은 지연 | WebSocket 또는 WebTransport |
| 대량 병렬 스트림 | WebTransport |
| 실시간 미디어 (순서 < 지연) | WebTransport 데이터그램 또는 WebRTC |
| 모바일 + 연결 안정성 | WebTransport |
| 브라우저 호환성 최대 | WebSocket (당분간) |
14. 실전 체크리스트 12가지
- CDN부터 QUIC ON — 서버 구현은 CDN이 대신 해준다
- Alt-Svc 헤더 설정 — 브라우저에게 "나 HTTP/3도 돼"를 알림
- 0-RTT는 GET에만 — 결제 등 비멱등은 금지
- UDP 버퍼 튜닝 / GSO ON — 안 하면 성능 절반
- BBRv3 고려 — CUBIC보다 현대 네트워크에 유리
- 방화벽에 UDP/443 명시 허용 — 사내 네트워크 잊지 말기
- qlog 기본 ON — 디버깅 어렵지 않게
- Connection Migration 테스트 — 모바일 앱 QA 시 와이파이↔LTE 전환
- 성능은 TCP 대비 반드시 측정 — CPU 오버헤드가 실제로 병목
- L4 LB는 Connection ID 인식 필요 — 아니면 Migration 깨짐
- QPACK 동적 테이블 사이즈 — 메모리 vs 압축률 트레이드오프
- WebTransport PoC부터 — 기존 WebSocket을 한 번에 바꾸지 말기
다음 글 예고 — WebAssembly의 세계
HTTP/3가 "전송 계층의 근본 재설계"라면, WebAssembly는 "브라우저 런타임의 근본 재설계"다. 다음 글에서는:
- WebAssembly의 탄생 — asm.js에서 Wasm 1.0까지
- Wasm의 바이트코드 구조와 실행 모델
- 왜 sandbox가 강력한가 — Capability-based security
- WASI — 브라우저 밖의 Wasm (Docker의 대항마?)
- Wasmtime, Wasmer, WasmEdge 런타임 비교
- Component Model — 언어 중립 모듈 시스템
- 엣지 컴퓨팅 (Cloudflare Workers, Fastly)의 Wasm 전략
- GC 제안과 Wasm의 미래
"브라우저에서 C++을 돌린다"는 낭만이 어떻게 백엔드를 바꾸는 현실이 됐는지 짚어보자.
"Wasm은 플랫폼 중립 바이트코드라는 JVM의 꿈을 30년 뒤에 다시 시도하는 프로젝트다. 이번엔 브라우저가 기본이라는 점이 다르다."