네트워크

HTTP/1.1와 비교하면서 알아보는 HTTP/2

teo_99 2024. 6. 16. 16:50

HTTP/1.1과 HTTP/2

HTTP/2는 HTTP/1.1과 어떤 점이 다른지 알아보기에 앞서, 왜 HTTP/2가 등장하게 되었는지를 먼저 이해하도록 하자.
 
HTTP의 유래는 하이퍼텍스트 전송으로, 실은 논문을 전송하기 위해 만들어진 프로토콜이다. 즉, 텍스트로 구성된 문서 전송에 있어서 최적화된 프로토콜이라는 것이다. 
 
하지만 최근의 웹 서비스를 살펴보면, 단순히 문서 교환을 위해 HTTP가 사용되는 것이 아니라, 이미지나 비디오와 같은 다양한 포맷의 정보를 전송하는 경우가 많아졌다. 즉, 요구사항이 시대의 흐름을 따라 변화하였다는 것인데, 기존의 HTTP/1.1로는 이러한 요구사항을 대응하기가 어려워졌다.
 
가장 큰 문제는 성능이었는데, 대표적으로는 헤더를 예시로 들 수 있겠다. HTTP 메세지의 경우 기본적으로 많은 헤더 정보를 가지고 있는데, HTTP/1.1에서는 이러한 헤더에 대한 압축 기술이나 최적화 기술이 없어 매번 똑같은 헤더를 전송해야만 했다. 심지어 쿠키를 사용하는 경우엔, 헤더 크기가 1KB 이상이 되는 경우도 있다 하니 상당히 네트워크에 부담이 갈 수 밖에 없다.

Naver HTTP Request Message Headers

 
헤더와 관련된 문제 이외에도, TCP와 관련된 문제도 상당히 성능에 병목이 되었다. TCP/IP 4 Layer를 기준으로 HTTP/1.1은 응용 계층 프로토콜이며, TCP 위에서 동작한다. 이는 반대로 말하면 'HTTP/1.1이 TCP를 사용하는 클라이언트다' 라고도 볼 수 있을 것인데, 문제는 HTTP/1.1이 TCP를 사용하는 방식에서 발생하게 되었다. 이는 추후 설명하겠지만, 근본적으로는 TCP와 HTTP의 특성이 일치하기 않기 때문이라는 사실만 이해하면 좋을 듯 하다. 
 
그리고 이러한 이유들로 HTTP/2가 등장하게 되었다. 다만 짚고 넘어가야 할 부분 중 하나는 'HTTP/2는 HTTP/1.1의 의미 체계를 수정하지 않는다는 것'이다. 즉 기존에 우리가 알던 HTTP의 인터페이스(메세지, 헤더, 바디, 메소드와 같은 개념들)는 변화하지 않는다. 다만 구조적인 부분에 있어서는 큰 차이가 있고 (바이너리 프레이밍 계층의 도입), 이는 HTTP/1.2가 아닌 HTTP/2라는 버저닝이 부여된 이유이기도 하다.
 

HTTP/1.1의 문제점과 HTTP/2의 개선안

앞서 HTTP/1.1에는 헤더, TCP와 관련된 문제점들이 있었다고 이야기했다. 여기서 문제점이라는 말은, 기술적으로 결함이 있다는 것이 아니라 요구사항의 변화를 만족하지 못한다는 관점에서 말한 것임을 이해하자.
 
이제 하나씩 HTTP/1.1의 어떠한 부분이 문제가 되었고, 이를 HTTP/2에서는 어떻게 개선하는지를 알아보도록 하자.
 

Head Of Line blocking (HOL 차단)

첫번째로 소개할 HTTP/1.1의 문제점은 Head Of Line blocking이다. 이는 컴퓨터 네트워크에서 흔히 사용되는 용어인데, 주로 큐에서 첫번째 요소로 인해 나머지 요소들이 정체되어 성능 제한 현상이 발생할 때 HOL 차단이 발생했다고 한다. 
 
HTTP/1.1에서도 이러한 HOL 차단이 발생할 수 있는데, HTTP/1.1은 하나의 TCP 커넥션에서 전송된 세그먼트들을 순서대로 처리하기 때문에, 이전에 전송한 HTTP 메세지가 빠르게 처리되지 못하면 이후 전송한 HTTP 메세지는 대기하게 된다.

