Article/Network - Deep Dive

HTTP/2.0 - HTTP 멀티플렉싱, HoL Blocking, 그리고 HTTP/3.0의 등장 배경

조금씩 차근차근 2025. 4. 23. 23:33

HTTP/2.0의 등장 배경

HTTP/1.1 까지의 메시지 형식

  • HTTP/1.1은 그 구현의 단순성과 명료함, 접근성으로 많은 사랑을 받아왔고, 받고 있다.
    하지만, 하나의 커넥션으로 여러 요청/응답을 처리하기 어렵고, 응답을 받아야만 그 다음 요청을 보낼 수 있는 것은 분명한 아쉬움으로 남아있었다.
  • 그리하여, 다양한 곳에서 속도를 개선하기 위한 다양한 방법의 연구가 진행되었다.
  • 최종적으로, 구글의 "SPDY" 가 HTTP/2.0의 표준으로 결정되었다.
    • 하나의 TCP 커넥션에 여러 요청을 동시에 보내, 레이턴시를 줄이는 것 - HTTP 멀티플렉싱
    • RTT가 20ms인 경우, 12.34% 성능 개선 효과를 보았다.
    • RTT가 80ms인 경우, 23.85% 성능 개선 효과를 보았다.
    • RTT가 200ms인 경우, 26.79% 성능 개선 효과를 보았다.

HTTP/2.0 개요

RFC 문서에 표현된 HTTP/2.0의 메시지 형식 변화

HTTP/2.0은 TCP 커넥션 위에서 동작하는 모델로, 클라이언트가 TCP 커넥션의 초기화를 수행하여 연결이 시작된다.

기본 통신 (요청-응답 모델)

  • 요청과 응답은 길이가 정의된 (2^14바이트) 한개 이상의 프레임에 담긴다.
    • 이때 헤더는 압축되어 담긴다.
  • 프레임들에 담긴 요청과 응답은 "스트림"을 통해 보내진다.
    • 한개의 스트림이 한쌍의 요청과 응답을 처리한다.
  • 여러개의 스트림이 하나의 커넥션을 사용한다.
    • 여러개의 요청과 응답을 동시에 처리하는 것 역시 가능하다.
  • 스트림에 대한 흐름 제어와 우선순위 부여 기능도 제공한다.

HTTP/2.0의 특징 - HTTP/1.1과의 차이점

프레임

  • 기존 TEXT 기반 메시지를 더이상 사용하지 않고, 이진 데이터 기반 프레임(Frame) 이라는 PDU를 사용하게 된다..
  • 모든 데이터를 프레임단위로 전송한다.

지금부터 프레임의 구성요소를 하나씩 알아보자.

Length

  • 페이로드 길이(바이트 단위)
  • 24비트 부호없는 정수로 표현한다.
  • 기본 최댓값은 2^14바이트이다.

Type

  • 프레임의 종류를 나타내는 8비트 값
  • 이 값에 따라 페이로드의 형식과 의미가 결정된다.
  • 알 수 없는 타입이면, 프레임은 폐기된다.

플래그

  • 프레임 타입별로 정의도니 플래그 비트
  • 해당 타입에 따라서, 플래그의 의미가 달라진다.
  • 정의되지 않은 플래그 비트가 등장한 경우, 무시해야 한다.

R

  • Reserved
  • 예약된 비트이다.
  • 전송 시 항상 0으로 하고 수신 시 무시해야 한다.

Stream Identifier

  • 스트림의 식별자
  • 스트림이 클라이언트에 의해 초기화되었다면, 이 식별자는 홀수여야 한다.
  • 스트림이 서버에 의해 초기화되었다면, 이 식별자는 짝수여야 한다.
  • 스트림 ID 0번은 연결 제어 메시지를 위해 예약되어 있으며, 새 스트림을 여는 데에는 사용할 수 없다.
  • 송신자가 이 규칙을 어겼다면, 수신자는 PROTOCOL_ERROR 인 커넥션 에러로 응답해야 한다.

스트림과 멀티플렉싱

