HTTP(Hypertext Transfer Protocol)는 웹에서 데이터를 주고받는 서버-클라이언트 모델의 프로토콜이다. 여기서 데이터란 본래 HTML 문서와 같은 HyperText를 칭하였지만, 현재는 이미지, 오디오, 비디오 등 다양한 형태의 데이터를 전송할 수 있다. 오늘은 이 HTTP의 발전 과정을 살펴보고자 한다.
1. HTTP/0.9 (1991)
문서화된 최초의 HTTP 버전은 0.9이다. 상태 코드, 헤더가 존재하지 않았으며, 지원되는 메소드 또한 GET뿐이었다. 그리고 HTTP의 이름에 걸맞게 HTML 파일만 전송 가능하였다. HTTP의 초기 버전답게 가장 단순한 형태를 갖고 있다.
2. HTTP/1.0 (1996)
이어서 등장한 HTTP/1.0은 인터넷 인프라가 빠르게 변화하면서 더 다양한 요구사항을 충족시킬 수 있도록 발전하였다. HTTP WG은 HTTP/1.0의 통신 스펙에 대한 기술 문서 RFC 1945를 발표하였으며, 정형화된 HTTP 포맷의 시초가 되었다.
- 요청에 HTTP 버전 정보 포함
- 응답에 상태 코드 도입
- 헤더 개념 도입 (Content-Type, User-Agent 등)
- POST, HEAD 메소드 추가
- HTML 외 다양한 파일 형식 지원
⛔ Short-lived Connection
1.0은 이전 버전에 비하면 많은 부분들이 개선되었지만, 매 요청마다 새로운 커넥션을 맺어야 해서 성능 저하 및 서버 부하 비용이 증가한다는 명확한 단점이 존재하였다.

예를 들어, 클라이언트가 웹페이지를 요청하게 되면 서버는 html, css, js, 이미지 등 다양한 자원들을 전달하게 되는데, 각 자원마다 매번 새로운 TCP 연결을 맺어줘야 했다. 이러다 보니 전체 자원을 받으려면 많은 시간이 소요되었다.
3. HTTP/1.1 (1997)
Short-lived Connection를 비롯한 단점들을 개선하기 위해 HTTP/1.1은 보다 빠르게 릴리즈 되었다. 1.1은 현재까지도 가장 많이 쓰이는 프로토콜 버전이며, 현재 다들 익히 알고 있는 HTTP 포맷의 형태를 갖추고 있다.
- 지속 연결 가능
- 파이프 라이닝 도입
- 청크 전송 인코딩
- 호스트 헤더 필수화
- PUT, DELETE, OPTIONS, PATCH 메서드 추가
- 캐싱 메커니즘 개선
주요 변경점들에 대해 알아보도록 하자.
🟢 Persistent connection
HTTP/1.1은 기본적으로 Connection: keep-alive 헤더를 설정하여, 하나의 TCP 연결을 통해 여러 요청/응답을 처리할 수 있게 개선하였다.

그림과 같이 최초 연결이 수립된 이후 타임아웃 시간 동안 지속적으로 연결을 유지하여 재사용할 수 있다. 또한, Connection: close 헤더를 통해 연결 종료 요청을 보낼 수도 있다.
🟢 Pipelining
파이프라인닝은 응답을 기다리지 않고 여러 요청을 연속적으로 보낼 수 있는 기술이다.

