Skip to content

필사 모드: HTTP/3와 QUIC 내부 완전 해부 — 0-RTT, Stream Multiplexing, Connection Migration, BBR, WebTransport까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — "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 내부 구조](/blog/culture/2026-04-15-dns-internals-recursive-authoritative-edns-dnssec-doh-coredns-happy-eyeballs-deep-dive-guide-2025)에서 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의 핵심 선택:

1. **UDP 위에서 구현** — 커널 수정 없이 배포 가능

2. **TLS 1.3 통합** — 별도 핸드셰이크 X, 암호화 기본

3. **스트림이 각자 독립** — 한 스트림 손실이 다른 스트림을 막지 않음

3. QUIC 핸드셰이크 — 1-RTT와 0-RTT

TCP+TLS 1.2의 3-RTT 지옥

1. TCP SYN/SYN-ACK — 1 RTT

2. TLS ClientHello/ServerHello — 1 RTT

3. 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 — 두 번째부터는 공짜

이전에 한 번 연결했던 서버라면:

1. 클라이언트가 이전 세션의 **PSK(Pre-Shared Key)**를 기억

2. 첫 패킷에 **암호화된 데이터**를 바로 붙여 보냄

3. 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면 같은 연결.

플로우:

1. 와이파이에서 QUIC 연결 성립 (Conn ID = X)

2. 와이파이 끊김 → 모바일 LTE로 전환, 클라이언트 IP 변경

3. 클라이언트가 **같은 Conn ID**로 패킷 발송

4. 서버가 "아, 이건 기존 연결. 새 IP로 응답하겠음"

5. 사용자는 스트리밍이 끊기지 않음

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은:

1. 지속적으로 **max bandwidth** 추정

2. 지속적으로 **min RTT** 추정

3. `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가지

1. **CDN부터 QUIC ON** — 서버 구현은 CDN이 대신 해준다

2. **Alt-Svc 헤더 설정** — 브라우저에게 "나 HTTP/3도 돼"를 알림

3. **0-RTT는 GET에만** — 결제 등 비멱등은 금지

4. **UDP 버퍼 튜닝 / GSO ON** — 안 하면 성능 절반

5. **BBRv3 고려** — CUBIC보다 현대 네트워크에 유리

6. **방화벽에 UDP/443 명시 허용** — 사내 네트워크 잊지 말기

7. **qlog 기본 ON** — 디버깅 어렵지 않게

8. **Connection Migration 테스트** — 모바일 앱 QA 시 와이파이↔LTE 전환

9. **성능은 TCP 대비 반드시 측정** — CPU 오버헤드가 실제로 병목

10. **L4 LB는 Connection ID 인식 필요** — 아니면 Migration 깨짐

11. **QPACK 동적 테이블 사이즈** — 메모리 vs 압축률 트레이드오프

12. **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년 뒤에 다시 시도하는 프로젝트다. 이번엔 브라우저가 기본이라는 점이 다르다."**

현재 단락 (1/222)

30년 된 질문: TCP가 1974년 설계 이후 이렇게 오래 살아남은 이유는? 답: **작동하니까**.

작성 글자: 0원문 글자: 7,851작성 단락: 0/222