사무실 유선망에서는 멀쩡하던 서비스가 지하철이나 엘리베이터에서 유독 버벅인다. 원인을 파고들면 HTTP/2도 완벽하지 않다는 사실에 부딪힌다. HTTP/2는 하나의 TCP 연결 위에 여러 요청을 다중화(multiplexing)해 HTTP/1.1의 병목을 상당 부분 해소했지만, 그 아래에 깔린 TCP는 여전히 하나의 순서 있는 바이트 스트림이다. 패킷 하나가 손실되면 그 뒤에 도착한 멀쩡한 패킷들까지 커널 버퍼에 갇혀 애플리케이션으로 올라오지 못한다. 손실률이 조금만 높아져도 여러 스트림이 동시에 멈추는 이유다.

HTTP/3은 이 문제를 전송 계층에서부터 다시 설계한 프로토콜이다. TCP를 버리고 UDP 위에 QUIC이라는 새로운 전송 계층을 얹어, 스트림별로 독립적인 순서 보장을 제공한다. 즉 한 스트림에서 패킷이 손실돼도 다른 스트림은 영향을 받지 않는다. 그런데 모든 서비스가 지금 당장 HTTP/3으로 갈아타야 하는 것은 아니다. 이 글에서는 HTTP/2의 한계와 QUIC이 그것을 해결하는 원리, 실제로 빨라지는 상황과 별 차이 없는 상황, Nginx·CDN에서의 배포 방법, 그리고 관측·보안 주의점과 도입 판단 체크리스트까지 실전 관점에서 정리한다.

HTTP/2의 한계: TCP 레벨 헤드오브라인 블로킹

HTTP/2가 해결한 것은 애플리케이션 레벨의 헤드오브라인(Head-of-Line, HOL) 블로킹이다. HTTP/1.1에서는 한 연결에서 앞선 응답이 끝나야 다음 응답을 받을 수 있어, 느린 요청 하나가 뒤를 전부 막았다. HTTP/2는 요청·응답을 프레임 단위로 잘게 쪼개 하나의 연결에서 인터리빙하므로, 이 계층의 막힘은 사라졌다.

문제는 그보다 한 층 아래다. HTTP/2의 다중화된 스트림들은 모두 단 하나의 TCP 연결을 공유한다. TCP는 손실된 세그먼트를 재전송하고 순서를 맞춰 애플리케이션에 넘기는 것을 보장하는데, 이 순서 보장은 스트림을 구분하지 못한다. TCP 입장에서는 여러 HTTP/2 스트림이 그저 하나의 연속된 바이트 흐름일 뿐이다.

  • 스트림 A의 세그먼트 하나가 유실되면, TCP는 재전송이 도착할 때까지 그 뒤의 모든 바이트를 버퍼에 붙잡아 둔다
  • 이때 무관한 스트림 B, C의 데이터가 이미 도착해 있어도 애플리케이션으로 올라오지 못한다
  • 결국 다중화의 이점이 손실 상황에서 무너진다 — 이것이 TCP 레벨 HOL 블로킹이다

연결이 안정적인 유선망에서는 손실률이 낮아 이 현상이 드물게 나타난다. 하지만 셀룰러·와이파이 환경, 특히 이동 중에는 패킷 손실과 지연 변동이 잦아 이 병목이 체감 속도를 크게 갉아먹는다.

QUIC이 UDP 위에서 해결하는 원리

QUIC은 UDP를 전송 수단으로 쓴다. UDP는 순서 보장도, 재전송도 하지 않는 단순한 데이터그램 프로토콜이라, QUIC이 이 위에서 자체적으로 신뢰성·혼잡 제어·순서 보장을 구현한다. 핵심은 이 순서 보장을 스트림 단위로 독립시킨 것이다.

  • 스트림별 독립 전달: QUIC은 각 스트림을 별개의 순서 공간으로 다룬다. 스트림 A에서 패킷이 유실돼도 스트림 B·C는 도착 즉시 애플리케이션으로 전달된다. TCP 레벨 HOL 블로킹이 원천적으로 사라진다.
  • 0-RTT / 1-RTT 핸드셰이크: QUIC은 TLS 1.3을 전송 계층에 통합했다. 새 연결은 1-RTT로 암호화까지 끝나고, 재방문 시에는 이전 세션 정보를 활용해 0-RTT로 첫 요청 데이터를 핸드셰이크와 함께 즉시 보낸다. TCP+TLS의 순차적 왕복(3-way handshake 후 TLS 협상)에 비해 연결 수립 지연이 크게 줄어든다.
  • 연결 마이그레이션: TCP 연결은 (출발지 IP, 포트, 목적지 IP, 포트) 4-튜플로 식별된다. 와이파이에서 셀룰러로 넘어가 IP가 바뀌면 연결이 끊긴다. QUIC은 연결을 IP가 아닌 Connection ID로 식별하므로, 네트워크가 바뀌어도 핸드셰이크를 다시 하지 않고 같은 연결을 이어간다.

