생각을 정리하는 마인드맵, 풀맵 →
기술 딥다이브마지막 수정

[기술 딥다이브] HTTP/3는 왜 UDP 위에 있는가


한 줄 요약: HTTP/3가 TCP 대신 UDP 위의 QUIC을 쓰는 이유는, 패킷 하나의 손실이 전체 연결을 멈추는 TCP의 헤드오브라인 블로킹을 스트림별로 독립적인 신뢰성으로 해결하기 위해서다.

HTTP는 버전이 올라갈수록 빨라졌어요. HTTP/1.1에서 HTTP/2(멀티플렉싱)를 거쳐 HTTP/3에 이르렀어요. 그런데 HTTP/3는 이전 버전과 달리 TCP 대신 UDP 위에서 동작해요. ’신뢰성이 없는 UDP로 웹을?’이라는 의문이 들지만, 그럴 만한 이유가 있어요. 그리고 이 글의 전제가 되는 TCP 연결 설정은 3-way handshake 글에 정리돼 있어요.

TCP의 고질병: 헤드오브라인 블로킹(HOL blocking)

TCP는 ‘순서 보장’ 전송 프로토콜이에요. 패킷 1, 2, 3을 보냈는데 2가 지연되면 3은 2가 도착할 때까지 기다려요. 한 줄 계산대에서 맨 앞 사람이 잠깐 버벅이면 뒤 전원이 멈추는 것과 같아요. HTTP/2가 여러 요청을 하나의 TCP 연결로 묶어(멀티플렉싱) 보내더라도, TCP 수준에서 한 패킷이 손실되면 해당 연결의 모든 요청이 멈춰요. TCP 계층이 ’순서’를 강제하므로, 그 위의 HTTP는 어쩔 수 없어요. 이것이 TCP의 헤드오브라인 블로킹이에요.

QUIC의 해법: 스트림별로 독립인 신뢰성

아래 도식은 같은 패킷 손실 상황에서 TCP와 QUIC이 어떻게 다르게 반응하는지 보여줘요.

TCP와 QUIC의 패킷 손실 대응 차이 TCP는 한 패킷이 손실되면 그 뒤의 모든 데이터가 대기하는 헤드오브라인 블로킹이 일어나고, QUIC은 스트림별로 손실이 독립이라 다른 스트림은 계속 진행되는 것을 나란히 비교. TCP: 한 줄 대기열 패킷 3이 손실되면… 패킷 1 · 2 도착 ✓ 패킷 3 손실 ✗ 4 · 5 전부 대기 (HOL blocking) 연결된 모든 요청이 멈춘다 QUIC: 스트림별 대기열 스트림 B의 패킷이 손실되면… 스트림 A A1 A2 A3 스트림 B B1 B2 ✗ 재전송 대기 스트림 C C1 C2 C3 A와 C는 계속 전달된다 ✓ 손실은 해당 스트림만 재전송 TCP: 연결 전체가 하나의 순서 / QUIC: 연결 안에 여러 개의 독립된 순서

QUIC: UDP 위에 ‘직접 구현한’ 신뢰성

HTTP/3는 QUIC이라는 전송 프로토콜 위에서 동작해요. UDP 자체는 순서 보장과 재전송 기능이 없지만, QUIC이 그 기능을 애플리케이션 계층에서 직접 구현해요. 즉 ‘TCP의 역할을 UDP 위에서 다시 만들되, 더 효율적으로’ 설계한 것이에요. 왜 굳이 UDP 위에서 다시 만드느냐? TCP는 운영체제 커널에 박혀 있어 수정이 어렵고, 중간 장비(방화벽, 미들박스)가 TCP 헤더를 해석해 개입해요. UDP는 ’벌거벗은 배송 트럭’이라, 화물 관리를 완전히 애플리케이션이 자유롭게 설계할 수 있어요.

연결 설정도 빠르다: TLS를 포함한 1-RTT

QUIC은 TLS 1.3을 프로토콜 안에 통합했어요. TCP와 TLS를 각각 맺던 기존 방식은 왕복이 2~3번 필요하지만, QUIC은 연결 설정과 암호화 협상을 겹쳐 1-RTT로 줄이고, 이전 연결 정보가 있으면 0-RTT까지 가능해요. 모바일처럼 왕복 지연(RTT)이 큰 환경에서 체감 속도가 향상돼요.

연결 마이그레이션: IP가 바뀌어도 연결 유지

QUIC은 연결 식별을 IP 주소가 아니라 연결 ID로 수행해요. 사람을 좌석 번호가 아니라 이름으로 기억하면 옮겨 앉아도 대화가 이어지는 것과 같아서, 와이파이에서 LTE로 전환해 IP가 바뀌어도 연결이 유지돼요. TCP는 IP와 포트가 바뀌면 연결이 끊어졌지만 QUIC은 그렇지 않아요.

실제로 확인해 보기: alt-svc 헤더

브라우저가 HTTP/3로 ‘전환하는’ 과정은 실제 응답 헤더에서 직접 볼 수 있어요. cloudflare.com에 HTTPS 요청을 보낸 실제 출력이에요:

$ curl -sI https://cloudflare.com | grep -i alt-svc
alt-svc: h3=":443"; ma=86400

해석하면 이래요. 첫 요청은 HTTP/3를 모르는 상태에서 출발하므로 HTTP/2로 응답을 받아요(실제로 %{http_version} 값은 2였어요). 응답과 함께 서버가 이 헤더로 말해요: ‘이 서버는 443 포트에서 h3(HTTP/3)도 서비스한다. 86400초(24시간) 동안 이 정보를 믿어도 된다.’ 브라우저는 이 정보를 캐시해 두었다가 다음 요청부터 UDP 443으로 QUIC 연결을 시도해요. HTTP/3로의 전환이 점진적인 이유가 이 발견(discovery) 메커니즘 때문이에요.

참고로 QUIC은 UDP 기반이므로 네트워크가 UDP 443을 막으면 HTTP/3는 실패하고 HTTP/2로 낙하해요. 필자 환경에서도 HTTP/3를 강제한 연결은 성공하지 않았어요. 이 실패 자체가 설계의 증거예요: HTTP/3는 UDP 환경에 따라 선택적으로 쓰이고, 막혀 있으면 TCP 기반으로 자연스럽게 돌아가도록 만들어져 있어요.

어떻게 연결되는가

한 줄로 요약하면: TCP는 한 패킷의 손실이 전체 연결을 멈추게(HOL 블로킹) 하지만, QUIC은 UDP 위에서 신뢰성을 스트림별로 독립적으로 재구현해 그 문제를 해결하고, TLS 통합으로 연결 설정까지 단축한 전송 프로토콜이에요. ’신뢰성이 없는 UDP’를 쓰되 ’더 효율적으로 재구성한 것’이 HTTP/3의 핵심이에요.

출처: RFC 9114: HTTP/3 · RFC 9000: QUIC · 위키백과: HTTP/3

이미지: Renepick, Wikimedia Commons (CC-BY-SA 3.0) — 원본