HTTP/1.1 HOL Blocking Example

 
HTTP/1.1은 단순성을 중요시했기에, 위와 같이 설계되었고 이를 회피하기 위한 방법으로 TCP 커넥션을 여러 개 사용하는 경우도 있었다. 이후 살펴볼 예정이지만, HTTP/2에서는 이러한 문제를 스트림이라는 개념과 멀티플렉싱을 도입함으로서 해결한다. (하나의 커넥션을 여러 논리적인 스트림으로 나누어서 사용하는 방법)
 

HTTP/2 개선안: 스트림과 멀티플렉싱

HTTP/2에서는 TCP 커넥션을 조금은 다른 방식으로 활용한다. HTTP/1에서는 TCP 커넥션을 단순히 바이트들을 전송하기 위한 도구로만 활용했기에, 어떠한 개념적인 분리라고 할 것이 없었다. 아래는 HTTP/1.1과 HTTP/2의 TCP 커넥션 활용에서의 차이점을 나타내는 그림이다.

https://freecontent.manning.com/mental-model-graphic-how-is-http-1-1-different-from-http-2/

 
쉽게 말해, TCP 커넥션을 한 단계 추상화해 사용하는 것이라 보면 된다. 스트림을 통해 연관된 HTTP 메세지들이 전송되고, 각각의 스트림은 고유한 식별자를 가진다. 
 
스트림 이외에도 한 가지 개념이 더 추가되었는데, 바로 프레임이다. 단, 여기서 말하는 프레임은 데이터 링크 계층(OSI Layer 2)에서의 프레임이 아니라, HTTP/2에서 사용하는 용어임을 인지하자. 프레임은 HTTP/2에서의 최소 통신 단위이며 프레임마다 헤더를 가진다. 이러한 프레임은 스트림 내에서 송수신되며, 하나의 HTTP 메세지가 여러 프레임으로 쪼개져 전송된다고 보면 된다.
 
굳이 HTTP 메세지를 스트림을 통해 그대로 전송하지 않고 프레임이라는 단위로 한번 더 나눈 이유는 곧바로 설명할 압축과 관련된 이유 때문이다. HTTP 메세지는 첫 줄에 위치하는 start line을 제외하면 헤더와 바디로 구성되는데, 이 둘은 성격 자체가 다르기 때문에 동일한 압축 알고리즘을 적용할 수 없고, 이에 Headers Frame, Data Frame으로 분리되어 압축되어 전송된다. 
 
다시 돌아와서, 스트림이라는 개념을 이해했다면 추가적으로 한가지 개념을 더 이해할 필요가 있는데, 바로 멀티플렉싱이다. 스트림이라는 개념을 도입하면서 하나의 TCP 커넥션에는 여러 스트림이 존재할 수 있게 되었고, 이는 병렬적으로 많은 HTTP 메세지들이 송수신될 수 있음을 의미한다. 
 

 
이렇게 멀티플렉싱을 통해 여러 HTTP 메세지를 병렬적으로 송수신하기 때문에 앞서 설명한 HOL 차단이 발생하지 않는다. 다만 TCP 레벨에서는 HOL 차단이 발생할 수 있는데, 오류가 발생하거나 패킷이 손실되는 경우, 결국 하나의 패킷 안에는 여러 스트림의 정보가 포함되어 있기 때문에 스트림 간의 격리성을 보장받을 수 없다. 우선은 HTTP 레벨에서의 HOL 차단이 없어졌음을 이해하자.
 
또한, 하나의 TCP 커넥션을 사용하는 구조이기 때문에 리소스 측면에서도 이점을 가진다. 서버나 클라이언트는 더 이상 HTTP를 위한 TCP 커넥션을 파이프라이닝해서 사용할 필요도 없어졌으며, 메모리, 프로세싱 비용도 줄어들게 되었다. 이외에도 암호화 연결을 위한 키 교환 과정이 한 번만 이루어지면 되기 때문에, 보안 연결 측면에서도 이점이 있다.
 

Fat Message Headers (방대한 메세지 헤더)

