컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
network · 18

HTTP/3(QUIC)

UDPQUICHTTP/3

HTTP/1.1과 HTTP/2는 TCP 위에서 동작합니다. TCP는 데이터를 순서대로, 빠짐없이 전달해 주는 믿을 만한 프로토콜이지만, 이 성질이 웹 성능에는 걸림돌이 되기도 합니다. 연결을 맺는 데 왕복이 여러 번 필요하고, 패킷 하나가 유실되면 그 뒤의 데이터가 모두 기다려야 하며, 네트워크가 바뀌면 연결이 끊어집니다. HTTP/3는 이런 문제를 줄이려고 TCP 대신 UDP 위에서 동작하는 QUIC을 전송 프로토콜로 씁니다.

QUIC은 UDP를 바탕으로 신뢰성 있는 전달, 혼잡 제어, 암호화(TLS 1.3), 여러 스트림의 다중화를 직접 제공하는 전송 프로토콜이며, 대부분의 구현이 운영체제 커널이 아니라 사용자 공간 라이브러리로 동작합니다. HTTP/3는 이 QUIC 위에서 HTTP의 의미(메서드, 헤더, 상태 코드)를 그대로 쓰는 새 매핑입니다. 이 토픽에서는 연결 수립에 걸리는 시간, 패킷 유실이 스트림에 미치는 영향, 네트워크가 바뀔 때의 연결 유지를 단계별로 봅니다.

핵심 개념

QUIC

UDP 위에서 동작하는 전송 프로토콜입니다. 재전송과 혼잡 제어로 신뢰성을 직접 보장하고, TLS 1.3 암호화를 기본으로 통합했습니다. 표준은 RFC 9000입니다.

핵심 개념

HTTP/3

QUIC 위에서 동작하는 HTTP 버전입니다. 요청과 응답의 의미는 HTTP/2와 같고, 전송 방식과 헤더 압축(QPACK)이 달라졌습니다. 표준은 RFC 9114입니다.

핵심 개념

HOL 블로킹

앞의 데이터가 늦어져 뒤의 데이터가 함께 기다리는 현상입니다. TCP 한 연결에 스트림을 여럿 태운 HTTP/2에서는 패킷 유실이 모든 스트림을 지연시킬 수 있습니다.

핵심 개념

스트림 다중화

QUIC은 한 연결 안에 여러 스트림을 두고, 스트림마다 독립적으로 순서를 관리합니다. 한 스트림의 유실이 다른 스트림의 전달을 막지 않습니다.

핵심 개념

0-RTT와 1-RTT

QUIC은 전송과 TLS 1.3 핸드셰이크를 합쳐 처음 연결을 1 RTT에 맺습니다. 전에 접속한 서버에는 첫 패킷에 데이터를 실어 보내는 0-RTT 재연결도 가능합니다.

핵심 개념

연결 ID와 연결 이동

QUIC은 연결을 IP 주소와 포트가 아니라 연결 ID로 식별합니다. 그래서 Wi-Fi에서 모바일 네트워크로 옮겨 가 주소가 바뀌어도 연결을 이어 갈 수 있습니다.

TCP의 한계를 전송 계층에서 풉니다 — HTTP/2가 HTTP 계층에서 다중화를 해도 아래의 TCP는 연결 하나의 순서만 알기 때문에, 유실의 영향은 모든 스트림에 번집니다. QUIC은 스트림을 전송 계층이 직접 압니다.
연결이 빨리 맺힙니다 — 전송과 암호화 핸드셰이크를 합쳐 처음 연결은 1 RTT, 재연결은 0 RTT에 요청을 보낼 수 있어, 왕복 시간이 길거나 손실이 많은 네트워크에서 체감이 큽니다.
항상 암호화됩니다 — QUIC은 TLS 1.3을 통합해 HTTP 헤더와 본문을 포함한 애플리케이션 데이터를 암호화하고 패킷 번호 같은 일부 헤더 필드도 보호합니다. 연결 ID 같은 최소한의 필드는 평문입니다. 암호화 없는 HTTP/3는 없습니다.
UDP가 막히면 TCP로 돌아갑니다 — 일부 네트워크는 UDP를 막거나 제한하므로, 브라우저는 HTTP/3가 안 되면 HTTP/2나 HTTP/1.1로 접속합니다. 서버는 TCP도 계속 열어 두어야 합니다.
이 시각화에서 확인할 것 — "연결 수립" 탭에서 첫 응답까지의 시간을, "패킷 유실" 탭에서 지연되는 패킷의 수를, "연결 이동" 탭에서 주소가 바뀔 때의 차이를 STEP으로 확인하세요.