HTTP/2.0이 하나의 커넥션으로 다양한 요청을 한번에 보낼 수 있도록 하기 위해 만든, HTTP 멀티플렉싱에 대해 알아보자.

스트림

  • 위에서 설명했듯이, 프레임들에 담긴 요청과 응답은 "스트림"을 통해 보내진다
  • 한개의 스트림이 한쌍의 요청과 응답을 처리한다.
  • 정확히 무슨뜻인지는 멀티플렉싱 부분을 보면 이해하기 좋다.
  • 서버와 클라이언트는 스트림을 상대방과 협상 없이 일방적으로 만든다.
    • 스트림을 만들 때, 협상을 위해 TCP 패킷을 주고받느라 시간을 낭비하지 않아도 됨을 의미한다.
  • 한번 사용한 스트림 식별자는 다시 사용할 수 없다.
    • 그럼 스트림에 할당할 수 있는 식별자가 고갈되면?
    • 그때는 TCP 연결을 새로 맺으면 된다.
    • 스트림은 TCP 커넥션별로 생성된다는 것을 기억하자.

파이프라인 커넥션의 한계 - HoL Blocking (Head of Line Blocking)

  • 파이프라인은 응답 순서가 요청 순서와 동일해야 하는데, 그렇게 되면 반드시 첫번째 요청부터 먼저 처리해야 한다.
  • 그런데 첫번째 요청이 매우 오래걸리면, 금방 끝나는 뒷 요청까이 오랫동안 기다리다가 타임아웃이 발생하는 사태가 일어날 수 있다.
  • 이를 HOL 블로킹이라고 하며, HTTP/2.0에서는 HTTP 멀티플렉싱으로 파이프라인이 해결하려던 문제를 해결하였다.

HTTP 멀티플렉싱

  • 주의) 전송계층의 Multiplexing & Demultiplexing과는 다른 개념임에 유의하자.
  • 송신자는 HTTP 메시지를 프레임 단위로 인터리빙(interleaving)한다.
  • 수신자는 같은 스트림 ID를 가진 프레임들을 모아, 원래의 HTTP 메시지로 재조립한다.
  • HTTP/1.1의 파이프라인에서 발생하던 HoL 문제가 일부 완화되어 전송되었다.
    • HTTP/1.1 에서의 keep-alive 커넥션을 상상해보자.
    • 서버에서 특정 응답이 블로킹되어도 커넥션 자체는 다른 요청들을 응답할 수 있기 때문에,
    • 하나의 커넥션으로 여러 응답을 수행할 수 있게 되었다.

스트림의 우선순위

  • 스트림을 우선순위별로 순서를 정하여 처리하게 유도할 수 있는 기능이다.
  • 의무적으로 이 순서대로 처리해야 하는건 아니다.
  • 즉, 요청이 우선순위대로 처리된다는 보장은 없다.

스트림 별 프레임의 순서 보장? HoL Blocking 문제는 해결되었는가? - HTTP/3.0의 등장 배경

  • HTTP 메시지가 프레임 단위로 인터리빙(interleaving)하며 전송된다.
    • 그렇다면, 비동기처럼 메시지가 쪼개져서 날아가는건데 “해당 메시지의 순서는 어떻게 되는가”라는 질문을 던져볼 수 있고, 이는 TCP에서 보장해준다는 것을 우리는 잘 알고있다.
  • 애플리케이션 단 블로킹으로 인해, HOL 블로킹이 발생하여 응답이 지연되는 상황은 해결되었다.
  • 하지만 만약 전송 과정 중, 패킷이 유실된 상황을 상상해보자.

  • 클라이언트의 전송 계층에서 클라이언트의 응용 계층으로 데이터가 넘어가지 않는 상황이 발생한다.
  • 이는 TCP의 Selective ACK로 해결할 수 없는, TCP의 설계 상의 트레이드오프이다.
    • Selective ACK는 불필요한 전송을 막을 수는 있지만, 뒷 세그먼트의 HoL 블로킹까지는 막을 수 없다.
  • 한 스트림의 TCP재전송 대기가 모든 스트림의 진행을 일시 정지시킨다.
  • TCP에서 전송 순서가 보장되는 것이 결국 문제를 일으키는 것이다.