다음으로 소개할 문제점은 메세지 헤더와 관련된 것이다. HTTP 메세지의 경우 바디를 설명하기 위한 메타데이터와 같은 정보들로 헤더가 상당량을 차지하게 된다. 다만 HTTP/1.1에서는 이러한 헤더를 매번 반복적으로 전송했고, 이는 네트워크를 대역폭을 낭비하는 원인으로 자리잡게 된다. 
 
HTTP/2에서는 이를 HPACK 이라는 압축 방식으로 해결하는데, 쉽게 말해 이전에 보낸 적이 있었던 헤더거나, 서로 알고 있는 헤더인 경우 헤더 그 자체를 보내는 것이 아닌, 인덱스 (번호)만 보내 수신측에서 테이블을 참고하여 헤더 값을 알아낼 수 있도록 하고 보낸 적이 없던 헤더에 대해서는 Huffman 부호화 방식을 적용하는 방식이다.
 

HTTP/2 개선안: HPACK

HTTP/2에서는 HPACK 알고리즘을 통해 헤더를 압축한다. HPACK은 앞서 말했듯 이미 알고 있는 헤더에 대해서는 인덱스만 보내고, 그렇지 않은 헤더에 대해서는 Huffman 부호화를 적용하는 방식이다. 조금 더 구체적으로 어떻게 압축이 이루어지는지를 알아보자.
 

https://developers.google.com/web/fundamentals/performance/http2?hl=ko

 
Request #1을 통해 전송한 적이 있었던 헤더들에 대해서는 이후 반복적으로 전송하지 않고, 새로운 헤더에 대해서만 전송하게 된다. 이런 작업이 가능한 이유는 Indexed List 방식 때문인데, 클라이언트와 서버는 헤더 정보를 위한 static table과 dynamic table을 유지하기 때문에 서로 알고 있는 헤더에 대해서는 인덱스만 전송하는게 가능하다.

  • static table: 일반적인 HTTP 헤더 필드들에 대한 정보를 기록하는 테이블. 대부분의 HTTP 메세지에서 사용하는 헤더들이 여기에 기록되어 있음
  • dynamic table: 처음에는 빈 테이블로 초기화되고, 주고받는 메세지 헤더에 따라 동적으로 헤더가 채워지는 테이블
https://zhuanlan.zhihu.com/p/34108036/voters

 
또한, 서로 알고 있지 못하는 헤더들(테이블에 없는 헤더)에 대해서는 Huffman 부호화 방식을 적용한다. Huffman coding은 무손실 압축 알고리즘인데, 정보의 빈도수를 기반으로 높은 출현율을 가지는 정보는 짧은 부호를, 낮은 출현율을 가지는 정보는 긴 부호를 할당하여 압축하는 방식을 의미한다. 
 

Limited Priorities (우선순위의 부재)

어떠한 정보를 서버에 요청할 때, 특정 정보는 더 우선순위를 가질 수 있다. 가령 뉴스 웹 사이트를 방문한다고 해보자. 페이지가 로드됨에 따라 어떠한 정보가 먼저 사용자에게 도달하는 것이 좋을까? 광고나 기타 부수적인 정보들보단, 헤드라인의 제목 등을 먼저 가시화하는 편이 나을 것이다. 
 
이처럼 HTTP로 제공하는 것이 결국은 비즈니스기에 우선순위를 지정할 수 있어야 된다는 니즈가 생기게 되었지만, HTTP/1.1에서는 관련한 수단이 없었다. HTTP/2에서는 스트림 우선순위 지정이라는 방식을 통해 이 문제를 해결한다.
 

HTTP/2의 개선안: 스트림 우선순위 지정

HTTP/2에서는 스트림마다 우선순위를 지정할 수 있다. 각 스트림은 가중치와 종속성을 가질 수 있는데, 가중치는 1~256 사이의 정수로 할당되며 이러한 가중치는 송수신 시 비중을 나타내게 된다. 종속성은 말 그대로 스트림간의 종속성을 나타내는 것이며, 상위 스트림이 먼저 처리되어야 하위 스트림이 처리될 수 있다.
 

