HTTP/2 서버 푸시는 죽었다. Early Hints는 덜 함으로써 이겼다

서버 푸시는 HTTP/2의 간판 기능이었다. 브라우저가 묻기 전에 파일을 보낸다. Chrome은 2022년 이걸 뜯어냈다 — 사용률 0.7%, 게다가 자주 더 느려서. 그 자리를 대신한 것은 정반대로 동작했고, 그래서 이겼다.

2015년 HTTP/2가 나왔을 때, 서버 푸시는 다들 슬라이드에 올리던 기능이었다. 홍보 문구가 거부하기 힘들었다. 브라우저에 HTML 페이지를 보내고, 그게 파싱되고, style.css가 필요하다는 걸 알아내고, 두 번째 왕복으로 그걸 요청하기를 기다리는 대신, 서버가 그냥 HTML 옆에 style.css를 파이프로 밀어 넣으면(push) 된다. 브라우저가 그걸 원할 걸 이미 아는데, 왜 묻게 하나? 왕복을 건너뛰어라. 공짜 속도.

7년 뒤, Chrome 106 — 2022년 9월 1일 베타 — 에서 Google은 서버 푸시를 꺼내 버렸다. 폐기하되 지원, 이 아니라. 제거. Chrome은 푸시된 스트림을 아예 받지 않게 됐다. Edge도 같은 릴리스에서 따라왔고, Firefox는 2024년 10월 Firefox 132에서 뺐다. HTTP/2의 헤드라인이 되기로 했던 기능은, 이제 어떤 주요 브라우저도 받지 않는 것이 됐다.

무슨 일이 있었는지는 프로토콜 설계의 꽤 좋은 교훈이고, 그 교훈은 “서버 푸시는 멍청한 아이디어였다”가 아니다. 아니었다. 요점은 그 자리를 대신한 것이 덜 함으로써 이겼다는 것이고, 왜 그런지 이해하는 게 어떤 성능 체크리스트보다 값지다.

그것을 죽인 숫자

이유에 앞서, 숫자부터. Chrome 팀이 서버 푸시가 실제로 얼마나 쓰이는지 들여다봤을 때, **HTTP/2 사이트의 1.25%**에서 발견했다 — 그리고 나중에 분석을 다시 돌리자 그게 0.7%로 떨어졌다. 간판 기능을 낸 지 7년이 지났는데 99% 넘는 사이트가 쓰기를 거부했고, 쓰던 소수마저 떠나고 있었다.

그것만으로도 유지보수를 접을 이유다. 하지만 제거를 어깨 으쓱이 아니라 승리로 만든 건 성능 데이터였다. Chrome 자체의 푸시 분석은, 그들 표현으로, 결과가 뒤죽박죽 — 뚜렷한 순 성능 이득 없음, 그리고 많은 경우 성능 퇴행 — 이었다. 페이지를 빠르게 하도록 설계된 기능이, 실전에서 자주 페이지를 더 느리게 하고 있었다. 속도 기능이 믿을 만하게 빠르지도 않고 거의 아무도 안 쓰면, 유지하는 건 전부 비용이다.

그럼 왜 뻔한 최적화 — 요청되기 전에 파일을 보낸다 — 가 실전에서 어긋났을까?

서버가 볼 수 없는 캐시

여기 결함이 있고, 이건 누가 패치할 버그가 아니라 구조적이다.

푸시의 전제 전체는 서버가 브라우저에게 필요한 걸 정하고 청하지 않은 채 보낸다는 것이다. 그런데 서버에겐 그 판단을 올바로 하게 해줄 단 하나의 정보가 없다: 브라우저 캐시에 이미 뭐가 있는가. 재방문에서 브라우저는 이미 style.css를 갖고 있다. 필요 없다. 하지만 눈먼 채 푸시하는 서버는 알 길이 없어서 — 파일을 어쨌든 밀어 넣고, 브라우저는 그걸 한 번 보고 버린다. 대역폭을 아끼려던 최적화가 대역폭을 쓴다. 하필 당신이 가장 빠르게 해주고 싶던 재방문자에게.

게다가 “멈춰”라고 할 깔끔한 방법도 없어서 더 나쁘다. 브라우저가 이미 가진 걸 밀어 넣고 있다고 깨달을 즈음, 서버는 이미 바이트를 보내기 시작한 경우가 많다. HTTP/2는 RST_STREAM으로 스트림을 취소할 수 있지만, 그건 전송이 진행된 뒤의 취소다 — 바이트는 이미 회선에 있고, 대역폭은 이미 쓰였다. 푸시를 리셋할 순 있어도, 이미 보낸 걸 안 보낸 걸로 만들 순 없다. 푸시는 서버를 추측에 묶어 놓고, 그 추측을 물릴 좋은 방법을 아무에게도 주지 않았다.

