최근 또 네트워크 문제 하나를 파헤보다가, 그 김에 HTTP 버전들의 진화를 다시 훑어보게 됐습니다. HTTP/1.1은 20년 넘게 사용됐고, HTTP/2는 2015년에 표준이 됐으며, HTTP/3은 최근 2년 사이 주류 사이트에 널리 적용되기 시작했습니다. 이들은 단순히 필드 몇 개를 추가한 수준이 아니라, 하위 전송 모델 자체가 바뀐 것이 핵심입니다.
이 글에서는 HTTP/1.1, HTTP/2, HTTP/3 (QUIC)를 한데 놓고, “왜 부족했던가 → 어떻게 바꿨는가 → 바꿔도 남는 문제는 무엇인가”라는 흐름으로 정리했습니다.
관련 노트
- [[笔记/web/计算机网络/TCP 重传、滑动窗口、流量控制与拥塞控制]]
- [[笔记/web/计算机网络/计算机网络框架]]
- [[笔记/web/计算机网络/HTTP 1.1 与 HTTP 2 协议对比]]
1. HTTP/1.0简史
HTTP는 Tim Berners-Lee가 1989년 만든 것으로, 처음 목적은 아주 단순했습니다. 세계 어디에나 있는 물리학자들이 서로의 문서를 참조할 수 있게 하는 것이었습니다. 문서 하나가 곧 파일 하나였고, 브라우저가 파일명을 서버에 보내면 서버는 내용을 돌려주었습니다.
첫 공식 버전인 HTTP/1.0(RFC 1945, 1996)의 핵심은 단 하나였습니다. 요청 한 번, 응답 한 번, 그리고 TCP 연결 종료.
페이지 하나에 이미지 10개가 들어가려면 TCP 연결을 10번 만들어야 했습니다. TCP 핸드셰이크에 1.5 RTT가 걸리고 TLS까지 더하면 몇 개의 RTT가 더 필요했습니다. 전화 회선으로 인터넷에 접속하던 시절에는 핸드셰이크가 가장 큰 비용이었습니다.
HTTP/1.0은 이 문제를 해결하지 못했지만, 대신 확장 가능한 프로토콜 뼈대를 마련했습니다. 메서드, 상태 코드, 헤더 필드, 바디가 여기서 정해졌습니다. 이 뼈대는 이후로도 바뀌지 않았습니다. HTTP/2가 바꾼 것은 “이것들을 어떻게 전달할 것인가”이지, “이것들이 무엇을 의미하는가”가 아니었습니다.
3. 브라우저는 왜 6개의 연결을 여는가
HTTP/1.1에서는 하나의 연결에서 요청을 순차적으로 처리하므로, 동시에 처리하려면 TCP 연결을 여러 개 열어야 했습니다.
Chrome은 같은 도메인에 기본적으로 6개의 연결을 동시에 사용하고, Firefox도 약 6개를 사용합니다. 이것은 버그가 아니라 HTTP/1.1 시대에 불가피했던 타협이었습니다.
부작용:
- 각 연결은 TCP 혼잡 제어를 독립적으로 수행하므로 서로 조정되지 않습니다.
- 각 연결이 메모리, 파일 디스크립터, 포트를 점유합니다.
- 프론트엔드 엔지니어들은 “요청 수 줄이기”를 위해 스프라이트 시트, CSS/JS 번들링, 도메인 샤딩(sharding) 같은 요령을 발명했습니다.
하지만 HTTP/2 이후에는 이런 기법들이 모두 필요 없어졌습니다.
5. 바이너리 프레이밍
HTTP/1.x의 메시지는 텍스트 형식입니다:
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n\r\n
텍스트 프로토콜의 장점은 사람이 읽기 쉽고 디버깅이 편하다는 점입니다. 대신 파싱이 느리고 모호한 부분이 많습니다(줄바꿈이 \n인지, \r\n인지, 그 외의 형식인지).
HTTP/2는 이를 바이너리로 바꿨습니다. 모든 프레임에는 9바이트짜리 고정 헤더가 있습니다:
Length: 프레임 페이로드의 바이트 수Type: 프레임 유형(HEADERS, DATA, PRIORITY 등)Flags: 플래그 비트(ENDSTREAM, ENDHEADERS)Stream Identifier: 스트림 ID
자주 등장하는 프레임 유형:
| Type | 역할 |
|---|---|
| HEADERS | 요청/응답 헤더 전달 |
| DATA | 바디 전송 |
| PRIORITY | 스트림 우선순위 선언 |
| RST_STREAM | 스트림 하나 취소 |
| SETTINGS | 연결 파라미터 협상 |
| WINDOW_UPDATE | 플로 제어 |
| PING | 하트비트 및 RTT 측정 |
| GOAWAY | 연결 종료 예정임을 알림 |
| PUSH_PROMISE | 서버 푸시 예고 |
중요한 점은 HEADERS와 DATA가 별개의 프레임이라는 것입니다. 따라서 요청은 먼저 HEADERS를 보낸 뒤, 바디를 여러 개의 DATA 프레임으로 나누어 보낼 수 있고, 각 DATA 프레임의 길이는 자유롭게 정할 수 있습니다.
7. HPACK 헤더 압축
HTTP 헤더는 웹 성능을 오랫동안 발목을 잡던 문제입니다:
- 일반 요청의 헤더는 약 600-800바이트입니다.
Cookie,User-Agent,Accept,Referer같은 필드는 요청마다 거의 같습니다.- HTTP/1.x는 헤더를 압축하지 않습니다(gzip은 바디를 압축합니다).
- 페이지에 요청이 50개라면 헤더만으로 30-40KB입니다.
HTTP/2는 HPACK(RFC 7541)을 도입했습니다.
정적 테이블과 동적 테이블
핵심은 두 개의 테이블입니다:
- 정적 테이블: 명세에 고정된 일반 헤더 이름-값 쌍 61개. 예를 들어
:method GET,:scheme https,:authority example.com등입니다. - 동적 테이블: 연결 양쪽에서 각각 관리하는, 변경 가능한 테이블입니다.
헤더를 보낼 때:
- 정적 테이블에 있으면 → 1바이트 인덱스만 전송합니다.
- 동적 테이블에 있으면 → 인덱스와 매칭 방식을 전송합니다.
- 둘 다 없으면 → 리터럴 값을 보내고, 선택적으로 동적 테이블에 삽입합니다.
첫 번째 요청:
같은 도메인으로 두 번째 요청을 보낼 때:
Huffman 인코딩
리터럴 값에는 HTTP 헤더의 문자 빈도에 맞춰 설계한 Huffman 코딩을 한 번 더 적용합니다. 디코딩에는 O(n) 비트 연산만 필요하며, 처리 성능이 높습니다.
HPACK이 gzip보다 HTTPS에 적합한 이유
gzip으로 압축하면 그만 아닌가요?
핵심은 보안입니다:
- gzip은 반복 패턴에 의존하는 문맥 기반 압축 방식입니다.
- 공격자는 압축된 길이를 관찰해 평문을 추론할 수 있습니다. 이것이 CRIME와 BREACH 공격입니다.
- HPACK은 딕셔너리 인덱싱을 기반으로 사전 정의된 인코딩 방식이므로 공격자가 길이만으로 평문을 추론할 수 없습니다.
HPACK은 HTTPS 환경에서 압축과 보안을 함께 제공하는 것을 목표로 특별히 설계됐습니다.
9. HTTP/2가 해결하지 못한 문제: TCP 계층의 HOL 블로킹
HTTP/2는 애플리케이션 계층의 HOL 블로킹을 해결했지만, 전송 계층의 HOL 블로킹은 해결하지 못했습니다.
왜일까요? HTTP/2의 멀티플렉싱은 논리 계층에 구현된 기능일 뿐, 그 아래에는 여전히 TCP 연결 하나가 있습니다. TCP는 데이터를 순서가 있는 바이트 스트림으로 취급하며, 프로토콜은 다음과 같이 규정합니다:
TCP 패킷 하나라도 유실되면 TCP는 손실된 패킷이 재전송되어 전달될 때까지 이후의 모든 패킷을 버퍼링해야 하며, 그때서야 상위 애플리케이션에 넘겨야 합니다.
따라서 TCP 패킷 하나라도 유실되면 해당 연결의 모든 HTTP/2 스트림이 멈춥니다. 다른 스트림의 프레임이 이미 도착했더라도 마찬가지입니다.
패킷 손실률이 낮은 유선 네트워크에서는 문제가 되지 않습니다. 하지만 모바일 네트워크, 위성 네트워크, 지하철 칸 같은 환경에서는 HTTP/2의 장점이 크게 약해집니다.
HTTP/2가 해결하지 못한 문제는 두 가지 더 있습니다:
- 핸드셰이크 오버헤드: TCP 3-way 핸드셰이크와 TLS 1.2를 거쳐 첫 바이트를 보내기까지 최소 3-RTT가 필요합니다.
- 연결 마이그레이션: TCP 연결은 4-튜플(IP + 포트)에 묶여 있어, 네트워크를 Wi-Fi → 4G로 바꾸면 끊어집니다.
이 문제를 해결하려고 Google은 아예 TCP까지 갈아엎었습니다. UDP를 기반으로 QUIC을 만든 것이 바로 HTTP/3입니다.
11. 핸드셰이크 비교: HTTP/3은 왜 0-RTT인가
HTTP/3에서 가장 간과되기 쉬운 개선점입니다. 먼저 비교해 보겠습니다.
HTTP/1.1 + TLS 1.2
HTTP/2 + TLS 1.2
HTTP/1.1과 같은 핸드셰이크 비용이 듭니다. ALPN 협상에서 h2를 선언한다는 점만 다릅니다.
HTTP/2 + TLS 1.3
TLS 1.3은 핸드셰이크를 2-RTT에서 1-RTT로 줄였습니다.
HTTP/3 + QUIC
QUIC은 TCP 핸드셰이크와 TLS 핸드셰이크를 하나로 합쳤습니다. 1-RTT 핸드셰이크 + 0-RTT 세션 재개.
0-RTT가 공짜는 아닙니다. 재생 공격 위험이 있기 때문에 일반적으로 멱등 요청(GET, HEAD)에만 사용합니다. 금전 거래가 관련된 작업(POST, 이체)에는 0-RTT를 사용할 수 없습니다.
13. 실전 적용: Nginx에서 HTTP/2와 HTTP/3 켜기
HTTP/2
http2는 listen의 매개변수이며, 별도의 설정 항목이 아닙니다. 반드시 HTTPS와 함께 사용해야 합니다. 브라우저는 평문 HTTP/2를 지원하지 않습니다.
HTTP/3
Nginx 1.25.0+는 QUIC을 네이티브로 지원합니다(이전에는 실험 모듈이었습니다):
핵심 포인트:
- 443 포트에서 TCP와 UDP를 동시에 수신해야 합니다. HTTP/3은 UDP를 사용하고, 오래된 클라이언트는 TCP/HTTP/2를 사용합니다.
add_header Alt-Svc는 브라우저에 이 사이트가 HTTP/3을 지원한다는 사실을 알려줍니다.reuseport를 사용하면 여러 워커 프로세스가 동일한 UDP 포트를 공유할 수 있습니다.- 방화벽에서 UDP 443을 허용해야 합니다. 많은 기업 내부망은 TCP 443만 허용하며, 이런 네트워크에서는 HTTP/3을 사용할 수 없습니다.
검증
15. 참고 자료
[1] RFC 1945 - HTTP/1.0. https://datatracker.ietf.org/doc/html/rfc1945 [2] RFC 7230 - HTTP/1.1: 메시지 구문과 라우팅. https://datatracker.ietf.org/doc/html/rfc7230 [3] RFC 7540 - HTTP/2. https://datatracker.ietf.org/doc/html/rfc7540 [4] RFC 7541 - HPACK. https://datatracker.ietf.org/doc/html/rfc7541 [5] RFC 8446 - TLS 1.3. https://datatracker.ietf.org/doc/html/rfc8446 [6] RFC 9000 - QUIC. https://datatracker.ietf.org/doc/html/rfc9000 [7] RFC 9114 - HTTP/3. https://datatracker.ietf.org/doc/html/rfc9114 [8] High Performance Browser Networking. Ilya Grigorik. https://hpbn.co/ [9] HTTP/2 소개. Google Developers. https://developers.google.com/web/fundamentals/performance/http2 [10] Chrome Status - HTTP/2 서버 푸시 제거. https://chromestatus.com/feature/5762410658996224 [11] Nginx HTTP/2 모듈. https://nginx.org/en/docs/http/ngx_http_v2_module.html [12] 네트워크를 그림으로 이해하기. 小林. https://xiaolincoding.com/network/