따라서 구글은 QUIC라는 새로운 전송 계층 프로토콜을 제시하여, HTTP/3.0를 설계하였다.

HTTP/3.0

  • UDP 기반
    • 응용 계층에서 연결 정보를 관리한다.
    • 연결 정보를 응용 계층에서 관리해주기 때문에, UDP 특성과 엮여 커넥션이 일시적으로 끊겨도 활용 가능하다.
  • 응용 계층 레벨 순서 보장
    • 클라이언트 애플리케이션 단에서 직접 조립함으로써, HoL Blocking 원천 방지가 가능해졌다.
  • 이제 전송 계층에서 발생하는 HoL 블로킹은 제거되었지만, QUIC에서 헤더가 유실되었을 때 발생하는 특정 스트림 내의 순서 보장을 위한 블로킹은 남아있다고 한다.

헤더 압축

탄생 배경

  • 웹 페이지 하나를 방문할 때의 요청이 많지 않았기 때문에, 헤더의 크기가 그다지 큰 문제가 되지 않았다.
    • 따라서, HTTP/1.1에서는 헤더가 아무런 압축 없이 그대로 전송되었다.
  • 현대는 웹 페이지 하나를 보기 위해 수십~수백번의 요청을 보내기 때문에, 헤더의 크기가 레이턴시에 영향을 끼치게 되었다.

헤더 압축이란?

  • 송신자는 헤더를 압축한 뒤, "헤더 블록 조각"들로 쪼개져서 전송된다.
  • 수신자는 이 조각들을 이은 뒤 압축을 풀어 원래의 헤더 집합으로 복원한다.
  • HPACK이라는 전용 알고리즘을 사용한다.
  • 헤더의 중복 전송으로 인한 오버헤드를 크게 줄여준다.

자세한 알고리즘 및 구현 방식은 RFC 7541을 참고하자.

서버 푸시

서버 푸시란?

  • 하나의 요청에 대해, 응답으로 여러개의 리소스를 보낼 수 있도록 해주는 기능이다.
  • 서버가 클라이언트에서 어떤 리소스를 요구할 것인지 미리 알 수 있는 상황에서 유용하다.
    • HTML 요구하면, CSS, JS 도 같이 요구하겠지?
  • 다시 요청을 수행해야 하는 트래픽과 레이턴시를 낮춰줄 것이라 생각했다.

서버 푸시의 동작 과정

  • 리소스를 푸시하려는 서버는, 먼저 클라이언트에게 자원을 푸시할 것임을 PUSH_PROMISE 프레임을 보내어 미리 알려주어야 한다.
  • 클라이언트가 PUSH_PROMISE 프레임을 받게 되면, 해당 프레임의 스트림은 클라이언트 입장에서는 '예약됨(원격)' 상태가 된다.
    • 이 상태에서, 클라이언트는 RST_STREAM 프레임을 보내어 푸시를 거절할 수 있다.
    • 이 경우, 스트림은 즉각 닫히게 된다.
  • 스트림이 닫히기 전까지, 클라이언트는 서버가 푸시하려고 하는 리소스를 요청해서는 안된다.
    • 서버가 푸시하려고 하는 자원을 클라이언트가 별도로 또 요청하게 되는 상황을 피하기 위함이다.

현대에서는 안쓰이는 이유

  • 서버 입장에서는 클라이언트가 이미 캐시한 리소스를 정확히 알기 어려워, 클라이언트가 캐시해둔 리소스를 반복해서 전송하게 될 수도 있다.
  • 서비스워커 등을 이용한 캐싱 전략이 더 예측 가능하고 제어하기 쉬워 많이 쓰인다.
  • HTTP/3.0에서는 아예 서버 푸쉬가 명세에서 사라졌다.

 

함께 보면 좋은 글: HTTP/2.0의 스트림 블로킹 & 흐름 제어

 

참고 자료