타이밍 맞추기도 어려웠다. 왕복을 실제로 아끼려면 서버 생각-시간(think-time) 중에 — 백엔드가 HTML을 만드는, 응답이 나가기 전 그 창에 — 푸시해야 했다. 너무 늦게 푸시하면 브라우저가 이미 스스로 그 파일을 요청했고, 엉뚱한 걸 푸시하면 대역폭을 두고 다투는 에셋 뒤로 HTML을 밀어낸 셈이다. nginx, Apache, CDN마다 여기서 각자의 날카로운 모서리가 있었다. 서버 푸시는 켜기는 쉬운데 아무것도 안 하는 것보다 빠르게 만들기는 진짜 어려운 기능으로 드러났다.

Early Hints는 이 전체를 뒤집는다

Chrome이 모두에게 가리킨 대체재는 103 Early Hints다. 2017년 RFC 8297로 표준화됐고, 2022년 6월 Chrome 103에서 실렸다 — 공교롭게도 푸시를 제거한 버전보다 몇 버전 앞. 우아한 부분은, 푸시가 노리던 바로 그 기회 — 서버 생각-시간 — 을 써서 거의 정반대를 한다는 것이다.

백엔드가 아직 진짜 응답을 만드는 동안, 서버는 몇 개의 Link 헤더가 붙은 중간 103 Early Hints 상태를 보낸다:

103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </app.js>; rel=preload; as=script

그다음, 나중에, HTML이 담긴 진짜 200이 따른다. 그게 전부다. 103은 응답이 아니라 — 귀띔이다. style.css를 보내지 않는다. style.css라는 이름과 제안을 보낸다: “아마 이게 필요할 거야. 원하면 가져오기 시작해.”

무엇이 옮겨졌는지 보라. 푸시는 서버가 정하고 보냈다. Early Hints는 서버가 제안하고, 브라우저가 정하고 가져온다. 그리고 브라우저야말로 자기 캐시에 뭐가 있는지 실제로 아는 유일한 당사자다. style.css를 이미 갖고 있으면 힌트를 무시한다 — 버려지는 바이트 없음. 없으면 푸시가 노리던 그 생각-시간 창에 요청을 시작한다 — 그래서 왕복은 여전히 건너뛴다. 푸시의 이점 — 서버 유휴 시간으로 에셋을 데워두기 — 을 그 치명적 결함 없이 얻는다. 판단이 이제 그걸 내릴 정보를 가진 당사자에게 있으니까.

한 줄로 요약하면 이거다: 푸시는 볼 수 없는 캐시를 대신해 추측했다. Early Hints는 그 추측을 볼 수 있는 쪽에 넘긴다. 브라우저 캐시와 싸우는 최적화는 브라우저 캐시에 진다. 캐시와 협력하는 최적화는 이긴다.

기능보다 오래 남는 교훈

이걸 “잘 안 된 또 하나의 웹 기능”으로 분류하고 넘어가기는 쉽다. 하지만 여기엔 계속 튀어나오는 설계 원칙이 있고, 이름 붙일 값어치가 있다: 판단을 정보가 있는 곳에 두어라.

서버 푸시가 실패한 건 어떤 판단 — 이 리소스가 필요한가? — 을, 구조적으로 답을 알 수 없게 보장된 바로 그 자리로 옮겼기 때문이다. 서버는 요청은 볼 수 있어도 캐시는 못 본다. 아무리 튜닝해도 엉뚱한 위치에서 내린 판단은 못 고친다. Early Hints는 더 영리하거나 빨라서 성공한 게 아니다. 같은 판단을 한 홉 옮겨, 답이 실제로 사는 브라우저로 보내고, 비켜섬으로써 성공했다.

한번 눈에 들어오면 이 모양이 인프라 도처에 보인다. 통보받는 대신 무효화를 추측하는 캐시 계층. 낡은 헬스 데이터로 라우팅하는 로드밸런서. 첫 시도가 실제로 도착했는지 모른 채 재시도를 정하는 재시도 로직. 하나같이 봐야 할 걸 볼 수 없는 컴포넌트가 내린 판단이고, 하나같이 푸시와 똑같이 조용히 비싼 방식으로 실패한다 — 에러가 아니라, 잘 되는 것처럼 보이는 낭비로.

서버 푸시는 돌아오지 않는다. 돌아올 게 없다. 하지만 그것이 사라진 이유가 쓸모 있는 부분이다. 한 컴포넌트가 다른 컴포넌트를 대신해 뭔가를 정하려 할 때, 지루한 질문부터 하라: 정하는 그 컴포넌트가 잘 정할 정보를 실제로 갖고 있는가? 없다면 당신은 최적화를 만든 게 아니다. 추측을 만든 것이고, 하필 빠르게 하려던 바로 그 트래픽에서 대가를 치른다.

토론 참여

← 블로그로 돌아가기