본문 바로가기

Computer Science/NetWork

HTTP/3 vs HTTP/2, QUIC 도입 기준 정리

추천캐릭터 2026. 7. 15. 13:00
728x90

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
728x90