또한 QUIC의 신뢰성·혼잡 제어 로직은 커널이 아니라 사용자 공간(주로 애플리케이션·라이브러리)에서 동작한다. 운영체제 업데이트를 기다리지 않고 혼잡 제어 알고리즘을 개선·배포할 수 있다는 실무적 이점도 있다.

실제로 빨라지는 상황과 그렇지 않은 상황

HTTP/3은 만능이 아니다. 이득의 크기는 네트워크 조건에 달려 있다. 어디서 효과가 큰지 명확히 알아야 도입 판단이 선다.

확실히 빨라지는 상황:

  • 고지연 네트워크: RTT가 큰 원거리·위성·모바일 회선에서는 0-RTT/1-RTT 핸드셰이크의 왕복 절감이 첫 화면 표시 시간(TTFB)을 눈에 띄게 줄인다
  • 패킷 손실이 있는 환경: 손실률이 1~2%만 돼도 TCP 레벨 HOL 블로킹의 영향이 커지는데, QUIC은 스트림 독립성으로 이를 회피한다
  • 이동 중인 모바일: 와이파이↔셀룰러 전환, 기지국 핸드오버가 잦은 상황에서 연결 마이그레이션이 재연결 비용을 없앤다

별 차이가 없거나 오히려 손해일 수 있는 상황:

  • 안정적인 유선·저지연 네트워크: 손실이 거의 없고 RTT가 낮은 데이터센터 내부나 사무실 유선망에서는 HTTP/2 대비 체감 이득이 미미하다
  • CPU가 병목인 서버: QUIC의 암호화·순서 관리가 사용자 공간에서 이뤄져 커널 오프로드가 성숙한 TCP보다 CPU를 더 쓸 수 있다. 대량 트래픽에서 CPU 여유가 없다면 오히려 처리량이 떨어질 수 있다
  • UDP가 차단·throttling되는 망: 일부 기업·공공 네트워크는 UDP 443을 막거나 우선순위를 낮춘다. 이 경우 클라이언트는 HTTP/2(TCP)로 폴백하므로 최소한 느려지진 않지만, HTTP/3의 이득도 없다

정리하면, 트래픽의 상당 비중이 모바일·불안정 네트워크에서 온다면 HTTP/3의 가치가 크고, 내부 서비스나 안정적 유선 사용자가 대부분이라면 우선순위를 낮춰도 된다.

배포 방법: Nginx와 CDN에서 HTTP/3 켜기

HTTP/3은 HTTP/2를 대체하는 것이 아니라 병행한다. 서버는 TCP 443(HTTP/2)과 UDP 443(HTTP/3)을 동시에 열어두고, 클라이언트가 HTTP/3을 지원하면 그쪽으로 업그레이드하도록 유도한다. 이 업그레이드 신호가 Alt-Svc 헤더다.

Nginx는 1.25.0부터 HTTP/3(QUIC)을 정식 지원한다. 설정의 핵심은 UDP용 listen ... quic 지시자를 추가하고, 응답에 Alt-Svc 헤더를 붙이는 것이다.

server {
    # 기존 HTTP/2 (TCP 443)
    listen 443 ssl;
    http2 on;

    # HTTP/3 (QUIC, UDP 443) 추가
    listen 443 quic reuseport;

    server_name example.com;

    ssl_certificate     /etc/ssl/example.com.crt;
    ssl_certificate_key /etc/ssl/example.com.key;

    # QUIC은 TLS 1.3 필수
    ssl_protocols TLSv1.2 TLSv1.3;

    # 브라우저에 "이 출처는 UDP 443에서 h3로도 받는다"고 광고
    # ma=86400 → 24시간 동안 이 정보를 캐시
    add_header Alt-Svc 'h3=":443"; ma=86400' always;

    location / {
        proxy_pass http://backend;
    }
}

