HTTP 에 관한거라고는 시작줄, 헤더, 공백, 바디 정도로 이루어져있다. 는 것 정도밖에 모르고 있었다. 사실 HTTP 통신의 경우 워낙 추상화가 잘되어있기 때문에 그냥 뭐 알아서 잘하나보지라고 블랙박스인 상태로 놔두고 있었던 것 같다. 그러나 최근에 등장한 HTTP 2, 3 버전은 통신 방식에서 기존 HTTP 1 버전과 확연한 차이를 보이고 있기 때문에 버전별로 어떤 차이가 있는지 대강은 알 필요가 있는 것 같다. 그리고 공부하면서 조금 이해가 안갔던 부분도 정리를 해두려고 한다. 전체적인 배경은 GPT 선생님께서 정리해주셨다.
1. HTTP 1.0 ~ 1.1
HTTP 1.0 버전은 HTTP가 1991년 처음 발표되고 난 후 인기를 끌면서 발생한 다양한 요구사항들을 반영하여 처음으로 표준화하여 발표된 버전이다. 이 버전이 발표 되면서 기존 버전에는 0.9 라는 버전명이 붙이게 되었다. 그러나 1.0 버전이 발표된 바로 다음 해에 1.0 의 불완전한 부분들을 보충하기 위해 1.1 버전이 발표되었는데, 1.1 버전에 와서야 현재 우리에게 익숙한 HTTP 의 형태로 자리하게 된다. 우선 1.0 버전의 한계점을 알아보고 이를 보완한 1.1 버전에 대해서 알아보도록 한다.
1) HTTP/1.0 (1996)
단점 1: 비연결형 프로토콜 (Non-persistence Connections)
- 각 HTTP 요청마다 새로운 TCP 연결을 생성하고, 응답 후 연결을 끊음
- 웹 페이지를 불러올 때 HTML, CSS, 이미지 등 모든 리소스에 대해 개별적인 연결이 필요하게됨
- 따라서 여러 개의 연결이 생기면서 네트워크 리소스가 낭비되고 지연 시간이 증가함
단점 2: 콘텐츠 전송의 비효율성
- 전송하고자 하는 콘텐츠의 크기에 상관없이 모든 콘텐츠를 로드하여 콘텐츠의 크기를 Content-Length 헤더에 넣어서 보냄
- 때문에 데이터를 생성하면서 동시에 전송을 하는 스트리밍이 불가능
단점 3: 요청 및 응답의 순차처리
- 하나의 요청이 완료될 때 까지 다음 요청을 보낼 수 없음
- 따라서 특정 응답이 지연될 시, 전체 요청/응답이 지연됨
이외 단점
- 호스트 헤더가 없어 IP 당 하나의 도메인만 호스팅 할 수 있음
- 협상 헤더(언어, 인코딩 등)이 없어서 최적화된 콘텐츠 제공이 어려움
- 쿠키같은 상태 저장 메커니즘이 없어서 사용자 식별이나 세션 유지가 어려움
- 캐싱 매커니즘의 부재
2) HTTP/1.1 (1997)
특징 1: 지속 연결(Persistent Connections) 도입
- 지속 연결 헤더를 통해 TCP 연결을 여러 요청과 응답에 재사용하여
- Connection: keep-alive / Connection: close 를 통해 TCP 연결을 지속 유지 / 종료 할 수 있음
- 이를 통해 TCP 생성과 해제(핸드셰이크) 에 따른 네트워크 오버헤드를 줄임
특징 2: 청크 전송 인코딩(Chunked Transfer Encoding) 도임
- Transfer-Encoding: chunked 헤더를 통해 콘텐츠의 길이를 사전에 고지하지 않고 청크 단위로 전송함
- 이를 통해 실시간으로 데이터를 생성하는 동시에 전송할 수 있으며, 메모리 효율성이 향상됨
특징 3: 파이프라이닝(Pipelining) 도입
- 연결을 유지한 상태에서 여러 요청을 여러번 전송 가능
- 요청에 대한 응답이 오기전에 요청을 또다시 보낼 수 있어 응답 지연에 따른 문제가 줄어듦
- 그러나 요청에 대한 응답은 요청 순서 대로 받아야 하므로 하나의 응답이 지연되면 다른 응답 시간도 지연됨
이외 상기 여러 1.0 의 단점들을 개선
- 쿠키가 표준화 되어 사용자 식별과 세션 관리가 간편해짐
- Accept 헤더를 통한 콘텐츠 협상
- 다양한 HTTP 메서드의 등장 (PUT, DELETE, OPTIONS ... )
- 다양한 상태코드 도입
- Host 헤더 필수화
2. HTTP/2.0
HTTP/1.1이 발표된 1997년 이후 인터넷의 급격한 성장과 더불어 전송해야 할 리소스의 양이 증가하면서, 기존의 HTTP/1.1 프로토콜이 가지는 한계점을 해결할 필요성이 대두되었다. 이에 따라 2015년에 HTTP/2.0이 정식 발표되었고, 이는 HTTP/1.1의 데이터 전송 지연 문제를 근본적으로 해결하는 여러 기능을 도입했다.
특징 1: 이진 프로토콜 도입
- HTTP/1.1
- 텍스트 기반 프로토콜로 Human-Readable 하다
- 파싱 속도가 느리고 파싱 오류가 발생할 가능성이 높음
- HTTP/2.0
- 데이터가 기계가 읽을 수 있는 이진 데이터 형식으로 전송이 되어 파싱 속도를 높이고 파싱 오류를 줄임
Q. HTTP/1.1 도 결국 0 과 1 로 인코딩되어 보내지는 거 아닌가?
당연히 맞는 말인데 해당 HTTP 메세지는 다시 역직렬화를 통해 텍스트로 변환되고, 텍스트 상태에서 파싱된다. 그에 반해 HTTP/2.0 은 HTTP 메세지를 역직렬화하여 처리하는 것이 아니라, 이진데이터 그대로를 파싱하여 해당 데이터의 정보를 추출해낸다. (특히 데이터 프레임의 헤더 정보와 HPACK 압축된 HTTP 헤더를 처리할 때) 당연히 응답 본문은 다시 텍스트로 역직렬화 되어 사용된다.
특징 2: 멀티플렉싱
- HTTP/1.1
- 하나의 TCP 연결에서 한 번에 하나의 요청과 응답만 처리할 수 있었음.
- 파이프라이닝을 한다고 해도, 응답은 순차적으로 받아야 해서 Head Of Line blocking 이라는 지연 발생
- HTTP/2.0
- 데이터를 프레임이라는 작은 단위로 분할하여 전송
- 프레임 마다 스트림 ID 가 있어, 어떤 요청/응답에 포함되는 데이터인지를 구분 가능
- 여러 개의 요청과 응답을 동시에 처리를 하는 멀티플렉싱 기능을 통해 한번에 여러개의 요청/응답을 동시에 처리가능
- 기존에 발생하던 Blocking 을 제거함으로써 지연 속도를 최소화 함
+) 데이터 프레임
HTTP/1.1 에서는 HTTP 메세지가 헤더 + 본문으로 분할되지 않고 하나의 단위로 처리가 되었다. (물론 TCP 영역에서 MCU 에 맞춰 패킷으로 분할이 됨) 하나의 메세지가 분할되지 않고 연속에서 보내지기 때문에, 하나의 메세지를 다 보내야지만 다음 메세지를 보낼 수 있다. 또한, 응답도 요청을 보낸 순서대로 처리해야 한다. 왜냐하면 어떤 요청에 대한 응답인지 알 수 없기 때문이다.
HTTP/2.0 에서는 HTTP 메세지를 '데이터 프레임' 이라는 단위로 나누고 각 데이터 프레임 마다 프레임의 헤더를 달아서 전송한다. 이 헤더에는 데이터 프레임의 메타 정보를 담고 있는데, 어떤 스트림에 속하는 지에 대한 정보도 나와있기 때문에 어떤 요청/응답 메세지의 데이터를 담고있는지 식별이 가능하다. 때문에 메세지들을 동시에(비동기적으로) 처리가 가능해지고, 지연시간이 최소화 되는 것이다.
각 데이터 프레임의 헤더는 프레임의 길이 / 타입 / 플래그 / 스트림 ID 로 구성 되며 이후 데이터 프레임의 페이로드이 연결된다.
프레임 타입 | 역할 |
DATA (0x0) | HTTP 본문(바디) |
HEADERS (0x1) | HTTP 헤더 정보 |
PRIORITY (0x2) | 스트림의 우선순위 조정 |
RST_STREAM (0x3) | 스트림을 강제 종료 |
SETTINGS (0x4) | 연결의 설정 정보 교환 ex) 데이터 프레임 크기, 최대 스트림 수 제한 |
PUSH_PROMISE (0x5) | 서버 푸시 자원 알림 |
PING (0x6) | 연결 상태 확인 |
GOAWAY (0x7) | 연결 종료 |
WINDOW_UPDATE (0x8) | 윈도우 크기 조정 : 흐름 제어(Flow Control) 을 위함 소켓 버퍼나 네트워크 대역폭 등을 고려하여 사이즈를 정하는 거 같은데 잘 모르겠다. 우선 TCP 의 Flow Control 부터 공부해야지 |
CONTINUATION (0x9) | 헤더 프레임이 너무 큰 경우 추가적인 헤더 데이터 전송 |
특징 3: 헤더 압축
- HTTP/1.1
- HTTP 헤더를 보면 대부분이 커스텀 헤더가 아닌 반복적으로 쓰이는 헤더임
- 이는 불필요하게 데이터 낭비를 의미
- HTTP/2.0
- HPACK 알고리즘 및 Huffman Code 를 통해 헤더를 효율적으로 압축
- 이를 통해 네트워크 사용량을 감소하고, 전송 속도를 높임
+) HPACK 알고리즘
RFC 7541 (HPACK: Header Compression for HTTP/2) 에 정의된 압축 알고리즘으로, HTTP 프로토콜에서 자주 쓰이는 헤더 이름이나 값들을 미리 인덱싱해놓고, 해당 인덱스 테이블을 사용하여 압축하는 방식. 인덱싱되지 않은 문자열의 경우 동적으로 테이블을 구성하거나 리터럴로 전송이 되는데, 리터럴로 전송이 되는 경우는 Huffman Code 라는 인코딩 방식으로 다시 한번 압축이 된다. Huffman Code 는 다량의 HTTP 헤더 샘플을 통계적으로 분석하여 자주 등장하는 문자들을 더 짧은 코드에 맵핑하는 식으로 동작한다.
특징 4: 서버 푸시 (Deprecated)
- HTTP/1.1
- 클라이언트가 필요한 리소스를 요청하기 전까지 서버에서 리소스를 제공할 수 없음.
- HTTP/2.0
- 서버가 클라이언트의 요청을 예측해서 필요한 리소스를 미리 전송이 가능
- 이를 통해 요청하고 응답을 받는 대기 시간을 줄일 수 있다
+) 서버 푸시 매커니즘
서버는 PUSH_PROMISE 라는 데이터프레임을 전송하여 미리 어떤 데이터를 보낼지 클라이언트에게 알려주는데, 클라이언트에서 이를 받을지 말지를 결정한다. 클라이언트가 거부하지 않으면 서버에서는 해당 데이터를 전송함으로써 클라이언트는 별도의 요청 없이 해당 데이터를 받을 수 있다.
그런데 너무 신기한 기능이라 개발자 도구를 켜고 여기저기 돌아다니면서 서버 푸시 사례를 찾아보았는데 좀처럼 찾아볼 수 없었다. 알아보니 '22년에 크롬에서는 서버 푸시 지원 자체를 중지 시켰는데, 성능 향상이 명확히 확인되지 않았고, 대부분 저하를 일으켰기 때문이라고 한다. (https://developer.chrome.com/blog/removing-push) 해당 페이지에서는 Early Hints 나 Preload critical assets 같은 다른 최적화 기법들을 대안으로 제시한다.