이전 요청에 대한 응답이 완전히 전송되기 전에 다음 전송을 가능하게 하여, 여러 요청을 연속적으로 보내 그 순서에 맞춰 응답을 받을 수 있다. RTT(네트워크 지연 시간)를 낮추기 위해 도입되었지만, HOL Blocking 및 구현 복잡성으로 인해 현재는 사장되었다.
🟢 Chunked Transfer Encoding
청크 전송 인코딩은 응답 생성 시점에 전체 콘텐츠 크기를 알 수 없을 때 활용하는 데이터 전송 방식이다. 지속 커넥션에선 클라이언트가 컨텐츠의 길이를 반드시 알아야 하는데, 전송할 컨텐츠가 크기가 클수록 이를 계산하는 데 어려움이 존재한다. 이에, 가변적인 데이터 전송을 위해 청크 전송 인코딩이 도입되었다.
동작 방식은 다음과 같다.
- Transfer-Encoding: chunked 헤더 사용
- 데이터를 일련의 청크(chunks)로 분할하여 전송
- 각 청크는 크기 정보(16진수)와 데이터로 구성
- 마지막에 0 크기의 청크로 전송 종료 표시
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n
⛔ HOL Blocking
1.1에서 도입된 파이프 라이닝을 통해 응답을 기다리지 않고 바로 요청을 보냄으로서 전체 네트워크 통신 시간을 줄일 수 있었다. 하지만, 여기에는 큰 한계점이 존재하였는데, 연속으로 보낸 요청에 대한 응답을 순차적으로 받아야 한다는 특성 때문에 앞의 요청 처리에 문제가 생기면 이후의 응답 또한 마냥 늦어지게 된다는 것이었다. 이러한 현상을 HOL Blocking(Head Of Line Blocking)이라고 불렀으며, 이로 인해 파이프 라이닝은 활용이 매우 제한적이였다.
⛔ 무거운 헤더
1.1의 헤더는 쿠키를 비롯한 많은 메타 정보들로 인해 무거워지게 되었는데, 전송하려는 데이터보다 오히려 헤더 값이 큰 경우도 발생하게 되었다. 또한, 지속 커넥션 속에서 주고 받는 요청에서 중복된 헤더를 사용하여 자원이 낭비되기도 하였다.
4. HTTP/2.0 (2015)
HTTP/2.0은 Google이 2009년에 개발한 SPDY(스피디) 프로토콜을 기반으로 발전하였다. 이전보다 웹 복잡성이 증가함에 따라 HTTP/1.1의 한계점이 부각되었고, 이에 대한 성능을 개선하고자 하였다.
성능 향상 수치는 다음과 같다.
- 페이지 로딩 시간 15-50% 단축
- 동일 연결에서 HTTP/1.1 대비 약 6-10배 많은 요청 처리 가능
- 헤더 압축으로 헤더 크기 80-90% 감소, 전체 트래픽 양 약 10-30% 감소
다음으로 주요 특징들을 알아보자.
🟢 Binay Framing Layer
HTTP/1.1에서의 메세지가 text로 전송되었던 반면, HTTP/2.0에선 binary frame으로 인코딩되어 전송된다.

이전에 헤더와 바디가 \n과 같은 개행 문자로 구분되었다면, HTTP/2.0에선 그림과 같이 다른 layer로 구분해놓았다. 이에 따라 파싱 효율성이 향상되었고, 오류 가능성 또한 감소되었다.

HTTP/1.1의 요청, 응답이 Message 단위로만 구성되어 있었다면, HTTP/2에선 Frame, Stream이라는 단위가 추가되었다.
- Frame: 통신 최소 단위, Header와 Data가 들어있다.
- Message: 요청, 응답의 단위, 다수의 Frame으로 이루어진다.
- Stream: 연결된 Conncetion에서 양방향으로 Message를 주고 받는 흐름이다.
🟢 Multiplexing
멀티플렉싱은 여러 개의 스트림을 사용하여 송수신하는 것이다. 이를 통해 특정 스트림의 패킷이 손실되었다고 하더라도 해당 스트림을 제외한 나머지 스트림은 정상적으로 동작할 수 있다.

그림과 같이 하나의 커넥션을 통해 동시에 여러 개의 메시지 스트림을 응답 순서에 상관 없이 주고 받을 수 있다. 해당 기술로 HTTP/1.1의 HOL Blocking 현상을 개선함과 동시에, latency를 줄여 전체 네트워크 비용을 감소시켰다.
🟢 헤더 압축
HTTP/2.0는 크기가 큰 헤더에 대해 압축을 시켜 개선하였다. 이때 허프만 코딩 압축 알고리즘을 사용한 HPACK 방식을 택하였다.
허프만 코딩
문자열을 문자 단위로 쪼개 빈도수를 세어 빈도가 높은 정보는 적은 비트 수를 사용하여 표현하고, 빈도가 낮은 정보는 비트 수를 많이 사용하여 전체 데이터 표현에 사용되는 비트 양을 줄였다.
HPACK