reuseport는 여러 워커 프로세스가 같은 UDP 포트를 커널 수준에서 분산 수신하게 해 성능에 중요하다. 브라우저는 첫 요청을 HTTP/2(TCP)로 보낸 뒤 응답의 Alt-Svc를 보고, 다음 연결부터 UDP 443으로 QUIC 연결을 시도한다. 즉 첫 접속이 곧바로 HTTP/3이 되는 것은 아니며, 두 번째 방문부터 전환되는 점을 이해해야 관측 데이터를 해석할 수 있다.

CDN을 쓴다면 대개 대시보드에서 HTTP/3 토글 하나로 활성화된다. Cloudflare·Fastly·CloudFront 등은 엣지에서 HTTP/3을 종단 처리하고 오리진과는 HTTP/2로 통신하는 구조라, 오리진 서버를 건드리지 않고도 사용자에게 HTTP/3 이득을 줄 수 있다. 자체 오리진 직접 서빙보다 CDN 앞단에서 켜는 편이 운영 부담이 훨씬 적다.

가장 흔한 함정은 방화벽이다. 그동안 웹 트래픽은 TCP 443만 열면 됐지만, HTTP/3은 UDP 443이 뚫려 있어야 한다. 서버·보안그룹·클라우드 방화벽에서 인바운드 UDP 443을 명시적으로 열어야 한다.

# Linux (ufw) — UDP 443 인바운드 허용
sudo ufw allow 443/udp

# firewalld
sudo firewall-cmd --permanent --add-port=443/udp
sudo firewall-cmd --reload

# AWS 보안그룹 (CLI 예시) — UDP 443 개방
aws ec2 authorize-security-group-ingress 
  --group-id sg-0abc123 
  --protocol udp --port 443 --cidr 0.0.0.0/0

관측과 디버깅: 정말 h3로 붙는지 확인하기

배포 후 반드시 실제로 HTTP/3으로 연결되는지 검증해야 한다. curl은 HTTP/3 지원 빌드(BoringSSL/quiche 등과 함께 컴파일된)에서 --http3 옵션으로 강제 테스트할 수 있다.

# HTTP/3으로만 강제 연결 (지원 안 되면 실패)
curl --http3-only -I https://example.com

# 응답 헤더에서 프로토콜과 Alt-Svc 확인
# HTTP/3 200
# alt-svc: h3=":443"; ma=86400

# Alt-Svc를 보고 자동 업그레이드하도록 두는 방식
curl --http3 -sSv https://example.com 2>&1 | grep -i "using HTTP"

브라우저에서는 개발자 도구 네트워크 탭의 Protocol 컬럼에 h3으로 표시되면 성공이다. 서버 접근 로그에도 프로토콜 버전을 남겨, 실제 트래픽 중 HTTP/3 비율을 지속 관측하는 것이 좋다.

# 로그 포맷에 협상된 프로토콜 기록
log_format h3log '$remote_addr "$request" $status '
                 'proto=$server_protocol ssl=$ssl_protocol';

access_log /var/log/nginx/access.log h3log;
# proto=HTTP/3.0 이면 QUIC으로 붙은 요청

보안 관점에서 반드시 짚어야 할 것이 0-RTT 재전송(replay) 공격이다. 0-RTT로 보내는 초기 데이터(early data)는 아직 핸드셰이크가 완료되기 전이라, 공격자가 이 패킷을 가로채 그대로 재전송하면 서버가 같은 요청을 두 번 처리할 위험이 있다. 원칙은 명확하다.

  • 0-RTT 구간에서는 멱등(idempotent)한 요청만 허용한다 — GET·HEAD처럼 여러 번 실행해도 부작용이 없는 것
  • 결제·주문 생성처럼 상태를 바꾸는 POST 같은 요청은 0-RTT early data로 처리하지 않도록 애플리케이션 레벨에서 차단한다
  • 서버·프록시에서 early data 처리 정책을 명시적으로 설정하고, 필요 없으면 0-RTT를 아예 끄는 것도 방법이다

도입 판단 체크리스트

