2-4. HTTP 버전 — 연결 관리와 전송 계층의 진화
한 줄 요약: HTTP/1.1 → 2 → 3의 각 세대가 어떤 병목을 풀었고 어떤 병목을 남겼는지 설명하고, 실서비스의 프로토콜 버전을 확인·평가하며 버전 전환의 실익을 판단할 수 있다.
학습 목표
- HOL 블로킹(head-of-line blocking)이 HTTP/1.1(응답 순서), HTTP/2(TCP 계층)에서 각각 어느 계층의 문제인지 구분해 설명할 수 있다.
- HTTP/1.1의 연결 제약이 낳은 최적화 관행(도메인 샤딩, 스프라이트, 인라이닝)이 왜 HTTP/2에서 역효과가 되는지 설명할 수 있다.
- HTTP/2의 서버 푸시와 우선순위 트리가 폐기·재설계된 이유를 말할 수 있다.
- QUIC이 UDP 위에 전송 계층을 다시 만든 이유(ossification)와, HTTP/3가 실질 이득을 주는 네트워크 조건을 판단할 수 있다.
- DevTools와
curl로 실서비스의 프로토콜 버전과 협상 과정을 확인할 수 있다.
배경: 왜 이것이 존재하는가
2-1에서 확인했듯 HTTP의 의미론은 버전과 무관하다. GET은 세 버전 모두에서 GET이고, 304는 어디서나 304다. 그런데 전송 계층은 25년 사이 두 번 전면 교체됐다. 무엇이 그렇게 문제였는가?
한 단어로 답하면 병렬성이다. 웹 페이지 하나가 리소스 수십~수백 개의 묶음이 되면서, "요청 여러 개를 어떻게 동시에 나르는가"가 전송 계층의 중심 문제가 됐다. 그리고 각 세대는 이 문제를 한 계층씩 아래로 파고들며 풀었다: HTTP/1.1은 연결을 여러 개 열었고(응용 계층 위 우회), HTTP/2는 연결 하나 안에 논리 채널을 만들었고(응용 계층 해결), HTTP/3는 TCP 자체를 갈아치웠다(전송 계층 해결). 각 단계에서 "왜 그걸로 부족했는가"를 따라가는 것이 이 문서의 서사다.
백엔드 경험과 연결하자면, 이 문제 계열 자체는 익숙할 것이다 — DB 커넥션 풀의 크기 조정, 하나의 TCP 위에 논리 채널을 얹는 멀티플렉싱(gRPC가 정확히 HTTP/2다). 다른 점은 통제 범위다. 서버 간 통신에서는 양단을 모두 내가 제어하지만, 웹에서는 상대가 수십억 개의 브라우저와, 내가 존재조차 모르는 중간 장비들이다. 이 통제 불가능성이 프로토콜 진화의 방향을 결정했다는 것이 뒤에서 드러난다.
핵심 개념
HTTP/1.1 — 연결 재사용, 그리고 순서라는 족쇄
HTTP/1.0은 요청마다 TCP 연결을 새로 맺었다. 연결 수립에 1 RTT + (2-5에서 볼) TLS까지 치면 요청보다 핸드셰이크가 비싼 구조라, HTTP/1.1은 keep-alive(연결 재사용)를 기본으로 만들었다. Phase 0-1에서 본 브라우저 연결 풀이 이것이다.
하지만 재사용되는 연결 하나는 여전히 한 번에 하나의 요청-응답만 처리한다. 요청을 보내면 응답이 끝날 때까지 그 연결은 점유된다. 앞 응답이 느리면 뒤 요청들이 전부 기다린다 — 이것이 첫 번째 HOL 블로킹(head-of-line blocking, 줄 맨 앞이 전체를 막는 현상)이고, 계층은 HTTP 응용 계층이다.
스펙은 해법으로 파이프라이닝(응답을 기다리지 않고 요청을 연달아 보내기)을 정의했지만 실패했다. 응답은 여전히 요청 순서대로 와야 해서 HOL이 해소되지 않았고, 순서 규칙을 잘못 구현한 프록시·서버가 세상에 너무 많아 응답이 뒤섞이는 사고가 났다. 결국 모든 주요 브라우저가 파이프라이닝을 비활성화했다 — "스펙에 있어도 배포된 세계가 감당 못 하면 죽는다"는, 이 문서에서 반복될 교훈의 첫 사례다.
남은 우회로는 연결을 여러 개 여는 것뿐이었다. 브라우저들은 오리진당 약 6개의 동시 연결을 열도록 수렴했다(무한정 열지 않는 이유: 연결마다 핸드셰이크·메모리 비용이 있고, TCP들이 서로 대역폭을 뺏으며 혼잡 제어를 방해한다). 병렬성의 상한이 6이 되자, 이 상한을 우회하는 최적화 관행이 한 세대를 지배했다.
| 관행 | 내용 | 실제로 우회한 제약 |
|---|---|---|
| 도메인 샤딩 | 정적 자산을 img1.example.com, img2.…로 분산 | 오리진당 6연결 → 오리진 수를 늘려 병렬성 확대 |
| 스프라이트 | 아이콘 수십 개를 이미지 한 장에 합침 | 요청 수 자체를 줄임 |
| 번들 몰아치기 / 인라이닝 | JS·CSS를 거대 파일로 합치거나 HTML에 내장 | 요청 수 자체를 줄임 |
기억할 것: 이것들은 좋은 설계여서가 아니라 프로토콜의 제약이 애플리케이션 구조를 왜곡한 결과였다. 제약이 사라지면 어떻게 되는지가 다음 절이다.
HTTP/2 — 하나의 연결, 다수의 스트림
HTTP/2(2015, 현행 RFC 9113)의 핵심 아이디어는 하나다: 연결 하나 안에 독립적인 논리 채널(스트림)을 여러 개 만든다. 메시지를 텍스트 줄이 아니라 바이너리 프레임으로 쪼개고, 각 프레임에 스트림 ID를 붙여 하나의 TCP 위에서 뒤섞어 보낸다. 수신 측이 ID별로 재조립하므로, 느린 응답이 빠른 응답을 막지 않는다 — 응용 계층 HOL의 해소이자, 요청 병렬성 상한(6)의 소멸이다.
이 구조 전환이 낳은 부속 설계들:
- HPACK 헤더 압축(RFC 7541). 헤더가 프레임화되면서 압축이 가능해졌는데, 방식이 특이하다 — 양단이 연결별 동적 테이블을 유지하고, 한 번 보낸 헤더는 다음부터 인덱스 번호로만 참조한다. 매 요청 반복되는
Cookie,User-Agent가 사실상 첫 1회만 전송되는 셈이다. 2-3에서 본 쿠키 반복 전송 비용이 크게 완화되는 지점이다. (일반 압축 알고리즘을 안 쓰고 테이블 방식을 설계한 것은 압축된 TLS 트래픽의 길이 변화로 비밀 값을 추측하는 CRIME 계열 공격 때문이다.) - 1.1 시대 관행의 역전. 도메인 샤딩은 이제 멀쩡한 멀티플렉싱 연결을 두고 핸드셰이크 비용과 별도의 HPACK 테이블, 별도의 혼잡 제어 워밍업을 지불하는 순손실이 됐다. 스프라이트·과대 번들도 "요청 수 절약" 이득은 사라지고 "아이콘 하나 바뀌면 전체 캐시 무효화"(2-2의 관점에서) 비용만 남는다. 최적화는 특정 제약에 대한 반응이므로, 제약이 바뀌면 재평가해야 한다 — 이 원칙의 가장 선명한 실례다.
한편 HTTP/2가 야심 차게 넣었다가 실패한 기능이 둘 있고, 실패의 이유가 성공보다 교훈적이다.
- 서버 푸시의 폐기. "HTML 요청이 오면 CSS도 어차피 필요할 테니 요청 전에 밀어 넣자"는 아이디어였지만, 서버는 브라우저 캐시 상태를 모른다는 근본 문제가 있었다. 이미 캐시된 CSS를 매번 푸시하면 대역폭 순낭비이고, 이를 피할 정교한 수단이 없었다. 실측 결과 이득이 미미하거나 역효과였고, Chrome이 2022년 지원을 제거했다. 같은 목적의 현행 대안은
103 Early Hints상태 코드다 — 서버가 본 응답을 준비하는 동안 "이것들이 필요할 것"이라고 힌트만 먼저 보내고, 다운로드 여부는 캐시 상태를 아는 브라우저가 결정한다. 결정권을 정보가 있는 쪽에 두는 재설계다. - 우선순위 트리의 재설계. 스트림이 뒤섞이니 "무엇을 먼저 보낼 것인가"가 새 문제로 등장했고(CSS는 렌더링을 막으니 이미지보다 급하다 — Phase 1에서 본 렌더링 모델과 연결된다), HTTP/2는 스트림 간 의존 트리라는 정교한 모델을 정의했다. 그러나 너무 복잡해서 서버·중간자 대부분이 제대로 구현하지 않았고, 사실상 무시되는 스펙이 됐다. RFC 9218(2022)이 이것을
Priority: u=3, i같은 단순한 헤더 하나(urgency 0~7 + 점진 전송 여부)로 대체했다. 정교함보다 배포 가능성 — 파이프라이닝과 같은 교훈의 반복이다.
남은 병목 — TCP 자신
HTTP/2가 응용 계층 HOL을 없앴지만, 스트림들이 결국 하나의 TCP 바이트 스트림 위를 지난다는 사실은 그대로다. TCP는 바이트가 순서대로 도착함을 보장하는 프로토콜이므로, 패킷 하나가 유실되면 재전송이 도착할 때까지 그 뒤에 이미 도착해 있는 바이트 전부를 커널이 애플리케이션에 주지 않는다. 유실된 패킷이 이미지 스트림의 것이어도, 멀쩡히 도착한 CSS 스트림의 프레임까지 함께 묶인다. HOL 블로킹이 한 계층 아래로 내려가 부활한 것이다 — HTTP/2는 TCP 위에서 스트림 독립성을 흉내 냈을 뿐, TCP는 스트림의 존재를 모른다.
이 문제는 패킷 유실률이 높을수록 커진다. 유실률이 충분히 높은 환경(부실한 Wi-Fi, 이동 중 셀룰러)에서는 연결 6개에 손실이 분산되는 HTTP/1.1이 단일 연결 HTTP/2보다 오히려 나은 역전이 실측된다. DevTools 스로틀링은 대역폭·지연만 흉내 내고 패킷 유실은 재현하지 못하므로, 이 현상은 실지표(RUM)에서나 관찰된다는 점도 함께 알아 둘 것.
HTTP/3 — TCP를 고칠 수 없어서 다시 만들었다
논리적 해법은 명백하다: TCP에 스트림 개념을 넣으면 된다. 그런데 TCP는 고칠 수 없다. 두 가지 이유다.
- TCP는 OS 커널에 구현되어 있다. 변경이 전 세계 커널 업데이트에 묶인다.
- 더 치명적으로, 인터넷의 중간 장비들(NAT, 방화벽, 로드밸런서)이 TCP 헤더를 들여다보고 해석·간섭한다. TCP에 새 옵션이나 동작을 추가하면 이 장비들이 모르는 트래픽으로 취급해 변조하거나 폐기한다. 프로토콜이 배포된 장비들에 의해 사실상 동결되는 이 현상을 경화(ossification)라 부른다. 파이프라이닝을 죽인 "배포된 세계"가, 이번에는 전송 계층 전체의 진화를 막은 것이다.
QUIC(RFC 9000)의 설계는 이 두 제약에 대한 정면 대응이다.
- UDP 위의 사용자 공간 구현. UDP는 아무것도 안 하는 프로토콜이라 중간 장비가 간섭할 것도 없다. 그 위에 신뢰성 있는 전달, 혼잡 제어, 그리고 스트림별 독립 순서 보장을 다시 구현했다 — 스트림 A의 패킷 유실이 스트림 B의 전달을 막지 않는다. TCP 계층 HOL의 해소다.
- 거의 모든 것의 암호화. TLS 1.3을 별도 계층이 아니라 QUIC 내부에 통합했고, 전송 메타데이터(패킷 번호 등)까지 암호화한다. 보안 목적을 넘어 미래의 경화 방지가 명시적 설계 목표다 — 중간 장비가 볼 수 없는 필드는 중간 장비에 의해 동결될 수도 없다.
- 연결 마이그레이션. TCP 연결은 (출발 IP, 출발 포트, 도착 IP, 도착 포트) 4-튜플이 정체성이라, Wi-Fi에서 셀룰러로 넘어가면(출발 IP 변경) 연결이 죽고 처음부터 다시다. QUIC은 연결의 정체성을 IP가 아닌 연결 ID에 두므로, 네트워크가 바뀌어도 연결이 살아서 이어진다. 이동 중 사용이 기본인 모바일 환경을 정조준한 설계다.
- 핸드셰이크 통합. 전송 수립과 암호화 협상이 한 왕복에 끝난다(재연결이면 0-RTT도 가능). 상세 회계는 2-5에서 다룬다.
HTTP/3(RFC 9114)는 이 QUIC 위에 HTTP 의미론을 매핑한 것이다. 프레이밍은 QUIC 스트림이 대신하므로 HTTP/2보다 오히려 얇고, HPACK은 스트림 간 순서 의존이 TCP HOL을 재유발하지 않도록 QPACK(RFC 9204)으로 교체됐다.
협상과 확인 — 실서비스에서 버전 읽기
브라우저가 어떤 버전으로 접속할지는 협상의 결과다. HTTP/2는 TLS 핸드셰이크 중 ALPN 확장으로 협상된다(h2 지원 여부를 인증서 교환과 함께 알림). HTTP/3는 UDP 기반이라 "일단 접속"이 안 되므로 두 경로로 발견된다: 이전 응답의 Alt-Svc: h3=":443" 헤더(첫 방문은 TCP로 가고 다음부터 H3 — 첫 방문이 손해), 또는 DNS의 HTTPS 레코드(RFC 9460)에 실린 ALPN 정보(DNS 조회 시점에 알게 되어 첫 연결부터 H3 가능).
# 서버의 H3 광고 확인
curl -sI https://www.cloudflare.com/ | grep -i alt-svc
# alt-svc: h3=":443"; ma=86400 ← "다음엔 UDP 443의 HTTP/3로 와도 된다"
# DNS HTTPS 레코드 확인 (첫 연결부터 H3로 가는 경로)
dig +short https www.cloudflare.com
# 1 . alpn="h3,h2" ...
# 버전 강제 지정으로 각각 시도 (curl이 HTTP/3 지원 빌드일 때 --http3 사용 가능)
curl -sI --http2 https://www.cloudflare.com/ -o /dev/null -w '%{http_version}\n'
# 2DevTools에서는 Network 패널에서 열 머리글 우클릭 → Protocol 열 추가. h3/h2/http/1.1이 리소스별로 표시된다 — 같은 페이지 안에서도 오리진마다 버전이 다른 것이 정상이다. 실습 과제(과제 B)의 첫 분석 항목이 바로 이 분포다.
실무 관점
"HTTP/2로 올렸는데 안 빨라진다"
버전 업그레이드의 이득은 병목이 전송 계층에 있었을 때만 나타난다. 흔한 착시 케이스:
- 병목이 TTFB(서버 처리 시간)나 렌더 블로킹 JS라면, 요청 병렬성이 아무리 늘어도 총 시간은 그대로다. Phase 0-1의 타이밍 분해로 병목 위치부터 확인하는 것이 순서다.
- 오리진이 여러 개로 흩어져 있으면(자산 CDN, 서드파티 스크립트 다수) 멀티플렉싱은 오리진별로만 성립한다. 연결 수를 줄이는 통합(샤딩 해제, 자산의 자기 오리진 서빙)이 선행돼야 H2의 구조적 이득이 산다.
- 반대로 리소스 수십 개가 같은 오리진에서 6개씩 줄 서던 사이트(Waterfall에서 Stalled 구간이 계단처럼 보이는 경우 — Network 패널 Timing 탭에서 확인)는 극적으로 개선된다.
버전 선택·전환의 판단 기준
| 상황 | 판단 |
|---|---|
| 일반 공개 웹 서비스 | H2는 기본값. CDN을 쓰면 대부분 엣지에서 자동 활성화되어 있고, 확인만 하면 된다 |
| 모바일 사용자 비중 높음, 이동 중 사용 | H3의 이득이 가장 큰 지형(유실 내성 + 연결 마이그레이션 + 빠른 핸드셰이크). CDN의 H3 옵션 활성화 검토 |
| 사내망·서버 간 통신 | 유실률이 낮고 연결이 오래 살아서 H3 이득이 작다. UDP를 막는 방화벽·장비가 있으면 H3는 어차피 TCP 폴백 — H2로 충분한 경우가 많다 |
| 아직 H1.1 | 전환 이득이 가장 큰 상태. 단, 전환 시 1.1용 최적화(샤딩 등)의 해제까지가 한 세트다 |
H3는 UDP 443이 막힌 네트워크(일부 기업망)에서 동작하지 않으므로 항상 TCP 폴백이 전제다. 이 폴백 존재 덕분에 H3 활성화는 위험이 낮은 결정이기도 하다 — 안 되면 H2로 내려갈 뿐이다.
1.1 습관 재점검 체크리스트
프로토콜 제약이 바뀌었는데 관행만 남은 지점들. H2 이상으로 서빙 중이라면 점검한다.
- 도메인 샤딩이 남아 있는가 → 오리진 통합 (연결·핸드셰이크·HPACK 테이블이 하나로)
- "요청 수를 줄여야 한다"는 이유만으로 유지되는 스프라이트/인라이닝이 있는가 → 개별 파일 + 캐시 정밀화가 유리한 경우가 많다
- 단, 번들링 자체는 여전히 유효하다 — 모듈 수천 개를 개별 전송하면 요청당 고정 비용(우선순위 처리, 압축 컨텍스트)과 압축률 손실이 여전히 크다. "번들 무용론"이 아니라 "6연결 우회용 몰아치기의 종료"로 읽어야 한다. 적정 분할 단위는 Phase 6-2(번들러)에서 다룬다.
더 깊이
HPACK → QPACK — 압축이 HOL을 되살리지 않게
HPACK의 동적 테이블은 "이 헤더는 아까 보낸 그 값"이라는 참조로 동작하므로, 참조를 해석하려면 테이블 갱신이 순서대로 적용되어야 한다. TCP에서는 순서가 공짜였지만, QUIC 스트림들은 독립적으로 도착한다 — HPACK을 그대로 얹으면 테이블 갱신을 기다리느라 스트림 간 대기가 생겨, 애써 없앤 HOL이 압축 계층에서 부활한다. QPACK은 테이블 갱신을 전용 단방향 스트림으로 분리하고, 각 헤더 블록이 자신이 요구하는 테이블 상태를 명시해 "기다릴지(압축률 우선) 참조 없이 보낼지(지연 우선)"를 인코더가 선택할 수 있게 했다. 압축률과 HOL 회피 사이의 트레이드오프가 프로토콜 파라미터로 노출된 설계다.
RFC 9218 — 우선순위의 재설계가 보여주는 것
새 우선순위 스킴은 Priority 요청 헤더(구조화 필드 — 2-1에서 언급한 신문법의 실사용 예)로 urgency(0~7)와 incremental(부분 수신도 유용한가 — 점진 렌더링되는 이미지 등) 두 값만 전달한다. HTTP/2의 의존 트리가 표현력에서 압도적이었음에도 폐기된 것은, 프로토콜 기능의 가치가 표현력이 아니라 양단이 실제로 구현하는가로 결정되기 때문이다. 브라우저의 리소스별 우선순위 배정(왜 CSS가 high이고 이미지가 low인가)과 fetchpriority 속성으로 이를 조정하는 방법은 Phase 7-2에서 다룬다.
경화의 증거 — QUIC의 고정 비트까지
경화가 얼마나 심각하게 취급되는지 보여주는 세부: QUIC 설계 중, 패킷에서 암호화되지 않은 극소수 비트마저 중간 장비가 "QUIC 식별용"으로 학습해 버릴 것을 우려해, 스펙은 의도적으로 버전마다 해석이 달라질 수 있는 구조를 만들고 RFC 8999(Version-Independent Properties)로 "중간 장비가 의존해도 되는 것"의 목록을 명시적으로 못 박았다. 나아가 실험적 버전 번호를 주기적으로 회전시키는 관행(greasing)도 있다 — 프로토콜의 진화 가능성 자체를 지키기 위한 방어 설계다. TLS에도 같은 목적의 GREASE(RFC 8701)가 있다.
정리
- HTTP의 세 세대는 같은 의미론을 두고 병렬성 병목을 한 계층씩 내려가며 푼 역사다: 1.1은 연결 6개로 우회, 2는 연결 안의 스트림으로 응용 계층 HOL 해소, 3는 TCP를 QUIC으로 교체해 전송 계층 HOL까지 해소.
- 도메인 샤딩·스프라이트류는 1.1의 제약에 대한 반응이었고 H2에서 역효과다. 최적화는 제약의 함수이므로 제약이 바뀌면 재평가한다 — 단 번들링 자체는 여전히 유효하다.
- 서버 푸시(캐시 상태를 모르는 쪽이 결정)와 H2 우선순위 트리(정교하나 아무도 구현 안 함)의 실패는 각각 "결정권은 정보 있는 쪽에", "표현력보다 배포 가능성"이라는 프로토콜 설계 교훈을 남겼고, 103 Early Hints와 RFC 9218이 그 교훈의 산물이다.
- QUIC이 UDP 위에 구현된 것은 성능 때문이 아니라 TCP가 커널과 중간 장비에 의해 경화되어 고칠 수 없었기 때문이다. 전면 암호화는 미래의 경화를 막는 장치이기도 하다.
- H3의 실익은 네트워크 조건의 함수다 — 고유실·모바일에서 크고, 안정된 유선·사내망에서 작다. 판단의 시작은 DevTools Protocol 열과
Alt-Svc로 현재 상태를 읽는 것이다.
확인 문제
1. 2015년경 성능 최적화를 거친 레거시 서비스가 있다. 정적 자산이 static1~4.example.com 네 도메인에 분산되어 있고, 아이콘은 스프라이트, CSS는 전부 하나로 합쳐져 있다. 이 서비스를 그대로 HTTP/2로 전환하면 각 요소는 어떻게 되는가? 전환 후 무엇을 바꿔야 하는가?
정답과 해설
도메인 샤딩: 순수 악화 요소가 된다. H2의 멀티플렉싱은 오리진(연결)별로 성립하므로, 4개 도메인은 연결 4개 = TLS 핸드셰이크 4회 + 각자의 혼잡 제어 워밍업 + 각자의 HPACK 동적 테이블(압축 이득 분산)을 지불하면서, 정작 병렬성은 연결 하나로도 충분했다. 도메인을 통합해 단일 연결로 모아야 H2의 구조가 산다. 스프라이트·통합 CSS: 요청 수 절약이라는 원래 이득은 소멸하고, "일부만 바뀌어도 전체 캐시 무효화"라는 비용만 남는다. 다만 즉시 해체가 의무는 아니고, 캐시 무효화 빈도와 재빌드 비용을 보고 점진 전환하면 된다 — 극단적으로 파일 수천 개로 쪼개는 것도 요청당 고정 비용 때문에 손해다. 핵심 원리: 이 관행들은 "오리진당 6연결"이라는 제약의 함수였고, 제약이 사라진 환경에서는 근거를 잃는다.
2. 모바일 사용자의 RUM 데이터에서, 페이지 로드가 유독 느린 세션들이 지하철 이용 시간대에 몰려 있다. 서비스는 HTTP/2다. 이 패턴을 전송 계층 관점에서 설명하고, HTTP/3 전환이 도울 수 있는 근거 두 가지를 대라. DevTools 스로틀링으로 이 문제를 재현할 수 있는가?
정답과 해설
지하철 환경은 패킷 유실률이 높고 네트워크 전환(셀룰러 기지국 간, Wi-Fi↔셀룰러)이 잦다. H2는 단일 TCP 연결이므로 ① 패킷 하나만 유실돼도 TCP의 순서 보장 때문에 이미 도착한 다른 스트림의 데이터까지 커널에 묶인다(TCP 계층 HOL — 유실률이 높을수록 연결 전체가 자주 멈춘다). ② 네트워크 전환으로 IP가 바뀌면 TCP 연결의 정체성(4-튜플)이 깨져 연결이 죽고, TCP+TLS 핸드셰이크부터 다시다. H3가 돕는 근거: ① QUIC은 스트림별 독립 전달이라 유실이 해당 스트림만 지연시킨다. ② 연결 ID 기반이라 IP가 바뀌어도 연결이 유지된다(연결 마이그레이션). 재현 여부: DevTools 스로틀링은 대역폭·지연을 제한할 뿐 패킷 유실을 재현하지 못하므로 이 문제는 실험실에서 안 보인다 — RUM 같은 실지표가 필요한 이유이고, 성능 주장에 계측 방법을 붙이라는 원칙이 적용되는 지점이다.
3. HTTP/2 서버 푸시는 "필요할 리소스를 요청 전에 보내 준다"는, 직관적으로 명백해 보이는 이득을 약속했는데 폐기됐다. 반면 같은 목적의 103 Early Hints는 살아남아 확산 중이다. 두 메커니즘의 결정 구조 차이를 설명하고, 이 차이가 왜 성패를 갈랐는지 말하라.
정답과 해설
서버 푸시는 서버가 "이 리소스를 받아라"를 결정하고 데이터 전송까지 실행한다. 그런데 그 결정에 필요한 핵심 정보 — 브라우저 캐시에 이미 있는가 — 를 서버는 갖고 있지 않다. 재방문 사용자에게 이미 캐시된 CSS를 매번 푸시하면 대역폭 순낭비이고, 이를 막을 실용적 수단(캐시 다이제스트 표준화 시도는 실패)이 없었다. 103 Early Hints는 결정을 분리한다: 서버는 "아마 필요할 것"이라는 정보만 조기에 보내고(Link: rel=preload 헤더), 다운로드 실행 여부는 캐시 상태를 아는 브라우저가 결정한다. 캐시에 있으면 무시하면 그만이라 낭비의 하한이 0이다. 교훈: 최적화 결정권은 그 결정에 필요한 정보를 가진 쪽에 둬야 한다. 정보 없는 쪽의 적극적 실행(푸시)은 잘 맞으면 이득이지만 틀리면 순손실이고, 틀리는 빈도(재방문·캐시 적중)가 실웹에서는 압도적으로 높았다.
참고 자료
- RFC 9113 — HTTP/2 — 현행 H2 스펙. §5.3에서 구 우선순위 스킴을 공식적으로 deprecated 처리한 대목이 흥미롭다.
- RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport — QUIC 전송 스펙. 스트림·연결 마이그레이션의 정의.
- RFC 9114 — HTTP/3 / RFC 9204 — QPACK — H3 매핑과 압축 재설계.
- RFC 9218 — Extensible Prioritization Scheme for HTTP — 우선순위 재설계. 짧고 읽기 쉬워 "실패에서 배운 스펙"의 표본으로 좋다.
- web.dev — Content delivery best practices — H2/H3 시대의 자산 전달 실무 정리.
- Chrome for Developers — Removing HTTP/2 Server Push — 푸시 제거 결정의 실측 근거.
- Cloudflare Blog — HTTP/3: the past, the present, and the future — 경화와 QUIC 설계 배경을 배포 사업자 관점에서 해설.