무엇이 문제였나
HTTP/1.1은 커넥션 하나에 요청 하나를 처리한다. 앞선 요청이 끝나야 다음 요청이 나갈 수 있으니, 느린 응답 하나가 뒤의 모든 요청을 붙잡는다. 브라우저는 이걸 도메인당 커넥션을 여섯 개쯤 여는 것으로 우회했다. 문제를 푼 게 아니라 병렬로 여러 개 만든 것이다.
HTTP/2 — 커넥션 하나에 여러 흐름
HTTP/2는 커넥션 하나 안에 스트림이라는 논리적 통로를 여러 개 만든다. 요청과 응답을 프레임 단위로 쪼개 섞어 보내고 받는 쪽에서 다시 조립한다. 요청 순서와 응답 순서가 달라도 되니 앞의 응답을 기다릴 이유가 없다.
헤더도 압축한다(HPACK). 요청마다 거의 같은 쿠키와 User-Agent를 매번 온전히 보내던 낭비가 사라진다.
남은 병목은 TCP에 있었다
그런데 스트림을 아무리 나눠도 그 아래는 여전히 TCP 하나다. TCP는 바이트 순서를 보장하므로 패킷 하나가 유실되면 재전송이 도착할 때까지 그 뒤의 모든 데이터를 애플리케이션에 넘기지 않는다. 유실과 무관한 다른 스트림까지 함께 멈춘다. HTTP 계층에서 줄을 없앴더니 전송 계층에 줄이 남아 있던 셈이다.
HTTP/3 — 전송 계층을 갈아끼우다
HTTP/3는 TCP를 버리고 UDP 위에 얹은 QUIC을 쓴다. QUIC은 스트림마다 순서를 따로 관리하므로 한 스트림의 유실이 다른 스트림을 막지 않는다.
덤으로 따라온 것도 있다. TLS가 QUIC 안에 통합되어 핸드셰이크가 1-RTT로 줄고, 재접속은 0-RTT까지 가능하다. 연결을 IP·포트가 아니라 커넥션 ID로 식별하므로, 와이파이에서 LTE로 바뀌어도 연결이 끊기지 않는다.
정리
세 버전은 각자 바로 아래 계층에 남아 있던 줄서기를 걷어낸 기록이다.
- HTTP/1.1 → 2 : 커넥션 안의 요청 줄서기 제거
- HTTP/2 → 3 : TCP의 바이트 순서 줄서기 제거
그래서 손실이 적은 안정된 회선에서는 HTTP/3의 이득이 크지 않고, 패킷 유실이 잦은 모바일 환경에서 차이가 두드러진다.