HTTP의 진화 (1) - HTTP/1.0에서 HTTP/2까지 - soo:bak
작성일 :
웹의 언어
네트워크 통신의 원리 (3)에서 HTTP의 기본 구조를 살펴보았습니다.
HTTP(HyperText Transfer Protocol)는 웹의 애플리케이션 계층 프로토콜입니다.
1991년 팀 버너스리(Tim Berners-Lee)가 월드 와이드 웹과 함께 HTTP를 설계했으며, 처음에는 단순한 문서 전송만을 위한 프로토콜이었습니다.
30년이 지난 지금, 웹은 복잡한 애플리케이션 플랫폼이 되었고, HTTP도 이에 맞춰 함께 진화해왔습니다.
어떤 문제들을 어떻게 해결해왔는지 살펴보겠습니다.
HTTP/0.9: 시작
최초의 HTTP는 매우 단순했습니다.
1
2
3
4
5
6
7
요청:
GET /index.html
응답:
<html>
...페이지 내용...
</html>
특징:
- GET 메서드만 존재
- 헤더 없음
- HTML 파일만 전송
- 연결당 하나의 요청
HTTP/1.0: 확장
1996년 RFC 1945로 문서화되었습니다.
1
2
3
4
5
6
7
8
9
10
11
요청:
GET /page.html HTTP/1.0
Host: example.com
User-Agent: Mozilla/5.0
응답:
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 1234
<html>...
추가된 기능:
- HTTP 버전 표시
- 헤더 필드 (Host, Content-Type, 등)
- POST, HEAD 메서드
- 상태 코드 (200, 404, 500 등)
- 이미지, 비디오 등 다양한 콘텐츠
HTTP/1.0의 문제: 연결당 하나의 요청
1
2
3
4
5
요청 1 ──[TCP 연결]──► 응답 1 ──[연결 종료]
요청 2 ──[TCP 연결]──► 응답 2 ──[연결 종료]
요청 3 ──[TCP 연결]──► 응답 3 ──[연결 종료]
웹페이지 하나를 표시하려면 HTML, CSS, JavaScript, 이미지 등 수십 개의 리소스가 필요한데, 각 리소스마다 TCP 연결을 새로 맺어야 했습니다.
TCP 연결 비용:
- 3-way 핸드셰이크: 1 RTT
- TLS 핸드셰이크: 1-2 RTT 추가
- 슬로우 스타트: 처음에는 느리게 시작
리소스 50개면 50번의 연결 설정 오버헤드입니다.
HTTP/1.1: 지속 연결
1997년 RFC 2068, 1999년 RFC 2616으로 표준화되었습니다.
Keep-Alive (지속 연결)
1
2
──[TCP 연결]──────────────────────────────────────────►
요청1 → 응답1 요청2 → 응답2 요청3 → 응답3 ...
하나의 TCP 연결을 여러 요청에 재사용하는 방식으로, HTTP/1.1에서는 이것이 기본 동작이 되었습니다.
덕분에 연결 설정 오버헤드가 크게 줄었습니다.
파이프라이닝(Pipelining)
이론적으로, 응답을 기다리지 않고 여러 요청을 보낼 수 있습니다.
1
2
3
요청1 → 요청2 → 요청3 →
← 응답1 ← 응답2 ← 응답3
하지만 파이프라이닝은 실패했습니다.
이유는 Head-of-Line(HOL) Blocking 때문입니다.
HTTP/1.1에서 응답은 반드시 요청 순서대로 와야 합니다.
따라서 요청 1의 응답이 늦어지면, 이미 준비된 요청 2, 3의 응답도 함께 대기해야 합니다.
1
2
3
4
5
6
요청1 → 요청2 → 요청3 →
[응답1 지연...]
← 응답1 ← 응답2 ← 응답3
요청2, 3의 응답이 준비되어도 대기
결과적으로 대부분의 브라우저는 파이프라이닝을 비활성화했습니다.
HTTP/1.1의 한계
병렬 연결로 우회
HOL Blocking 문제를 완전히 해결할 수 없었기에, 브라우저는 우회 전략을 택했습니다.
도메인당 4~8개의 병렬 연결을 열어 각 연결에서 순차적으로 요청과 응답을 처리하는 방식입니다.
1
2
3
4
연결1: 요청1 → 응답1 요청5 → 응답5
연결2: 요청2 → 응답2 요청6 → 응답6
연결3: 요청3 → 응답3 ...
연결4: 요청4 → 응답4 ...
문제:
- 연결 수 제한 (동시 연결이 너무 많으면 서버에 부담)
- 각 연결마다 TCP/TLS 오버헤드
- 여전히 연결 내에서는 HOL Blocking
도메인 샤딩(Domain Sharding)
브라우저의 도메인당 연결 제한을 우회합니다.
1
2
3
4
메인 페이지: example.com
이미지: images.example.com
스크립트: scripts.example.com
스타일: styles.example.com
각 도메인에 6개씩 연결하면 총 24개까지 연결을 확보할 수 있습니다.
하지만 이 방식은 DNS 조회와 연결 설정 비용이 추가되는 단점이 있습니다.
헤더 중복
모든 요청에 같은 헤더가 반복됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /page1.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
Accept: text/html,application/xhtml+xml,...
Accept-Language: en-US,en;q=0.9,ko;q=0.8
Accept-Encoding: gzip, deflate, br
Cookie: session=abc123; preferences=...
GET /page2.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... ← 같음
Accept: text/html,application/xhtml+xml,... ← 같음
Accept-Language: en-US,en;q=0.9,ko;q=0.8 ← 같음
Accept-Encoding: gzip, deflate, br ← 같음
Cookie: session=abc123; preferences=... ← 같음
이처럼 수백에서 수천 바이트에 달하는 헤더가 매 요청마다 반복되며, 텍스트 기반이라 압축하기도 어렵습니다.
HTTP/2: 근본적 재설계
HTTP/1.1의 근본적인 한계를 극복하기 위해 HTTP/2가 등장했습니다.
Google의 SPDY 프로토콜에서 발전한 HTTP/2는 2015년 RFC 7540으로 표준화되었습니다.
핵심 변화: 이진 프레이밍 계층
HTTP/1.x가 텍스트 기반이었던 것과 달리, HTTP/2는 이진(binary) 프레이밍을 사용합니다.
1
2
3
4
5
6
7
8
9
10
HTTP/1.x:
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
\r\n
HTTP/2:
[프레임 헤더: 9바이트]
Length(24비트) | Type(8비트) | Flags(8비트)
Stream ID(31비트)
[프레임 페이로드]
왜 이진인가?
- 파싱이 빠름 (텍스트 파싱은 느림)
- 크기가 작음
- 오류 가능성 낮음
HTTP/2 스트림과 다중화
스트림(Stream)은 하나의 요청-응답 쌍을 의미하며, 각 스트림은 고유한 ID를 가집니다.
다중화(Multiplexing)
하나의 TCP 연결에서 여러 스트림이 동시에 전송됩니다.
1
2
3
4
5
6
7
┌──────────────────────────────────────┐
TCP 연결 │ [S1][S2][S1][S3][S2][S1][S3][S2]... │
└──────────────────────────────────────┘
S1: 스트림 1 (HTML 요청/응답)
S2: 스트림 2 (CSS 요청/응답)
S3: 스트림 3 (이미지 요청/응답)
각 스트림의 프레임이 인터리빙(interleaving)되어 전송되므로, 응답 순서와 상관없이 완료된 것부터 클라이언트에 전달됩니다.
HTTP/1.1과 비교:
1
2
3
4
5
6
7
8
9
10
HTTP/1.1 (순차적):
요청1 → 응답1 → 요청2 → 응답2 → 요청3 → 응답3
HTTP/2 (다중화):
요청1, 요청2, 요청3 →
← 응답2의 일부
← 응답1의 일부
← 응답3
← 응답2의 나머지
← 응답1의 나머지
큰 파일이 작은 파일을 막지 않습니다.
HTTP/2 프레임 유형
1
2
3
4
5
6
7
8
9
10
11
12
타입 설명
─────────────────────────────────────
HEADERS 요청/응답 헤더
DATA 바디 데이터
PRIORITY 스트림 우선순위
RST_STREAM 스트림 취소
SETTINGS 연결 설정
PUSH_PROMISE 서버 푸시 알림
PING 연결 상태 확인
GOAWAY 연결 종료 알림
WINDOW_UPDATE 흐름 제어
CONTINUATION HEADERS 연속
스트림 우선순위(PRIORITY)
브라우저가 중요한 리소스에 높은 우선순위를 부여합니다.
1
2
3
4
HTML: 우선순위 높음
CSS: 우선순위 높음
JavaScript: 중간
이미지: 낮음
서버는 우선순위에 따라 대역폭을 분배합니다.
HPACK: 헤더 압축
HTTP/2는 HPACK으로 헤더를 압축합니다.
정적 테이블
자주 사용되는 61개의 헤더를 미리 정의합니다.
1
2
3
4
5
6
7
8
9
인덱스 헤더 값
─────────────────────────────────────
1 :authority
2 :method GET
3 :method POST
4 :path /
5 :path /index.html
...
61 www-authenticate
따라서 인덱스만 전송하면 되어, “:method: GET”을 보내는 대신 “2”만 전송해도 됩니다.
동적 테이블
연결 중에 새 헤더가 나오면 동적 테이블에 추가하고, 이후 같은 헤더가 나타나면 인덱스로 참조합니다.
1
2
3
4
5
첫 번째 요청:
Cookie: session=abc123... → 전체 전송, 동적 테이블에 저장 (인덱스 62)
두 번째 요청:
Cookie: session=abc123... → "62"만 전송
허프만 인코딩
테이블에 없는 문자열은 허프만 코딩으로 압축하는데, 자주 사용되는 문자에 짧은 코드를 할당하는 방식입니다.
이러한 기법들을 통해 헤더 크기가 85~90%까지 감소합니다.
서버 푸시
서버가 클라이언트의 요청 전에 리소스를 보낼 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
클라이언트: GET /index.html
서버:
PUSH_PROMISE (나는 /style.css도 보낼 거야)
PUSH_PROMISE (나는 /script.js도 보낼 거야)
HEADERS (응답: /index.html)
DATA (/index.html 내용)
HEADERS (응답: /style.css)
DATA (/style.css 내용)
HEADERS (응답: /script.js)
DATA (/script.js 내용)
HTML을 파싱하고 CSS/JS를 요청하는 시간을 절약합니다.
하지만 실제로는 문제가 있습니다:
- 클라이언트가 이미 캐시에 가지고 있을 수 있음 (대역폭 낭비)
- 푸시할 리소스 결정이 어려움
- 브라우저 지원과 구현의 차이
결과적으로 서버 푸시는 실제로 잘 사용되지 않으며, HTTP/3에서도 지원은 하지만 권장하지 않습니다.
HTTP/2의 HOL Blocking 문제
HTTP/2는 HTTP 레벨의 HOL Blocking을 해결하여, 하나의 스트림이 느려도 다른 스트림은 진행될 수 있게 되었습니다.
하지만 TCP 레벨의 HOL Blocking이 남아있습니다.
TCP는 순서를 보장하는 프로토콜이므로, 패킷 하나가 손실되면 재전송을 기다려야 합니다.
1
2
3
4
5
6
7
8
TCP 패킷 흐름:
[패킷1] [패킷2] [패킷3 손실] [패킷4] [패킷5]
TCP 수신 버퍼:
[패킷1] [패킷2] [ ? ] [패킷4] [패킷5]
패킷 4, 5는 도착했지만 애플리케이션에 전달 불가
패킷 3 재전송 대기 중...
HTTP/2에서 이것이 문제가 되는 이유:
여러 스트림이 하나의 TCP 연결을 공유하기 때문에, 패킷 3이 스트림 1의 것이라도 스트림 2, 3도 모두 대기해야 합니다.
1
2
3
4
[S1-P1] [S2-P1] [S1-P2 손실] [S3-P1] [S2-P2]
스트림 2, 3의 패킷이 도착했지만
스트림 1의 패킷 재전송까지 모두 대기
이것이 HTTP/3에서 TCP를 버리고 QUIC를 사용하는 이유입니다.
Part 2에서 살펴봅니다.
HTTP/2 도입 시 고려사항
TLS 필수화
표준상 HTTP/2는 TLS 없이도 동작합니다 (h2c).
하지만 모든 주요 브라우저는 TLS 위에서만 HTTP/2를 지원합니다 (h2).
사실상 HTTP/2 = HTTPS입니다.
ALPN(Application-Layer Protocol Negotiation)
TLS 핸드셰이크 중에 HTTP 버전을 협상합니다.
추가 RTT 없이 HTTP/2 사용 가능 여부를 결정합니다.
기존 최적화 재고
HTTP/1.1을 위한 최적화가 HTTP/2에서는 오히려 해로울 수 있습니다:
- 도메인 샤딩: 불필요 (연결 하나로 충분)
- 이미지 스프라이트: 불필요 (다중화로 개별 요청해도 됨)
- 인라인 CSS/JS: 푸시로 대체 가능
- 연결 수 최소화: 이미 하나로 충분
HTTP/1.1에서 HTTP/2로
HTTP/2는 HTTP/1.1과 의미적으로(semantically) 호환됩니다.
- 같은 메서드 (GET, POST, PUT, DELETE…)
- 같은 상태 코드 (200, 404, 500…)
- 같은 헤더 필드 (대부분)
차이는 전송 방식(framing)입니다.
1
2
3
4
5
HTTP/1.1:
텍스트 기반, 개행 구분, 순차적
HTTP/2:
이진 프레임, 길이 지정, 다중화
따라서 애플리케이션 코드는 대부분 수정 없이 동작하며, 서버와 클라이언트 라이브러리가 전송 계층을 처리합니다.
관련 글