HTTP의 진화 (2) - HTTP/3과 WebSocket - soo:bak
작성일 :
TCP의 한계
Part 1에서 HTTP/2의 TCP HOL Blocking 문제를 언급했습니다.
HTTP/2는 HTTP 레벨의 다중화를 구현했지만, 여전히 TCP 위에서 동작합니다.
TCP는 순서 보장, 연결 지향(3-way 핸드셰이크), IP 주소와 포트 기반 연결 식별이라는 특성을 가지는데, 이 특성들이 현대 웹 환경에서 오히려 문제가 됩니다.
TCP HOL Blocking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/2 스트림들:
스트림 1: [A][B][C]
스트림 2: [D][E][F]
스트림 3: [G][H][I]
TCP 전송:
[A][D][G][B 손실][E][H][C][F][I]
TCP 수신 버퍼:
[A][D][G][ ? ][E][H][C][F][I]
↑
B가 재전송될 때까지 전체 대기
스트림 2, 3의 데이터(E, F, H, I)는 준비됐지만
애플리케이션에 전달 불가
HTTP/2가 여러 스트림을 다중화해도, 하나의 TCP 연결을 공유하므로 패킷 손실이 모든 스트림에 영향을 줍니다.
연결 설정 지연
새 연결을 설정하려면 TCP 핸드셰이크에 1 RTT, TLS 핸드셰이크에 2 RTT(TLS 1.2) 또는 1 RTT(TLS 1.3)가 필요하여, 최소 2~3 RTT가 소요됩니다.
모바일 네트워크에서 RTT가 100ms라면, 연결 설정에만 200~300ms가 소요되는 셈입니다.
연결 마이그레이션 불가
TCP 연결은 출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트의 4-튜플로 식별되므로, IP 주소가 바뀌면 연결이 끊어집니다.
Wi-Fi에서 LTE로 전환하거나, 다른 Wi-Fi 네트워크로 이동하거나, NAT 타임아웃으로 포트가 변경되는 경우 모두 새 연결을 맺어야 합니다.
QUIC: UDP 위의 새로운 전송 계층
QUIC(Quick UDP Internet Connections)은 Google이 2012년부터 개발하여 2021년 IETF에서 RFC 9000으로 표준화된 프로토콜입니다.
핵심 아이디어는 UDP 위에 TCP의 기능(신뢰성, 흐름 제어, 혼잡 제어)을 구현하되 TCP의 한계를 극복하는 것입니다.
1
2
3
4
5
6
7
8
9
10
HTTP/2 스택: HTTP/3 스택:
┌─────────────┐ ┌─────────────┐
│ HTTP/2 │ │ HTTP/3 │
├─────────────┤ ├─────────────┤
│ TLS │ │ QUIC │ ← TLS 1.3 내장
├─────────────┤ ├─────────────┤
│ TCP │ │ UDP │
├─────────────┤ ├─────────────┤
│ IP │ │ IP │
└─────────────┘ └─────────────┘
QUIC의 스트림 독립성
QUIC의 가장 중요한 특성입니다.
각 스트림이 독립적으로 전달됩니다.
1
2
3
4
5
6
7
8
스트림 1: [A][B 손실][C]
스트림 2: [D][E][F]
스트림 3: [G][H][I]
QUIC 동작:
- 스트림 1: B 재전송 대기 중
- 스트림 2: D, E, F 즉시 애플리케이션에 전달
- 스트림 3: G, H, I 즉시 애플리케이션에 전달
패킷 손실이 해당 스트림에만 영향을 미치고, 다른 스트림은 계속 진행됩니다.
TCP와 비교:
TCP는 바이트 스트림 순서를 보장하므로 애플리케이션에 데이터를 순서대로 전달해야 하며, TCP 계층에서는 “스트림”이라는 개념 자체가 없습니다.
반면 QUIC은 스트림을 프로토콜 수준에서 지원하여, 각 스트림 내에서만 순서를 보장하고 스트림 간에는 순서 관계가 없습니다.
0-RTT 연결 설정
QUIC은 연결 설정을 획기적으로 빠르게 합니다.
첫 연결: 1-RTT
1
2
3
4
5
6
7
8
9
10
Client Server
│ │
│ ──── Initial (TLS ClientHello) ──► │
│ │
│ ◄─── Initial (TLS ServerHello) ─── │
│ ◄─── Handshake (인증서 등) ──────── │
│ ◄─── 1-RTT 데이터 ───────────────── │
│ │
│ ──── Handshake (완료) ───────────► │
│ ──── 1-RTT 데이터 ───────────────► │
QUIC은 TLS 1.3을 프로토콜에 통합하여, TCP + TLS의 별도 핸드셰이크 대신 하나로 통합했습니다.
재연결: 0-RTT
이전에 연결했던 서버라면:
1
2
3
4
5
6
Client Server
│ │
│ ──── Initial + 0-RTT 데이터 ─────► │
│ (이전 세션 키 사용) │
│ │
│ ◄─── Handshake + 1-RTT 데이터 ──── │
이전에 협상한 파라미터를 캐시하여 재사용하므로, 첫 패킷에 애플리케이션 데이터를 포함할 수 있습니다.
0-RTT의 주의점:
다만 0-RTT는 리플레이 공격에 취약하여, 공격자가 0-RTT 패킷을 캡처해서 재전송할 수 있습니다.
따라서 GET처럼 멱등성(idempotent)이 있는 요청에만 사용해야 합니다.
연결 마이그레이션
QUIC 연결은 IP 주소와 포트가 아닌 Connection ID로 식별됩니다.
1
2
3
4
5
6
7
8
9
초기 연결:
Client (192.168.1.10:5000) ◄──► Server
Connection ID: 0x1234abcd
Wi-Fi → LTE 전환:
Client (10.0.0.5:6789) ◄──► Server
Connection ID: 0x1234abcd ← 같은 연결
서버는 같은 Connection ID를 보고 같은 연결임을 인식
사용자가 네트워크를 전환해도:
- 연결이 유지됨
- 재핸드셰이크 불필요
- 진행 중인 다운로드가 중단되지 않음
모바일 환경에서 큰 장점입니다.
HTTP/3
HTTP/3은 QUIC 위에서 동작하는 HTTP로, 2022년 RFC 9114로 표준화되었습니다.
HTTP/2와 의미적으로(semantically) 동일하여 같은 메서드, 상태 코드, 헤더를 사용하지만, 전송 방식이 TCP/TLS에서 QUIC으로 바뀌었습니다.
QPACK: HTTP/3의 헤더 압축
HTTP/2의 HPACK은 TCP의 순서 보장에 의존했기 때문에, QUIC의 스트림 독립성에 맞는 새로운 방식이 필요했습니다.
QPACK은 별도의 단방향 스트림을 사용합니다:
- Encoder 스트림: 동적 테이블 업데이트 전송
- Decoder 스트림: 테이블 업데이트 확인
헤더 블록은 동적 테이블의 특정 상태를 참조하며, 수신자가 해당 상태에 도달할 때까지 대기할 수 있습니다.
HTTP/3 vs HTTP/2
1
2
3
4
5
6
7
8
9
특성 HTTP/2 HTTP/3
───────────────────────────────────────────────────
전송 프로토콜 TCP QUIC (UDP)
HOL Blocking 있음 (TCP 레벨) 없음 (스트림 독립)
연결 설정 2-3 RTT 1 RTT, 0-RTT
암호화 TLS 별도 TLS 1.3 내장
연결 마이그레이션 불가 가능
헤더 압축 HPACK QPACK
서버 푸시 있음 있음 (권장 안 함)
언제 HTTP/3이 유리한가:
- 높은 지연 네트워크 (모바일, 위성)
- 패킷 손실이 잦은 환경 (무선 네트워크)
- 네트워크 전환이 빈번한 환경 (모바일)
HTTP/2가 여전히 적합한 경우:
- 안정적인 유선 네트워크
- 레거시 인프라 (UDP 차단, 방화벽)
- QUIC 미지원 클라이언트
WebSocket: 양방향 실시간 통신
HTTP는 요청-응답 모델로, 클라이언트가 요청해야 서버가 응답합니다.
서버에서 클라이언트로 먼저 데이터를 보내려면?
전통적인 방법들:
폴링(Polling)
클라이언트가 주기적으로 요청합니다.
1
2
3
4
5
클라이언트: 새 메시지 있어? → 서버: 없어
(5초 후)
클라이언트: 새 메시지 있어? → 서버: 없어
(5초 후)
클라이언트: 새 메시지 있어? → 서버: 있어! [메시지]
대부분의 요청이 헛되어 비효율적입니다.
롱 폴링(Long Polling)
서버가 데이터가 있을 때까지 응답을 보류합니다.
1
2
3
4
5
클라이언트: 새 메시지 있어?
서버: (대기... 30초 동안)
메시지 도착!
서버: 있어! [메시지]
클라이언트: (즉시 다시) 새 메시지 있어?
개선되었지만, 여전히 HTTP 오버헤드가 있습니다.
WebSocket 프로토콜
이러한 한계를 극복하기 위해 WebSocket이 등장했으며, 진정한 양방향 통신을 제공합니다.
WebSocket은 2011년 RFC 6455로 표준화되었습니다.
핸드셰이크: HTTP 업그레이드
1
2
3
4
5
6
7
8
9
10
11
12
13
클라이언트 요청:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
서버 응답:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
이후 같은 TCP 연결이 WebSocket 프로토콜로 전환되어, 더 이상 HTTP가 아니게 됩니다.
양방향 통신
1
2
3
4
5
6
7
8
9
10
연결 설정 후:
클라이언트 ←────────────────→ 서버
(언제든 전송 가능)
클라이언트 → 서버: "안녕"
서버 → 클라이언트: "반가워"
서버 → 클라이언트: "새 메시지 왔어"
클라이언트 → 서버: "확인했어"
...
HTTP처럼 요청-응답 쌍이 없으며, 어느 쪽이든 언제든 메시지를 보낼 수 있습니다.
WebSocket 프레임 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
주요 필드:
- FIN: 마지막 프레임인지 (1비트)
- Opcode: 프레임 유형 (4비트)
- 0x1: 텍스트
- 0x2: 바이너리
- 0x8: 연결 종료
- 0x9: Ping
- 0xA: Pong
- MASK: 페이로드가 마스킹되었는지 (클라이언트→서버는 필수)
- Payload length: 데이터 길이
마스킹
클라이언트에서 서버로 보내는 모든 프레임은 프록시 캐시 포이즈닝 공격을 방지하기 위해 마스킹되어야 합니다.
4바이트 마스킹 키로 페이로드를 XOR합니다.
1
masked[i] = original[i] XOR masking_key[i % 4]
WebSocket 사용 사례
실시간 채팅
메시지가 오면 즉시 모든 참여자에게 전달하여, 폴링보다 훨씬 효율적이고 반응이 빠릅니다.
실시간 게임
플레이어의 위치와 행동을 실시간으로 동기화해야 하므로 지연시간이 중요합니다.
주식/가상화폐 시세
빠르게 변하는 가격을 실시간으로 표시.
협업 도구
문서 동시 편집, 화이트보드 공유.
알림/푸시
서버에서 클라이언트에 이벤트 알림.
Server-Sent Events (SSE)
WebSocket의 대안으로 SSE가 있습니다.
WebSocket은 양방향 통신을 지원하지만, SSE는 서버에서 클라이언트로의 단방향 통신만 지원합니다.
대신 SSE는 일반 HTTP를 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
응답 헤더:
Content-Type: text/event-stream
응답 본문 (스트리밍):
data: 첫 번째 메시지
data: 두 번째 메시지
event: update
data: {"price": 123.45}
SSE가 적합한 경우:
- 서버에서 클라이언트로만 데이터 전송
- HTTP 인프라 활용 (프록시, 캐싱, 인증)
- 자동 재연결 지원
반면 양방향 통신이 필요하거나, 바이너리 데이터를 다루거나, 더 낮은 오버헤드가 필요한 경우에는 WebSocket이 적합합니다.
HTTP의 미래
HTTP는 계속 진화하고 있습니다.
HTTP/3 보급
HTTP/3 지원은 꾸준히 증가하고 있습니다.
Cloudflare, Google, Meta 등 주요 서비스는 이미 지원 중이며, 대부분의 최신 브라우저에서 기본 활성화되어 있습니다.
WebTransport
HTTP/3(QUIC) 위에서 양방향 통신을 제공하는 프로토콜로, WebSocket의 HTTP/3 버전이라고 볼 수 있습니다.
QUIC의 스트림 독립성과 연결 마이그레이션을 활용합니다.
진화의 방향
HTTP는 더 낮은 지연시간, 더 나은 모바일 지원, 더 간소화된 프로토콜, 더 강화된 보안을 향해 나아가고 있습니다.
네트워크 환경이 변하면 프로토콜도 변합니다.
TCP가 40년 넘게 사용되었지만 웹의 요구사항이 변하면서 QUIC이 등장했듯이, 앞으로도 새로운 요구사항에 맞춰 HTTP는 계속 진화할 것입니다.
관련 글