기술적 매력만으로 결정하지 말고, 아래 항목을 점검해 우선순위를 매기는 것이 실무적이다.

  • 트래픽 프로파일: 모바일·해외·불안정 네트워크 비중이 높은가? 높다면 이득이 크다. 내부용·유선 위주라면 우선순위 낮음
  • CDN 사용 여부: CDN을 쓴다면 엣지 토글로 저비용·저위험 도입이 가능하다. 자체 오리진 직접 서빙이면 Nginx 1.25+ 등 버전과 빌드 확인이 먼저
  • 네트워크 경로의 UDP 443 개방: 서버 방화벽·보안그룹·중간 장비에서 UDP 443이 열려 있는가. 막혀 있으면 폴백만 되고 이득 없음
  • 서버 CPU 여유: QUIC은 CPU를 더 쓸 수 있다. 피크 시 CPU 여력이 있는지 확인
  • 폴백 안전성: HTTP/3 실패 시 HTTP/2로 자연 폴백되므로 리스크는 낮다. 그래도 카나리로 일부 트래픽부터 켜고 관측하라
  • 0-RTT 정책: early data를 켠다면 멱등 요청 제한이 애플리케이션에 반영됐는가

마무리

HTTP/3의 본질은 “더 빠른 HTTP”가 아니라 “TCP의 구조적 한계를 UDP 위 QUIC으로 우회한 전송 계층 교체”다. TCP 레벨 헤드오브라인 블로킹, 느린 연결 수립, IP 변경 시 연결 끊김이라는 세 가지 고질병을 스트림 독립성·0-RTT·연결 마이그레이션으로 정면 해결한다. 그래서 고지연·패킷손실·이동 중인 모바일 환경에서는 확실한 이득이 있지만, 안정적인 유선·저지연 환경에서는 체감 차이가 크지 않고 CPU 비용만 늘 수 있다. 다행히 HTTP/3은 HTTP/2와 병행하며 실패 시 자연스럽게 폴백되므로, 도입 리스크 자체는 낮다. CDN을 쓴다면 엣지 토글로 가볍게 시작하고, 자체 서빙이라면 UDP 443 방화벽과 Alt-Svc 헤더부터 챙긴 뒤 카나리로 관측하며 확대하는 것이 정석이다. 모든 서비스가 지금 당장 갈아탈 필요는 없지만, 트래픽의 무게 중심이 모바일에 있다면 미룰 이유도 없다.

자주 묻는 질문

Q. HTTP/3을 켜면 기존 HTTP/2 사용자는 어떻게 되나요?
A. 아무 영향이 없습니다. HTTP/3은 HTTP/2를 대체하는 것이 아니라 TCP 443과 UDP 443을 동시에 열어 병행합니다. HTTP/3을 지원하지 않는 클라이언트나 UDP가 막힌 네트워크의 사용자는 그대로 HTTP/2(TCP)로 접속합니다. 브라우저는 첫 요청을 HTTP/2로 보낸 뒤 Alt-Svc 헤더를 보고 다음부터 HTTP/3으로 업그레이드를 시도하므로, 폴백이 자동으로 이뤄져 서비스가 느려지거나 끊기는 일은 없습니다.

Q. QUIC은 UDP를 쓰는데, UDP는 신뢰성이 없지 않나요? 데이터가 유실되면요?
A. UDP 자체는 순서 보장도 재전송도 하지 않는 단순한 프로토콜이 맞습니다. 다만 QUIC이 그 위에서 신뢰성, 재전송, 순서 보장, 혼잡 제어를 모두 자체 구현합니다. 즉 신뢰성 계층을 TCP처럼 커널이 아니라 QUIC이 사용자 공간에서 담당할 뿐, 데이터 유실이 방치되는 것은 아닙니다. 오히려 이 신뢰성 보장을 스트림 단위로 독립시켜 TCP의 헤드오브라인 블로킹까지 해결한 것이 핵심입니다.

Q. 0-RTT는 지연을 줄여주니 무조건 켜는 게 좋은가요?
A. 무조건은 아닙니다. 0-RTT로 보내는 early data는 핸드셰이크 완료 전에 전송되기 때문에, 공격자가 이 패킷을 가로채 재전송하면 같은 요청이 두 번 실행되는 replay 위험이 있습니다. 그래서 0-RTT 구간에서는 GET·HEAD 같은 멱등 요청만 허용하고, 결제·주문 생성처럼 상태를 바꾸는 요청은 early data로 처리하지 않도록 막아야 합니다. 이 정책을 세우기 어렵거나 지연 절감 이득이 크지 않다면, 0-RTT를 끄고 1-RTT만 쓰는 것도 충분히 합리적인 선택입니다.