업데이트됨 1개월 전
HTTP/1.1은 브라우저가 여섯 명의 서로 다른 클라이언트인 척 연기하게 만들었습니다. 같은 서버에 여섯 개의 별도 연결을 열어야 겨우 합리적인 속도로 페이지를 로드할 수 있었습니다. HTTP/2 멀티플렉싱은 마침내 브라우저가 실제로 하는 일에 솔직해질 수 있게 해주었습니다: 하나의 연결, 여러 개의 동시 요청, 줄 서서 기다리는 일 없이.
HTTP/2가 바로잡은 불합리함
HTTP/1.1은 요청을 순차적으로 처리합니다. 브라우저가 리소스를 요청하면, 해당 연결에서 다음 요청을 보내기 전에 완전한 응답이 돌아올 때까지 기다려야 합니다. 이것이 바로 HOL(Head-of-Line) 블로킹입니다 — 느린 응답 하나가 그 뒤에서 기다리는 모든 것을 막아버립니다.
브라우저는 각 도메인에 여러 개의 연결을 열어 이를 우회했습니다. 보통은 여섯 개였습니다. 힘으로 밀어붙이는 방식으로 병렬 처리를 만들어낸 셈인데, 그 대가가 작지 않았습니다. TCP 연결 하나하나마다 별도의 핸드셰이크가 필요해서 시간과 리소스를 낭비했습니다. 각 연결은 독립적으로 혼잡 제어를 운영해서 대역폭을 최적으로 쓰지 못했습니다. TCP 슬로 스타트 탓에 연결마다 처음에는 느리게 시작해서 시간이 지나야 전속력을 냈습니다.
수십, 수백 개의 리소스를 가진 웹 페이지는 고통받았습니다. 여섯 개의 연결로도 리소스들은 자리가 날 때까지 줄을 서야 했습니다. 개발자들은 점점 더 황당한 꼼수에 기댈 수밖에 없었습니다:
도메인 샤딩은 리소스를 여러 가짜 도메인에 흩뿌렸습니다 — images.example.com, scripts.example.com, styles.example.com — 브라우저를 속여서 더 많은 연결을 열게 하기 위해서였습니다. 프로토콜이 너무 망가져 있으니, 그 프로토콜을 속이는 게 오히려 정석이 되어버렸습니다.
파일 번들링은 작은 파일 여러 개를 큰 파일 하나로 합쳐서 요청 수를 줄였는데, 캐시 효율성과 유연성을 희생시켰습니다. CSS 한 줄 고쳐야 한다고요? 번들 전체가 무효화됩니다.
우아한 해결책이랄 게 없었습니다. 웹이 자기 프로토콜과 싸우는 모습이었습니다.
멀티플렉싱이 실제로 작동하는 방식
HTTP/2는 연결을 한 번에 하나씩만 운반하는 파이프가 아니라, 여러 가지를 동시에 운반하는 채널로 취급합니다.
핵심 추상화는 스트림입니다. 요청-응답 쌍마다 번호로 식별되는 스트림을 하나씩 받습니다. 여러 스트림이 하나의 연결을 공유합니다. 통신은 프레임 — 스트림 ID가 태그된 작은 데이터 조각 — 을 통해 이루어집니다.
브라우저가 열 개의 리소스가 필요하면 열 개의 요청을 전부 즉시 보냅니다. 서버는 이것들을 동시에 처리하고 준비되는 대로 응답 프레임을 보냅니다. 프레임들은 교차해서 흐릅니다 — 스트림 1에서 청크 하나, 그다음 스트림 3, 다시 스트림 1, 그다음 스트림 7. 브라우저는 각 스트림을 해당 프레임들로 재조립합니다.
애플리케이션 입장에서는 여전히 전통적인 요청-응답 쌍처럼 보입니다. 하지만 그 안에서는 모든 것이 하나의 멀티플렉싱된 연결을 통해 흐릅니다.
실제로 얻게 되는 것
더 이상 줄 서서 기다릴 필요 없음. 리소스들이 서로의 뒤에 늘어설 필요가 없습니다. 모든 요청이 즉시 나가고, 응답은 서버가 만들어낼 수 있는 만큼 빠르게 도착합니다.
연결 하나가 여섯 개의 역할을 합니다. 핸드셰이크는 줄어들고 메모리도 적게 쓰며, 양쪽 모두 CPU 부담이 줄어듭니다. 서버는 같은 브라우저에서 온 수백 개의 연결을 처리할 필요가 없어집니다.
대역폭을 더 잘 활용합니다. 모든 스트림이 하나의 TCP 혼잡 윈도우를 공유합니다. 슬로 스타트에서 각각 속도를 올리는 여섯 개의 연결 대신, 연결 하나가 최적의 처리량을 꾸준히 유지합니다.
꼼수들이 필요 없어집니다. 도메인 샤딩은 HTTP/2에서 오히려 역효과를 냅니다 — 연결 재사용을 막습니다. 번들링은 장점은 사라지고 단점만 남습니다. 이제 파일이 작고 많아도 아무 문제 없습니다.
우선순위 지정이 가능해집니다. 브라우저가 서버에게 어떤 리소스가 가장 중요한지 알릴 수 있습니다. 분석 스크립트보다 HTML과 CSS 먼저. 있으면 좋은 것들보다 핵심 콘텐츠 먼저.
아래에 깔린 바이너리 계층
HTTP/1.1은 텍스트 기반이었습니다. 눈으로 직접 읽을 수 있었습니다:
HTTP/2는 바이너리입니다. 모든 메시지는 정밀한 구조를 가진 프레임입니다: 길이, 타입, 플래그, 스트림 ID를 담은 9바이트 헤더 뒤에 페이로드가 옵니다.
주요 프레임 타입:
- DATA: 요청 또는 응답 본문을 전달
- HEADERS: HTTP 헤더를 전달 (압축됨)
- SETTINGS: 설정 정보를 전달
- RST_STREAM: 스트림을 중간에 종료
- PING: 왕복 시간 측정
바이너리 프로토콜은 텍스트 프로토콜보다 파싱이 빠르고 모호성이 적습니다. 단점이라면 원시 바이트를 눈으로 보며 디버깅할 수 없다는 것입니다. 하지만 브라우저 개발자 도구와 Wireshark는 HTTP/2를 잘 처리하므로 실제로는 거의 문제가 되지 않습니다.
혼란을 막는 흐름 제어
하나의 연결로 여러 스트림을 멀티플렉싱하면 새로운 문제가 생깁니다: 빠른 스트림 하나가 나머지를 굶기지 않도록 막으려면 어떻게 해야 할까요?
HTTP/2는 두 단계에서 흐름 제어를 구현합니다. 연결 수준 흐름 제어는 전체 처리량을 관리합니다. 스트림 수준 흐름 제어는 각 스트림을 독립적으로 관리합니다.
수신자는 윈도우 크기를 알립니다 — 얼마만큼의 데이터를 받아들일 수 있는지. 송신자는 이 윈도우 안에서만 데이터를 보내야 합니다. 수신자가 데이터를 처리하고 나면 윈도우 업데이트를 보내 더 많이 보낼 수 있도록 합니다.
이 방식 덕분에 빠른 서버가 느린 클라이언트를 압도하는 것을 막고, 스트림 간에 공정하게 대역폭을 나누며, 수신자가 어떤 스트림에 대역폭을 우선 할당할지 선택할 수 있습니다.
스트림 우선순위 지정 (이론상으로)
HTTP/2는 클라이언트가 스트림에 우선순위를 설정할 수 있게 합니다. 스트림이 다른 스트림에 의존하도록 설정할 수도 있습니다 — CSS가 HTML에 의존한다고 표시하면 HTML이 올 때까지 기다립니다. 스트림에는 상대적 중요도를 나타내는 가중치도 부여할 수 있습니다.
실제로는 충분히 활용되지 못했습니다. 브라우저마다 구현 방식이 달랐습니다. 많은 서버는 우선순위를 완전히 무시했습니다. 메커니즘 자체가 잘 활용하기에 너무 복잡했습니다. HTTP/3는 이를 더 단순한 우선순위 방식으로 교체했습니다.
서버 푸시 (가지 않은 길)
HTTP/2는 서버 푸시를 도입했습니다: 브라우저가 요청하기 전에 서버가 먼저 리소스를 보낼 수 있습니다. HTML을 전송할 때 서버가 브라우저에 필요한 CSS와 JavaScript를 미리 푸시하면 왕복 시간을 한 번 줄일 수 있습니다.
하지만 채택은 제한적이었습니다. 핵심 문제는 간단합니다: 서버는 브라우저의 캐시에 무엇이 들어 있는지 알 수 없습니다. 이미 캐시된 리소스를 푸시하면 대역폭만 낭비됩니다. 캐시 다이제스트 같은 해결책이 제안되었지만 실제로 자리를 잡지 못했습니다. 대부분의 사이트는 이제 프리로드 힌트를 사용합니다 — 리소스를 직접 푸시하지 않고 브라우저에게 중요한 리소스의 존재를 미리 알려주는 방식입니다.
연결 병합
HTTP/2는 도메인들이 동일한 IP 주소로 해석되고 모든 도메인을 아우르는 유효한 인증서를 제시하면 여러 도메인에 하나의 연결을 재사용할 수 있습니다.
example.com과 cdn.example.com이 모두 와일드카드 인증서를 가진 동일한 서버를 가리킨다면, 브라우저는 두 도메인 모두에 하나의 연결을 쓸 수도 있습니다. 오버헤드가 한층 더 줄어듭니다.
이를 위해서는 인증서 설정을 신중하게 해야 하고 IP 주소도 동일해야 합니다. 일부 브라우저는 병합 여부를 판단하는 데 보수적인 편입니다.
HTTP/3가 해결하는 한계
HTTP/2는 HTTP 수준의 HOL 블로킹을 없앴습니다. 하지만 TCP 수준의 블로킹은 여전히 남아 있습니다.
TCP는 순서 있는 전달을 보장합니다. 패킷이 손실되면 TCP는 이후 패킷들을 전달하기 전에 재전송을 기다립니다 — 그 패킷들이 서로 무관한 다른 HTTP/2 스트림에 속해 있더라도 마찬가지입니다.
패킷 하나의 손실이 모든 스트림을 멈춥니다. 한 형태의 HOL 블로킹을 없앤 멀티플렉싱이 전송 계층에서 또 다른 HOL 블로킹에 부딪힙니다.
HTTP/3는 TCP를 QUIC으로 대체하여 이 문제를 해결합니다. QUIC은 전송 수준에서 스트림을 독립적으로 처리합니다. 패킷 하나가 손실되어도 그 스트림에만 영향을 미칩니다.
HTTP/1.1에서 마이그레이션하기
꼼수들을 되돌리세요. 도메인 샤딩은 연결 재사용을 막아 HTTP/2에 오히려 해롭습니다. 파일 번들링은 세분화된 캐싱을 방해합니다. 이제 파일이 작고 많아도 괜찮습니다.
인프라를 확인하세요. 일부 프록시, 방화벽, 기업 네트워크는 HTTP/2를 방해합니다. HTTP/1.1로의 자연스러운 폴백이 보장되는지 확인하세요.
결과를 측정하세요. 모든 사이트가 동일하게 이득을 보지는 않습니다. 지연 시간이 높은 연결에서 효과가 가장 두드러집니다 — 지연 시간 30~50% 감소가 일반적입니다. 지연 시간이 낮은 로컬 연결에서는 개선이 적습니다.
HTTP/2는 HTTPS가 필요합니다
HTTP/2는 HTTP와 HTTPS 모두를 위해 설계되었지만, 브라우저는 HTTPS에서만 구현합니다. 이것은 의도적인 결정이었습니다 — HTTPS 보급을 앞당기기 위해서였습니다.
TLS 핸드셰이크 중에 클라이언트와 서버는 ALPN(Application-Layer Protocol Negotiation)을 사용하여 양쪽 모두 지원할 경우 HTTP/2를 사용하기로 협의합니다.
HTTP/2 멀티플렉싱에 관해 자주 묻는 질문
왜 브라우저는 도메인당 여섯 개의 연결로 제한했나요?
HTTP/1.1 사양은 리소스 사용을 배려해 클라이언트가 서버당 연결을 두 개 이상 열지 말도록 권고했습니다. 브라우저는 결국 병렬 처리와 서버 부하 사이의 현실적인 타협점으로 여섯 개에 안착했습니다. HTTP/2는 이를 의미 없는 문제로 만들어버렸습니다 — 연결 하나가 모든 것을 처리하니까요.
HTTP/2가 자동으로 사이트를 빠르게 해주나요?
보통은 그렇지만, 언제나 그런 건 아닙니다. 작은 리소스가 많은 사이트에서 효과가 가장 큽니다. 리소스가 적거나 이미 HTTP/1.1 최적화 기법을 쓰고 있다면 개선폭이 작을 수 있습니다. 도메인 샤딩을 여전히 사용하는 사이트는 오히려 제거하기 전까지 더 느려질 수 있습니다.
HTTPS 없이 HTTP/2를 사용할 수 있나요?
기술적으로는 가능합니다 — 프로토콜 자체는 지원합니다. 현실적으로는 불가능합니다 — 브라우저는 HTTPS에서만 HTTP/2를 구현합니다. TLS 인증서가 필요합니다.
왜 서버 푸시는 자리를 잡지 못했나요?
근본적인 문제는 이겁니다: 서버는 브라우저의 캐시에 무엇이 있는지 알 수 없습니다. 이미 캐시된 리소스를 푸시하면 대역폭이 낭비되고 오히려 느려질 수 있습니다. 무엇을 푸시해야 할지 신뢰할 수 있는 방법이 없으니, 대부분의 구현은 너무 많이 푸시하거나 아예 포기했습니다.
HTTP/2가 HTTP/3로 대체되고 있나요?
HTTP/3는 TCP 대신 QUIC을 사용하여 HTTP/2의 TCP HOL 블로킹 한계를 해결합니다. 하지만 HTTP/2는 여전히 널리 사용되고 있으며 앞으로도 수년간 HTTP/3와 공존할 것입니다. HTTP/2 멀티플렉싱에 대해 배운 내용의 대부분은 HTTP/3에도 그대로 적용됩니다.
이 페이지가 도움이 되었나요?