HTTP/3의 배경

QUIC은 구글이 실험하던 프로토콜에서 출발해 IETF에서 표준화되었고, 2021년 5월에 RFC 9000(QUIC 전송), RFC 9001(TLS 통합), RFC 9002(유실 감지와 혼잡 제어)로 발표되었습니다. HTTP/3는 2022년 6월 RFC 9114로 표준이 되었습니다. TCP는 운영체제 커널과 중간 장비에 깊이 박혀 있어 고치기 어렵지만, QUIC은 UDP 위에서 대부분 사용자 공간 라이브러리로 구현되어 애플리케이션 업데이트만으로 개선할 수 있다는 점도 도입 이유의 하나입니다.

숫자로 확인하기

첫 응답까지의 시간 — RTT가 50ms이고 서버 처리 시간을 뺀다면 처음 연결 기준으로 TCP + TLS 1.3은 150ms, QUIC은 100ms, QUIC 0-RTT는 50ms에 첫 응답을 받습니다. RTT가 클수록 차이는 그만큼 커집니다.
유실 한 번의 영향 — 스트림 3개에 패킷을 3개씩 보내다 A2가 유실되면, 재전송이 올 때까지 TCP에서는 뒤따르는 패킷 5개가 대기하고 QUIC에서는 같은 스트림의 A3 1개만 대기합니다.
TCP 재연결 비용 — 네트워크가 바뀌어 TCP가 끊기면 TCP와 TLS 1.3 핸드셰이크 2 RTT를 다시 치러야 해 RTT 50ms 기준으로 핸드셰이크에만 100ms가 걸립니다(끊김을 알아채는 시간은 제외).
포트 — HTTP/3는 보통 UDP 443을 쓰고, HTTP/2는 TCP 443을 씁니다. 방화벽에서 두 포트를 모두 열어야 합니다.

자주 하는 오해

UDP를 쓰니 신뢰할 수 없다 — QUIC은 UDP 위에서 재전송, 순서 보장, 혼잡 제어를 직접 구현해 신뢰성 있는 전달을 제공합니다. UDP는 그 아래의 단순한 운반 수단일 뿐입니다.
HTTP/3는 항상 더 빠르다 — 유실이 많거나 RTT가 긴 환경에서 이점이 크지만, 네트워크가 안정적이면 차이가 작을 수 있습니다. 암호화와 패킷 처리를 사용자 공간에서 하므로 CPU 비용이 TCP보다 커지는 경우도 있습니다.
HTTP/3는 HTTP를 새로 설계한 것이다 — 메서드, 헤더, 상태 코드의 의미는 HTTP/2와 같고, 달라진 것은 이 의미를 실어 나르는 전송 방식입니다.
0-RTT는 항상 안전하다 — 0-RTT로 보낸 데이터는 공격자가 재전송할 수 있습니다. 조회처럼 여러 번 처리해도 되는 요청에만 쓰는 것이 일반적입니다.

면접에서 이렇게 물어요

면접 질문

HTTP/3는 HTTP/2와 무엇이 다른가요?

전송 계층이 TCP에서 UDP 위의 QUIC으로 바뀌었습니다. 그 결과 연결 수립이 빨라지고, 패킷 유실이 다른 스트림을 막는 HOL 블로킹이 줄며, 네트워크가 바뀌어도 연결을 유지할 수 있습니다.

면접 질문

HTTP/2의 HOL 블로킹이 왜 생기나요?