https://web.dev/articles/performance-http2?hl=ko#streams_messages_and_frames

 
스트림 간의 가중치와 종속성이라는 개념은 결국 트리 구조로 표현될 수 있고, HTTP/2에서는 클라이언트가 우선순위 지정 트리를 구성하여 서버에게 요청할 수 있다. 가령 클라이언트는 '스트림 A는 대역폭의 3/4를 부여하고, 스트림 B는 1/4를 부여해라. 그리고 스트림 C는 A와 B 이전에 처리되어야 한다.' 라는 요구사항을 전달할 수 있는 것이다.
 
다만 중요한 점은, 이는 어디까지나 요구사항이기 때문에 서버에서 우선순위 지정 트리에 대한 작업을 반드시 보장하진 않는다. 위와 같이 우선순위를 클라이언트에서 전적으로 관리하게 되면 starvation과 같은 문제가 발생할 수도 있고, 서버 측의 안정성을 보장할 수 없기 때문이다.
 
참고로, 스트림의 경우 우선순위를 지정할 수 있을 뿐 아니라 개별적으로 흐름 제어도 가능하다. 별도의 스트림마다 윈도우 크기를 지정할 수 있기 때문에, 유튜브와 같은 서비스에서의 일시 정지 기능을 굳이 TCP 커넥션 단에서 구현하는게 아니라 스트림 레벨에서 구현할 수 있다.
 

Client-Driven Transmission (클라이언트 주도 전송)

HTTP/1.1은 클라이언트가 요청을 했을 시에만 응답을 제공한다. 이는 클라이언트와 서버의 관계를 고려하면 일반적으로는 적절한 시나리오이다. 하지만 서버가 송신해야 하는 리소스가 상당히 많고, 서버는 굳이 클라이언트가 요청하지 않아도 무엇을 제공해야 할지 아는 경우 전통적인 클라이언트 - 서버 관계는 비효율적이다.
 
HTTP/2에서는 Server Push(서버 푸쉬) 를 통해 이러한 문제를 해결한다.
 

HTTP/2의 개선안: Server Push (서버 푸쉬)

개념을 설명하기 이전에 중요한 점을 짚고 넘어가자면, HTTP/2의 서버 푸쉬는 대부분의 웹 브라우저에서 지원을 중단하게 되었다. 이는 생각보다 성능상의 이점을 실현하기가 어려운 경우가 많았고, 전체 HTTP/2 웹사이트의 1.25% 정도만 사용했기 때문이다. (참고: Chrome에서 HTTP/2 서버 푸시 삭제)
 
따라서 개념은 간략하게만 설명할 예정인데, 아래 그림과 같이 PUSH_PROMISE 프레임을 통해 클라이언트에게 푸쉬할 예정임을 알리고 이후 전송하는 방식이다.
 

https://web.dev/articles/performance-http2?hl=ko#streams_messages_and_frames

 

구조의 변화: Binary Framing Layer의 도입

HTTP/1.1과 HTTP/2의 차이점을 알아보았고, 마지막으로 구조적 차이점을 알아보고자 한다. HTTP/2는 바이너리 프레이밍 레이어를 통해 앞서 설명했던 HTTP 메세지를 프레임 단위로 나누는 작업, 압축하는 작업을 수행한다. 
 

https://web.dev/articles/performance-http2?hl=ko#streams_messages_and_frames

 
본문 제일 앞쪽에서 언급했듯이, HTTP의 인터페이스 자체는 변화하지 않았지만 바이너리 프레이밍 계층이 추가되면서 클라이언트와 서버가 HTTP/1.1의 의미 체계로는 HTTP/2 메세지를 이해할 수 없게 되었다. (그래서 버저닝도 1.x가 아닌 2가 되었다)
 

마치며

HTTP/1.1과 비교하며 HTTP/2의 기능, 구조를 알아보았다. 추후 HTTP/3와 QUIC에 대해서도 정리하도록 하겠다.
 

참고 자료

https://en.wikipedia.org/wiki/Head-of-line_blocking
https://web.dev/articles/performance-http2?hl=ko#streams_messages_and_frames
https://developer.chrome.com/blog/removing-push?hl=ko
https://freecontent.manning.com/mental-model-graphic-how-is-http-1-1-different-from-http-2/
https://www.cloudflare.com/ko-kr/learning/performance/http2-vs-http1.1/