HTTP/2를 공부하다 보면 multiplexing이 핵심 개선점으로 나온다. 하나의 연결 위에서 여러 stream을 섞어 보낼 수 있으니 HTTP/1.1처럼 응답 순서 때문에 뒤 요청이 막히는 문제를 줄일 수 있다는 설명이다. 그런데 바로 다음 질문이 남는다. HTTP/2도 결국 TCP 위에서 동작한다면, 패킷 하나가 유실됐을 때 TCP 계층의 순서 보장은 그대로 남지 않을까?
HTTP/3는 이 지점에서 출발한다. HTTP semantics 자체를 완전히 새로 만든다기보다, HTTP 메시지를 실어 나르는 전송 계층을 TCP+TLS에서 QUIC으로 바꾼다. QUIC은 RFC 9000에서 정의된 UDP 기반 전송 프로토콜이고, 암호화된 연결, stream multiplexing, 빠른 연결 수립, connection migration 같은 기능을 전송 계층 안에 포함한다.
처음에는 "UDP를 쓰면 빠르다" 정도로 단순하게 이해하기 쉬웠다. 하지만 RFC 9000을 따라가 보면 핵심은 UDP 자체가 아니라, TCP와 TLS와 HTTP/2가 나눠 갖고 있던 책임을 QUIC이 어떤 경계로 다시 묶었는가에 가깝다. 이 글에서는 HTTP/3가 QUIC을 쓰면서 무엇이 달라지는지, 그리고 그 차이가 실제 요청 흐름에서 어떤 의미를 갖는지 정리했다.
HTTP/3의 핵심은 UDP라서 빠른 것이 아니라, QUIC이 stream, 보안, 재전송, 연결 식별을 전송 계층에서 다시 설계했다는 점이다.
QUIC을 "UDP 버전의 TCP"라고만 부르면 중요한 부분이 빠진다. RFC 9000의 제목은 QUIC을 "UDP-Based Multiplexed and Secure Transport"라고 표현한다. 즉 QUIC은 UDP datagram을 사용하지만, 애플리케이션 입장에서는 단순 datagram API가 아니라 연결 지향적인 전송 기능을 제공한다.
TCP는 커널에 오래 들어 있던 범용 전송 프로토콜이다. 신뢰성 있는 바이트 스트림, 순서 보장, 혼잡 제어, 흐름 제어를 제공한다. TLS는 그 위에서 보안 채널을 만든다. HTTP/2는 다시 TLS로 보호된 TCP connection 위에 stream과 frame을 올린다.
QUIC은 이 경계를 다르게 잡는다. UDP 위에서 동작하되, QUIC 자체가 stream을 만들고, 패킷 번호를 관리하고, 손실을 감지하고, TLS 1.3과 결합해 암호화된 연결을 수립한다. 그래서 HTTP/3 입장에서는 TCP connection 하나 위에 HTTP/2 stream을 올리는 대신, QUIC connection 안의 stream을 HTTP 요청과 응답에 매핑한다.
간단히 비교하면 다음과 같다.
| 구분 | HTTP/2 | HTTP/3 |
|---|---|---|
| 전송 기반 | TCP | QUIC over UDP |
| 보안 | TLS가 TCP 위에 별도 계층으로 위치 | QUIC handshake가 TLS 1.3과 통합 |
| multiplexing | HTTP/2 frame/stream 계층 | QUIC stream 계층 |
| 손실 영향 | TCP 바이트 스트림 전체가 순서 대기 | 유실된 QUIC stream 중심으로 영향 제한 |
| 연결 식별 | 주로 4-tuple(IP, port)에 의존 | Connection ID 사용 |
이 표에서 가장 중요한 줄은 손실 영향과 연결 식별이다. HTTP/3가 체감 성능을 개선할 수 있는 이유가 주로 여기에서 나온다.
HTTP/2는 HTTP 계층의 Head-of-Line blocking을 크게 줄였다. 여러 요청과 응답을 stream으로 나누고 frame을 interleaving할 수 있기 때문이다. 하지만 TCP는 애플리케이션에 순서 있는 바이트 스트림을 제공한다. 중간의 TCP segment 하나가 유실되면, 그 뒤에 도착한 바이트가 커널 버퍼에 있어도 애플리케이션은 앞의 빈칸이 채워질 때까지 기다린다.
HTTP/2에서 이 상황은 하나의 stream만 늦는 문제가 아니라, 같은 TCP connection 위의 여러 HTTP/2 stream이 같이 영향을 받는 문제로 이어질 수 있다. HTTP/2 frame들이 모두 하나의 TCP 바이트 스트림 안에 섞여 있기 때문이다.
QUIC은 packet과 stream을 전송 계층에서 직접 다룬다. QUIC packet 하나가 유실됐을 때 그 packet 안에 어떤 stream frame이 들어 있었는지를 알 수 있고, 복구도 그 stream의 데이터 단위로 바라볼 수 있다. 다른 stream의 frame이 이미 도착했고 암호화 검증도 끝났다면, 애플리케이션은 그 stream을 계속 진행할 수 있다. RFC 9000은 QUIC이 flow-controlled streams를 제공한다고 설명하는데, 이 stream이 단순 HTTP 내부 개념이 아니라 전송 계층의 기본 단위라는 점이 차이다.
흐름을 아주 단순화하면 다음과 같다.
HTTP/2 over TCP
TCP bytes:
[stream A frame][stream B frame][stream C frame]
^
이 구간의 TCP segment 유실
결과:
뒤 bytes가 도착해도 TCP가 순서대로 애플리케이션에 넘기지 못함
stream B, C도 함께 대기할 수 있음
HTTP/3 over QUIC
QUIC packets:
packet 10: stream A data
packet 11: stream B data
packet 12: stream C data
^
packet 10 유실
결과:
stream A는 복구 대기
stream B, C는 가능한 범위에서 계속 처리
물론 이것이 "HTTP/3는 항상 빠르다"는 뜻은 아니다. 네트워크 상태, 서버와 클라이언트 구현, 중간 장비, 혼잡 제어, 캐시 조건에 따라 결과는 달라진다. 다만 구조적으로 보면 HTTP/3는 TCP 계층에서 생기는 connection 단위 HOL blocking을 QUIC stream 단위로 줄이려는 설계라고 이해할 수 있다.
HTTP/2에서 새 연결을 만든다고 생각해 보자. 일반적인 HTTPS라면 TCP handshake가 먼저 필요하고, 그 다음 TLS handshake가 이어진다. TLS 1.3은 이전보다 왕복 횟수를 줄였지만, TCP와 TLS가 별도 단계라는 구조 자체는 남아 있다.
QUIC은 연결 수립과 TLS 1.3 기반 키 협상을 통합한다. RFC 9000은 QUIC handshake가 cryptographic handshake와 transport handshake를 함께 수행한다고 설명한다. 클라이언트는 초기 QUIC packet에 TLS handshake 데이터를 담아 보내고, 양쪽은 암호화 키와 QUIC transport parameter를 협상한다.
처음 연결은 대략 이런 느낌이다.
HTTP/2 + TLS over TCP
Client Server
| ---- TCP SYN ----------> |
| <--- SYN/ACK ----------- |
| ---- ACK --------------> |
| ---- TLS ClientHello --> |
| <--- TLS messages ------ |
| ---- HTTP request -----> |
HTTP/3 over QUIC
Client Server
| ---- Initial QUIC packet
| + TLS ClientHello -> |
| <--- QUIC + TLS -------- |
| ---- HTTP/3 request ---> |
실제 handshake 세부는 인증서, 0-RTT 사용 여부, 서버 설정에 따라 달라진다. 그래도 큰 방향은 분명하다. QUIC은 전송 연결 수립과 암호화 협상을 한 프로토콜 안에서 같이 처리해서, 첫 요청을 보내기까지 필요한 대기 시간을 줄이려 한다.
여기서 0-RTT도 자주 언급된다. 이전에 연결했던 서버에 다시 접속할 때 클라이언트가 일부 데이터를 더 일찍 보낼 수 있는 기능이다. 다만 0-RTT 데이터는 재전송 공격 같은 보안 고려가 있어 모든 요청에 안전하게 쓸 수 있는 것은 아니다. 멱등성이 없는 요청, 예를 들어 결제나 상태 변경 요청에 무조건 적용하면 위험할 수 있다.
TCP connection은 보통 출발지 IP, 출발지 port, 목적지 IP, 목적지 port의 조합으로 식별된다. 이 조합을 4-tuple이라고 부른다. 문제는 모바일 환경에서 네트워크가 자주 바뀐다는 점이다. 지하철에서 Wi-Fi를 쓰다가 LTE로 전환되거나, NAT 환경에서 port가 바뀌면 기존 TCP connection은 유지되기 어렵다. 애플리케이션은 새 연결을 만들고, 필요한 경우 인증과 요청을 다시 진행해야 한다.
QUIC은 Connection ID를 사용한다. RFC 9000의 connection 관련 장에서는 QUIC endpoint가 connection ID를 발급하고, packet을 connection에 매칭하는 데 사용할 수 있다고 설명한다. 이 ID 덕분에 네트워크 경로가 바뀌더라도 peer가 같은 QUIC connection임을 식별할 수 있다.
connection migration은 이 구조 위에서 동작한다. 클라이언트의 IP나 port가 바뀌면 QUIC은 새 path를 검증하고, 성공하면 기존 connection 상태를 이어갈 수 있다. 특히 모바일 클라이언트나 이동 중 스트리밍처럼 네트워크 전환이 잦은 상황에서 의미가 있다.
다만 이것도 공짜는 아니다. 새 경로가 실제 peer의 경로인지 확인해야 하고, 주소 위조나 amplification attack을 막기 위한 검증이 필요하다. RFC 9000이 address validation, path validation, amplification attack을 별도 장으로 다루는 이유가 여기에 있다. QUIC은 이동성을 제공하지만, 그만큼 전송 계층에서 보안과 검증 책임도 함께 진다.
HTTP/3와 HTTP/2의 차이를 볼 때 "버전이 올라갔다"보다 "전송 계층의 책임 경계가 바뀌었다"는 관점이 더 도움이 됐다. HTTP/2는 TCP 위에서 HTTP 메시지를 stream으로 나누었다. HTTP/3는 QUIC 위에서 동작하며, QUIC 자체가 stream, packet number, 손실 복구, TLS 1.3 handshake, connection ID를 다룬다.
핵심은 세 가지로 정리할 수 있다.
첫째, QUIC stream은 전송 계층의 단위라서 TCP 바이트 스트림의 순서 대기 문제를 줄일 수 있다. 둘째, QUIC은 TLS 1.3과 handshake를 통합해 연결 수립 지연을 줄이는 방향으로 설계됐다. 셋째, Connection ID와 path validation을 통해 네트워크 경로가 바뀌어도 연결을 이어갈 수 있는 기반을 제공한다.
반대로 HTTP/3가 모든 상황에서 HTTP/2보다 빠르다고 단정하면 안 된다. UDP 차단이나 rate limit이 있는 네트워크, 구현 성숙도, 서버 설정, CDN 경로, 혼잡 제어 차이에 따라 체감은 달라질 수 있다. 그래도 RFC 9000을 기준으로 구조를 따라가 보면, HTTP/3가 왜 TCP 대신 QUIC을 선택했는지는 비교적 분명해진다. HTTP의 semantics를 유지하면서, 전송 계층의 오래된 병목과 이동성 문제를 다른 방식으로 풀기 위한 선택이다.
다음에 더 파고들 주제로는 HTTP/3 자체를 정의한 RFC 9114의 frame 구조, 그리고 QUIC loss detection과 congestion control을 다루는 RFC 9002가 있다. 이번 글은 그중 기반이 되는 QUIC transport의 큰 차이에 초점을 맞췄다.