HTTP/2는 TCP 연결 하나에 여러 스트림을 태우는데, TCP는 연결 전체의 바이트 순서를 보장합니다. 패킷 하나가 유실되면 재전송될 때까지 뒤의 모든 스트림 데이터가 전달되지 못합니다.

면접 질문

QUIC은 UDP를 쓰는데 어떻게 신뢰성을 보장하나요?

QUIC 자체가 패킷 번호, 확인 응답, 재전송, 혼잡 제어를 구현합니다. 신뢰성을 UDP가 아니라 QUIC이 맡는 구조입니다.

면접 질문

브라우저는 서버가 HTTP/3를 지원하는지 어떻게 아나요?

보통 처음에는 TCP로 접속하고, 서버가 응답에 담아 보낸 Alt-Svc 헤더(예: h3=":443")를 보고 이후 요청부터 HTTP/3를 시도합니다. DNS의 HTTPS 레코드로 처음부터 알 수도 있습니다. 실패하면 TCP로 돌아갑니다.

실제 시스템에서는

브라우저와 CDN — 많은 CDN과 클라우드 로드 밸런서가 HTTP/3를 제공하고, 브라우저는 서버가 알려 주면 HTTP/3로 접속합니다. 사용자는 별다른 설정 없이 쓰게 됩니다.
서버 소프트웨어 — nginx, Caddy, 여러 CDN 엣지 등이 HTTP/3를 지원합니다. nginx는 QUIC을 지원하게 빌드한 버전에서 listen 지시문의 quic 옵션으로 켭니다.
방화벽과 모니터링 — UDP 443 트래픽을 허용해야 하고, 암호화된 QUIC 헤더 때문에 기존 TCP 중심의 패킷 분석 도구로는 연결 상태를 보기 어렵습니다.
모바일 앱 — 와이파이와 셀룰러를 오갈 때 연결 이동으로 연결을 이어 갈 수 있습니다.

함께 보면 좋은 토픽

HTTP — HTTP의 의미와 버전별 변화를 봅니다.
HTTPS / TLS — QUIC에 통합된 TLS 1.3 핸드셰이크를 봅니다.
TCP 혼잡 제어 — QUIC도 구현하는 혼잡 제어의 원리를 봅니다.
TCP/IP — TCP와 UDP가 놓인 계층 구조를 봅니다.

참고 자료

nginx, curl — HTTP/3 켜고 확인하기

# nginx.conf (HTTP/3를 지원하게 빌드된 nginx 1.25 이상)
server {
    listen 443 quic reuseport;     # UDP 443: HTTP/3
    listen 443 ssl;                # TCP 443: HTTP/2, HTTP/1.1 (대비용)
    http2 on;                      # nginx 1.25.1 이상
    http3 on;

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

    add_header Alt-Svc 'h3=":443"; ma=86400';
}

# 확인: Alt-Svc 헤더와 HTTP/3 응답
curl -sI https://www.example.com/ | grep -i alt-svc
curl --http3-only -I https://www.example.com/

HTTP/3는 QUIC 위에서 UDP로 동작하므로 nginx는 listen 443 quic으로 UDP 443 포트를 따로 열고, http3 on으로 HTTP/3를 켭니다. 같은 포트의 TCP에서는 listen 443 ssl과 http2 on(nginx 1.25.1 이상)으로 HTTP/2와 HTTP/1.1을 계속 처리합니다. 브라우저는 처음에 TCP로 접속해 응답의 Alt-Svc 헤더에서 이 서버가 h3를 지원한다는 것을 알고, 이후 요청부터 HTTP/3를 시도합니다. ma는 이 정보를 기억하는 시간(초)이며, UDP가 막힌 네트워크에서는 TCP로 대신 접속합니다. curl의 --http3-only 옵션은 HTTP/3를 지원하게 빌드된 curl에서만 동작하며, HTTP/3가 안 되면 다른 버전으로 대체하지 않고 실패합니다(--http3는 대체합니다). 방화벽에서 UDP 443이 열려 있어야 HTTP/3가 쓰입니다.

Python — 첫 응답까지 걸리는 시간 계산

RTT = 50                       # 왕복 시간 (ms)

