HTTP/3 vs HTTP/2, QUIC 도입 기준 정리
결론부터
HTTP/3는 HTTP/2의 상위 버전이 아니다. HTTP 문법은 그대로 두고 아래 전송 계층을 TCP에서 UDP 기반 QUIC으로 갈아끼운 것에 가깝다. 그래서 켜면 무조건 빨라지는 스위치가 아니라, 손실과 지연이 큰 모바일·원거리 환경에서 이득이 크고 데이터센터 내부나 고대역폭 유선 구간에서는 오히려 느려질 수 있는 선택지다. 그리고 Spring Boot에는 아직 server.http3.enabled 같은 1급 지원 속성이 없다.
QUIC이 실제로 바꾼 세 가지
1. 전송 계층의 HOL 블로킹 제거. HTTP/2는 하나의 TCP 연결에 여러 스트림을 멀티플렉싱하지만, TCP는 결국 단일 바이트 스트림이다. 패킷 하나가 유실되면 이미 도착한 다른 스트림의 데이터까지 커널 버퍼에서 순서를 기다린다. QUIC은 스트림별로 순서 보장을 독립 관리하므로 한 스트림의 손실이 나머지를 막지 않는다. HTTP/2가 애플리케이션 계층에서만 풀었던 문제를 전송 계층까지 내려가 푼 셈이다.
2. 핸드셰이크 통합. TCP 3-way 핸드셰이크가 끝난 뒤 TLS 핸드셰이크가 따로 도는 구조에서는 첫 데이터 전송까지 최소 2 RTT가 필요하다. QUIC은 TLS 1.3을 전송 계층에 내장해 1-RTT로 끝내고, 재방문 시에는 0-RTT도 가능하다. 다만 0-RTT 데이터는 재전송 공격에 노출되므로 멱등한 요청에만 써야 한다.
3. 커넥션 마이그레이션. TCP 연결은 출발지·목적지 IP와 포트로 이루어진 4-튜플로 식별된다. 이동 중 Wi-Fi가 끊기고 셀룰러로 넘어가면 IP가 바뀌고 연결도 끊긴다. QUIC은 Connection ID로 연결을 식별하기 때문에 IP가 바뀌어도 세션이 유지된다. 모바일 비중이 높은 서비스에서 체감 차이가 가장 큰 지점이다.
한 가지 더 알아둘 것이 있다. 브라우저는 처음부터 HTTP/3로 붙지 못한다. 먼저 TCP로 연결한 뒤 응답 헤더의 Alt-Svc: h3=":443"; ma=86400를 보고 그다음 연결부터 HTTP/3를 시도한다. DNS의 HTTPS 레코드(RFC 9460)를 쓰면 첫 연결부터 협상할 수 있다.
Spring Boot에서 켜기
먼저 알아야 할 사실. Spring Boot는 HTTP/3를 공식 속성으로 지원하지 않는다. 관련 이슈(spring-boot#47163)에서 Spring 팀은 Netty의 HTTP/3·QUIC 코덱이 아직 인큐베이팅 단계라 이르다고 답했다. 서버 측 HTTP/3가 가능한 조합은 Reactor Netty 1.3 계열과 Jetty 12 계열뿐이고, Tomcat과 Undertow는 지원하지 않는다. 즉 WebFlux + Reactor Netty 환경에서 커스터마이저로 직접 켜야 한다.
의존성부터 추가한다. QUIC 코덱은 네이티브 라이브러리라 플랫폼 classifier가 필요하다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// QUIC 네이티브 코덱. 버전은 사용 중인 reactor-netty가 의존하는 값에 맞춘다.
runtimeOnly 'io.netty:netty-codec-native-quic:0.0.75.Final:linux-x86_64'
}
QUIC은 암호화가 기본이므로 SSL 번들이 반드시 있어야 한다.
spring.ssl.bundle.jks.server-http3.key.alias=http3
spring.ssl.bundle.jks.server-http3.keystore.location=classpath:keystore.p12
spring.ssl.bundle.jks.server-http3.keystore.password=changeit
그다음 서버 팩토리를 커스터마이징해 프로토콜을 HTTP/3로 바꾼다.
@Component
class Http3ServerCustomizer
implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
@Override
public void customize(NettyReactiveWebServerFactory factory) {
factory.addServerCustomizers(server -> {
SslBundle bundle = factory.getSslBundles().getBundle("server-http3");
Http3SslContextSpec sslContext = Http3SslContextSpec.forServer(
bundle.getManagers().getKeyManagerFactory(),
bundle.getKey().getPassword());
return server
.protocol(HttpProtocol.HTTP3) // 기본값은 HTTP/1.1이라 반드시 변경
.secure(spec -> spec.sslContext(sslContext))
.http3Settings(spec -> spec // 기본값이 없으므로 직접 지정
.idleTimeout(Duration.ofSeconds(5))
.maxData(10_000_000)
.maxStreamDataBidirectionalRemote(1_000_000)
.maxStreamsBidirectional(100));
});
}
}
컨트롤러는 손댈 필요가 없다. 클라이언트로 HTTP/3를 호출할 때도 같은 방식이다.
@Bean
WebClient http3WebClient() {
HttpClient httpClient = HttpClient.create()
.protocol(HttpProtocol.HTTP3)
.http3Settings(spec -> spec
.idleTimeout(Duration.ofSeconds(5))
.maxData(10_000_000)
.maxStreamDataBidirectionalLocal(1_000_000));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
동작 확인은 UDP 리스너와 Alt-Svc 헤더 두 가지를 본다.
# 서버에 UDP 리스너가 실제로 떠 있는지
ss -ulnp | grep ':443'
# Alt-Svc 광고 여부 (브라우저는 이 헤더를 보고 다음 연결부터 h3 시도)
curl -sI https://example.com | grep -i alt-svc
# HTTP/3 강제 요청 (HTTP/3를 지원하는 curl 필요)
curl --http3 -sI https://example.com | head -1
실무 주의사항
UDP 443을 열어야 한다. HTTP/3를 켜면 TCP 리스너와 UDP 리스너가 동시에 떠야 한다. 방화벽과 보안 그룹에서 UDP 443이 막혀 있으면 조용히 TCP로 폴백할 뿐 에러가 나지 않아 원인 파악이 늦어진다. 사내망이나 기업 프록시가 UDP를 차단하는 환경도 흔하다.
고대역폭에서는 역전될 수 있다. ACM Web Conference 2024에 발표된 측정 연구는 고속 회선에서 UDP+QUIC+HTTP/3 스택이 TCP+TLS+HTTP/2 대비 최대 45.2%의 데이터 전송률 감소를 보였고, 대역폭이 커질수록 격차가 벌어졌다고 보고했다. 원인은 프로토콜 설계가 아니라 수신 측 처리 오버헤드다. QUIC은 커널이 아닌 유저스페이스에서 돌고 ACK도 유저스페이스에서 처리하기 때문에 초당 처리해야 할 패킷이 많아질수록 CPU가 병목이 된다.
"지원"과 "실사용"은 다르다. Cloudflare Radar 데이터를 정리한 분석에 따르면 요청 기준 HTTP/3 비중은 20% 초반대에서 1년 가까이 정체 중이고, HTTP/2가 여전히 절반을 차지한다. 반면 W3Techs 기준으로 HTTP/3를 "지원"하는 사이트는 38%대다. 이 격차의 상당 부분은 앞서 설명한 Alt-Svc 발견 과정 때문이다. 사이트가 광고해도 브라우저가 실제로 협상하지 않는 첫 방문이 많다.
현실적인 배치는 엣지 종단이다. 애플리케이션 서버에서 직접 h3를 켜는 것보다, CDN이나 리버스 프록시(nginx 등) 엣지에서 HTTP/3를 종단하고 오리진과는 HTTP/2로 통신하는 구성이 운영 부담이 훨씬 적다. 클라이언트에 가까운 마지막 구간이 손실·지연이 큰 곳이고, QUIC의 이점도 대부분 거기서 나온다.
마무리
HTTP/3 도입 판단은 결국 트래픽 성격에 달렸다. 모바일 사용자가 많고 네트워크 품질 편차가 큰 서비스라면 엣지에서 켤 가치가 충분하다. 반대로 데이터센터 내부 통신, 사내 서비스, 대용량 파일 전송처럼 대역폭이 넉넉하고 손실이 적은 구간이라면 HTTP/2가 여전히 합리적인 선택이다. 자바 진영은 아직 과도기다. Reactor Netty로 실험은 가능하지만 Spring Boot의 1급 지원은 좀 더 기다려야 한다.
참고자료
- RFC 9114 (HTTP/3), RFC 9000 (QUIC), RFC 9460 (SVCB/HTTPS DNS 레코드)
- Spring Blog, "HTTP/3 support in Reactor 2024.0 Release Train"
- Zhang et al., "QUIC is not Quick Enough over Fast Internet", ACM Web Conference 2024
'Computer Science > NetWork' 카테고리의 다른 글
| 502 Bad Gateway가 떴다: Nginx가 범인인 척했지만 진범은 Spring Boot였다 (0) | 2026.07.19 |
|---|---|
| 오픈소스 공급망 공격, 2026년 개발자 대응 (0) | 2026.07.17 |
| CORS 동작 원리와 Spring Boot 설정 방법 (0) | 2026.07.03 |
| REST API 완벽 정리 — 개념, 설계 원칙, HTTP 메서드, 실전 예제까지 한방에 (0) | 2026.06.29 |
| 쿠키 vs 세션 vs 토큰(JWT) 완벽 정리 — 개념, 동작 원리, 코드, 보안까지 (0) | 2026.06.29 |