1. 개요
HTTP 버전이 올라갈 때마다 “더 빨라졌다” 는 얘기는 듣는데, 정확히 뭐가 느렸고 뭘 고쳤는지는 잘 모르고 넘어가기 쉽다. 세 버전을 관통하는 핵심 키워드는 하나다. 헤드 오브 라인 블로킹(Head-of-Line Blocking, HOL 블로킹). 앞의 요청이 막히면 뒤에 있는 멀쩡한 요청까지 같이 막히는 현상이다. 각 버전이 이 문제를 어떻게 풀었는지 순서대로 본다.
2. 핵심 내용
2-1. HTTP 1.1: 한 연결에 요청 하나
HTTP 1.1 은 기본적으로 한 TCP 연결 위에서 요청-응답을 순서대로 주고받는다. “파이프라이닝” 이라는 기능으로 여러 요청을 연달아 보낼 수는 있었지만, 응답은 반드시 보낸 순서대로 와야 했다. 그래서 첫 번째 요청 응답이 느리면 뒤의 요청들이 다 대기한다. 이게 HOL 블로킹이다.
브라우저는 이걸 우회하려고 도메인 하나당 TCP 연결을 6개 정도 동시에 열었다. 이미지, CSS, JS 를 위해 img1.example.com, img2.example.com 처럼 도메인을 쪼개 쓰던 것도(도메인 샤딩) 이 연결 수 제한을 피하려는 편법이었다.
2-2. HTTP 2: 한 연결에 여러 스트림을, 그런데 TCP 안에서
HTTP 2 는 하나의 TCP 연결 안에 여러 스트림을 만들어서 요청과 응답을 프레임 단위로 잘게 쪼개 인터리빙한다. 즉 요청 A 의 응답이 늦어도 요청 B, C 의 프레임은 같은 연결 위에서 섞여서 먼저 도착할 수 있다. 애플리케이션 레벨의 HOL 블로킹은 이렇게 풀었다.
문제는 TCP 자체다. TCP 는 바이트 순서를 보장하는 프로토콜이라, 패킷 하나가 유실되면 그 뒤에 도착한 패킷들도 커널이 순서를 맞추기 전까지 애플리케이션에 못 올려보낸다. 스트림을 아무리 여러 개 써도, 전송 계층에서 패킷 하나가 막히면 같은 TCP 연결 위의 다른 스트림들도 전부 같이 막힌다. 애플리케이션 계층의 HOL 블로킹을 풀었더니 전송 계층의 HOL 블로킹이 그대로 남아 있던 셈이다.
2-3. HTTP 3: TCP 를 버리고 QUIC 위로
HTTP 3 의 해법은 더 근본적이다. TCP 를 아예 안 쓴다. 대신 UDP 위에 구현한 QUIC 이라는 전송 프로토콜을 쓴다. QUIC 은 스트림 단위로 독립적인 순서 보장과 재전송을 한다. 스트림 A 의 패킷이 유실돼도 스트림 B, C 는 유실과 무관하게 애플리케이션에 바로 전달된다. TCP 수준에서부터 스트림이 분리돼 있으니 전송 계층 HOL 블로킹이 사라진다.
부수적인 이점도 있다. TCP 는 연결을 맺을 때 3-way handshake 를 거치고, 그 위에 TLS handshake 가 또 붙는다. QUIC 은 이 둘을 합쳐서 왕복 횟수를 줄였고(0-RTT 재연결까지 지원), 와이파이에서 LTE 로 네트워크가 바뀌어도 연결을 유지할 수 있는 커넥션 마이그레이션 기능도 갖고 있다. TCP 연결은 IP·포트 조합으로 식별되기 때문에 네트워크가 바뀌면 연결이 끊기지만, QUIC 은 연결 ID 로 식별해서 이 문제를 피한다.
2-4. 그래서 지금 뭘 써야 하나
Cloudflare, Google 같은 대형 CDN 은 이미 HTTP 3 를 기본으로 서빙한다. 다만 HTTP 3 는 UDP 를 쓰기 때문에 일부 사내망 방화벽이 UDP 443 을 막아두면 자동으로 HTTP 2 로 폴백된다. 실무에서는 버전을 강제하기보다, 서버와 CDN 이 Alt-Svc 헤더로 “나 HTTP 3 도 지원해” 라고 알려주면 클라이언트가 알아서 더 나은 버전으로 갈아타게 두는 게 일반적이다.
3. 마무리
요약
- HTTP 1.1 은 한 연결에 요청을 순서대로만 처리해 HOL 블로킹이 있었고, 브라우저는 연결을 여러 개 열어 우회했다.
- HTTP 2 는 한 연결 안에 스트림을 여러 개 둬서 애플리케이션 계층 HOL 블로킹을 풀었지만, TCP 의 순서 보장 때문에 전송 계층 HOL 블로킹이 남았다.
- HTTP 3 는 TCP 대신 UDP 기반 QUIC 을 써서 스트림별로 독립적인 재전송을 하고, 전송 계층 HOL 블로킹까지 없앴다.
다음은 5편: TLS 핸드셰이크 감 잡기 다.