def first_response(handshake_rtts):
    # 핸드셰이크 왕복 수 + 요청 · 응답 1왕복
    return (handshake_rtts + 1) * RTT

print(first_response(2))      # TCP 1 + TLS 1.3 1  -> 150
print(first_response(1))      # QUIC (TLS 1.3 통합)  -> 100
print(first_response(0))      # QUIC 0-RTT 재연결   -> 50

첫 응답이 도착하는 시간은 핸드셰이크에 쓰는 왕복 수에 요청과 응답의 1왕복을 더한 값에 RTT를 곱한 것입니다. 서버 처리 시간은 뺀 단순한 계산입니다. TCP는 연결 수립에 1 RTT, TLS 1.3 핸드셰이크에 1 RTT가 필요해 2를 넣고, QUIC은 전송과 TLS 1.3 핸드셰이크를 합쳐 1 RTT이므로 1을 넣습니다. 전에 접속한 서버와의 0-RTT 재연결은 첫 패킷에 요청을 실어 보내므로 0입니다. 이 값은 시각화의 연결 수립 탭이 보여 주는 150ms, 100ms, 50ms와 같으며, RTT가 클수록 차이도 그만큼 커집니다.

Python — 패킷 유실 때의 전달 가능 패킷 비교

WIRE = [(s, n) for n in range(1, 4) for s in "ABC"]   # A1 B1 C1 A2 ...
received = set(WIRE) - {("A", 2)}                  # A2가 유실됨

def deliver_tcp(received):
    out = []
    for pkt in WIRE:                 # 연결 전체의 순서를 지킴
        if pkt not in received:
            break
        out.append(pkt)
    return out

def deliver_quic(received):
    out = []
    for s in "ABC":                  # 스트림마다 순서를 지킴
        n = 1
        while (s, n) in received:
            out.append((s, n))
            n += 1
    return out

print(len(deliver_tcp(received)), len(deliver_quic(received)))   # 3 7

WIRE는 스트림 A, B, C의 패킷이 번갈아 도착하는 순서이고, 그 가운데 A2가 유실된 상황을 만듭니다. deliver_tcp는 연결 전체의 순서를 지키므로 첫 빈자리(A2)를 만나면 멈추고, 그 앞의 A1, B1, C1 세 개만 애플리케이션에 전달합니다. deliver_quic은 스트림마다 순서를 따로 지키므로 스트림 A는 A1에서 멈추지만 스트림 B와 C는 끝까지 전달해 모두 7개를 전달합니다. 이 차이가 TCP 위의 HTTP/2에서 생기는 HOL 블로킹과 HTTP/3에서 줄어드는 이유이며, 시각화의 패킷 유실 탭과 같은 결과입니다. 실제 QUIC은 유실된 패킷을 재전송해 결국 모든 데이터를 전달합니다.

Q1

HTTP/3이 사용하는 전송 프로토콜은?

HTTP/2까지는 TCP를 사용했지만 HTTP/3부터 바뀌었습니다.
HTTP/3은 TCP 대신 UDP 위에서 동작하는 QUIC 프로토콜을 사용합니다. QUIC이 신뢰성, 혼잡 제어, 암호화를 직접 처리합니다.

Q2

TCP 기반 HTTP/2에서 패킷 하나가 유실되면 어떤 문제가 생길까?

TCP는 바이트 스트림 전체의 순서를 보장합니다.
HTTP/2는 하나의 TCP 연결에 여러 스트림을 태우는데, TCP는 연결 전체의 순서를 보장하므로 패킷 하나가 유실되면 다른 스트림들도 대기합니다. QUIC은 스트림별로 독립적으로 처리해 이 문제를 줄입니다.

Q3

QUIC의 연결 수립 관련 장점은?

전송 계층 핸드셰이크와 TLS 핸드셰이크를 따로 하던 방식과 비교해 보세요.
QUIC은 전송 계층 연결과 TLS 1.3 핸드셰이크를 합쳐 처음 연결에 1-RTT, 이전에 접속한 서버에는 조건에 따라 0-RTT로 데이터를 보낼 수 있어 연결 지연이 줄어듭니다.