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 내부 구조에서 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