HPACK 압축 방식은 허프만 코딩과 정적/동적 테이블을 이용하여 구현하였는데, 메시지 헤더에 중복값이 존재하는 경우, 그림과 같이 index값만 전송하고 중복이 아닌 값은 허프만 코딩 알고리즘을 통해 인코딩하여 전송한다.
🟢 Server Push
이전까진 클라이언트가 서버에 요청을 보내야만 파일을 다운로드받을 수 있었다면, HTTP/2.0는 클라이언트의 요청 없이 바로 리소스를 푸시할 수 있게 되었다.
예를 들어, 클라이언트로부터 html 파일을 요청받았다면 서버 측에서 html에 포함된 css, js 파일들을 분석하여 알아서 보내게 된다. 이때, 서버는 각 리소스에 대해 PUSH_PROMISE 프레임을 클라이언트에 먼저 전송하게 되는데, 이는 해당 리소스를 보내줄 테니 이후 별도의 요청을 하지 말라는 것을 의미한다.
🟢 Stream Prioritization
멀티 플렉싱을 통해 비약적인 속도 향상이 이루어졌지만, 하나의 연결에 여러 요청과 응답이 섞이며 패킷 순서 또한 엉키게 되었다. 이에 클라이언트는 우선순위 지정 트리를 사용하여 스트림에 식별자를 설정함으로써 해결하였다.
HTTP/2의 각 stream은 1부터 256 사이의 정수 가중치를 할당할 수 있고, 다른 스트림에 대한 명시적인 의존성을 가질 수 있다. 이를 통해 클라이언트는 우선순위 트리 구성하고 전달할 수 있으며, 서버는 우선 순위가 높은 응답이 먼저 처리되게 대역폭을 설정할 수 있다.
트리 구조는 우선순위 가중치와 의존성이 섞여 조금 복잡한 형태를 갖게 되는데, 그림의 예시를 통해 이해해보자.

case 1.
같은 부모를 공유하는 경우, 가중치에 비례하여 리소스를 할당받게 되는데, 스트림 A의 가중치가 12, 스트림 B의 가중치가 4라면, 각 스트림이 받아야 할 리소스 비율은 각각 3/4, 1/4가 된다.
case 2.
스트림 C는 스트림 D에 의존하므로, D는 C보다 먼저 리소스의 전체 할당을 받게 된다. 이 경우, 가중치는 따지지 않는다.
case 3.
마찬가지로 D는 C보다 먼저 리소스의 전체 할당을 받아야하고, 이후 스트림 C는 A와 B보다 먼저 리소스의 전체 할당을 받게 된다. 이후 A와 B는 가중치에 따라 할당 받는다.
case 4.
D의 리소스 할당이 끝나게 되면, 스트림 E와 C는 같은 비율(1:1)로 할당 받는다. 이후 A와 B는 가중치에 따라 할당 받는다.
⛔ TCP의 한계?
HTTP/1.1에 비해 HTTP/2는 많은 성능적 향상을 이뤘다. 하지만 통신 프로토콜로 TCP를 사용하기에, TCP가 가지고 있는 한계점 또한 그대로 갖고 있다.
필연적인 RTT
다들 알고 있듯이 TCP/IP 네트워크에서 안정적이고 연결 지향적인 통신을 설정하기 위해 3-way handshake 과정을 거쳐야만 커넥션을 맺을 수 있다.

keep-alive 헤더를 통해 불필요한 handshake 과정은 줄일 수 있었지만, 여전히 이로 인한 지연 시간이 발생한다.
TCP 자체의 HOL Blocking
지금까지 언급한 HOL Blocking은 전부 HTTP 프로토콜이 사용되는 어플리케이션 계층에만 해당한다. 즉, 전송 계층에서는 패킷 유실, 오류에 대한 지연이 발생되면, 여전히 HOL Blocking이 발생한다.
5. HTTP/3.0 (2020)
결론적으로, HTTP/2.0은 TCP를 통해 통신함으로써 한계를 갖게 되었다. 이에 Google은 SPDY에 이어 UDP 기반의 QUIC 프로토콜을 고안하게 되었고, 이것을 애플리케이션 계층에서 동작하기 위해 설계된 것이 바로 HTTP/3.0이다.
주요 특징들에 대해 알아보자.
🟢 QUIC
HTTP Protocol Stack
여기, HTTP 버전별로 스택을 정리한 그림이 있다.

주목할 점은 HTTP/3를 이루는 기술 스택엔 TCP가 아닌 UDP를 사용하고 있는 점이다.
TCP/UDP
네트워크를 공부하게 되면, 다음과 같이 TCP와 UDP의 특징을 비교하는 표를 쉽게 접할 수 있다.

