줄 맨 앞의 것이 막혀서 뒤에 선 것들이 함께 지연되는 현상 (Head-of-Line Blocking). 계산대 한 줄에서 앞사람이 오래 걸리는 상황과 같다.
두 층에서 각각 일어난다 — 이 구분이 핵심
① 애플리케이션 계층 — HTTP/1.1
한 연결에서 요청을 하나씩 순서대로 처리한다. 앞 응답이 느리면 뒤 요청이 대기한다.
연결 1: [요청A ────── 3초 ──────][요청B]
↑ B는 A 때문에 3초를 기다린다
브라우저가 도메인당 연결을 6개씩 여는 것이 이 때문이었다.
→ HTTP/2의 멀티플렉싱이 해결했다. 한 연결에 여러 스트림을 만들고 데이터를 잘게 쪼개(프레임) 섞어 보낸다. A가 느려도 B의 프레임이 먼저 갈 수 있다.
② 전송 계층 — TCP
그런데 HTTP/2로도 안 풀리는 게 남는다. TCP는 순서를 보장하기 때문이다.
TCP가 받은 것: [스트림A-1] [유실] [스트림B-1] [스트림B-2]
↑
A의 패킷 하나가 빠지면, 뒤에 온 B의 데이터까지
커널 버퍼에서 대기한다 — 애플리케이션에 올려 주지 않는다
TCP는 "몇 번째 바이트"만 알지 그게 어느 스트림인지 모른다. 순서대로만 올려 주므로 관계없는 B가 A를 기다린다.
→ HTTP/3이 UDP 기반 QUIC으로 옮겨 해결했다. QUIC은 스트림을 알고 있어 A의 손실이 B를 막지 않는다.
정리
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| 앱 계층 HOL | 있음 | 해결 | 해결 |
| 전송 계층 HOL | 있음 | 있음 | 해결 |
| 기반 | TCP | TCP | QUIC(UDP) |
면접 함정
"HTTP/2가 HOL Blocking을 완전히 해결했다" → 틀렸다. 앱 계층만 풀었고 TCP 계층은 그대로다. 오히려 연결을 하나로 합쳤기 때문에, 손실이 발생하면 모든 스트림이 함께 멈춰 HTTP/1.1의 다중 연결보다 나빠질 수도 있다. 이 역설까지 짚으면 좋은 답변이다.
다른 곳에도 있다
큐에서 앞 메시지 처리가 실패해 계속 재시도하면 뒤 메시지가 전부 막힌다 (poison message). DLQ가 필요한 이유가 같은 구조다.