TCP는 통신에 있어 일련의 handshake 과정을 거치기 때문에 신뢰성을 보장받을 수 있는 반면, UDP는 이러한 과정을 생략하기 때문에 더 빠른 속도를 가질 수 있다. 그렇다면, UDP를 사용하는 것이 안전하지 않은 방식일까? 답은 사용하기 나름이다.

그림으로 다시 돌아와서, UDP 위에 QUIC(Quick UDP Internet Connections)라는 스택이 존재하는 것을 확인할 수 있다. 그리고 이 QUIC 안에는 TLS 1.3 기반의 내장 암호화 프로토콜이 포함되어 있는 것을 확인할 수 있다.
정리하자면, HTTP/3는 TCP 통신이 가지는 한계점을 넘기 위해 UDP 기반의 QUIC 프로토콜을 사용하며, 그 안에는 TLS 암호화 기술이 내장되어 있어 안전한 통신을 보장해준다. QUIC를 개발한 Google은 현재 서비스의 약 절반 정도가 QUIC 프로토콜을 사용하고 있는 만큼, 이미 성능은 입증된 상황이다.
🟢 RTT 단축

기존의 TCP/TLS(1.2) 환경에선 3번의 handshake 과정을 거친 RTT 비용이 발생하게 된다. 하지만, HTTP/3에선 클라이언트가 서버에 연결 요청을 보내면, 서버는 연결에 필요한 인증 정보와 데이터를 함께 보내버리기 때문에 단 1회의 handshake로 연결을 설정할 수 있다. 즉, 2번의 RTT 비용을 아낄 수 있다. 또한, 한번 연결에 성공해놓은 상태라면 이후의 연결은 캐시를 사용하여 0-RTT만에 바로 통신을 시작할 수 있다.
🟢 멀티플렉싱 개선
기존 HTTP/2의 멀티플렉싱은 특정 스트림의 패킷 손실이 발생하면, 해당 패킷이 재전송될 때까지 모든 패킷의 전달이 멈추는 HOL Blocking 현상이 발생하였다. 이는 그림과 같이 한 개의 TCP 연결에서 여러 스트림을 주고 받기에 생기는 문제였다.

반면, QUIC에선 여러 개의 독립적인 스트림을 구성하여 병렬 형태로 주고 받게 하여, 특정 스트림에 HOL Blocking이 발생하더라도 전체 스트림에 영향을 최소화할 수 있게 하였다.
🟢 보안 강화
TCP와 달리 QUIC는 PUBLIC_RESET 및 CHLO와 같은 몇 가지 초기 연결 메시지를 제외하고, 모든 패킷 헤더가 인증되고 페이로드(메시지 본문)가 암호화된다. 패킷 헤더의 인증은 수신 측에서 패킷 변조를 즉시 감지할 수 있게 하여 중간자 공격과 같은 보안 위험을 효과적으로 줄일 수 있다.
그림에서 보라색으로 표시된 부분은 스트림 프레임 패킷의 인증된 헤더를 가리키고, 노란색 부분은 완전히 암호화된 페이로드를 가리킨다.

⛔ 한계점
HTTP/3을 끝으로 과거부터 현재까지 HTTP의 발전 과정을 간략하게 다뤄보았다. 그렇다면, 20여 년 간의 발전 과정을 거친 HTTP/3은 완전무결한 프로토콜일까?
호환성 문제
이미 HTTP/1.1이나 HTTP/2.0 기반의 최적화가 진행된 상황이라면, 기존의 네트워크 인프라와의 호환성 문제로 HTTP/3 도입이 부담스러울 수 있다.
높은 CPU 사용량
QUIC의 암호화는 패킷별로 진행되기에 TCP보다 CPU 자원을 더 많이 소모하게 된다.
Reference
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Connection_management_in_HTTP_1.x
https://hpbn.co/http2/
https://www.cdnetworks.com/ko/blog/media-delivery/what-is-quic/
https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-HTTP-30-%ED%86%B5%EC%8B%A0-%EA%B8%B0%EC%88%A0-%EC%9D%B4%EC%A0%9C%EB%8A%94-%ED%99%95%EC%8B%A4%ED%9E%88-%EC%9D%B4%ED%95%B4%ED%95%98%EC%9E%90#%EB%B3%B4%EC%95%88%EC%9D%84_%EB%8D%94%EC%9A%B1_%EA%B0%95%ED%99%94
