작성일 :

TCP의 한계

Part 1에서 HTTP/2가 이진 프레이밍과 다중화로 HTTP 레벨의 HOL Blocking을 해결한 것을 살펴보았습니다. 하지만 TCP 레벨의 HOL Blocking은 여전히 남아있었습니다.

HTTP/2는 TCP 위에서 동작합니다. TCP의 특성들이 현대 웹 환경에서 오히려 한계가 됩니다.


TCP HOL Blocking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
서버                                         클라이언트
  │                                               │
  │ ─── [A:스트림1] ───►                          │ → 도착
  │ ─── [D:스트림2] ───►                          │ → 도착
  │ ─── [G:스트림3] ───►                          │ → 도착
  │ ─── [B:스트림1] ───X (손실)                   │
  │ ─── [E:스트림2] ───►                          │ → 도착
  │ ─── [H:스트림3] ───►                          │ → 도착
  │                                               │
  │                     TCP 수신 버퍼:            │
  │                     [A][D][G][ ? ][E][H]      │
  │                              ↑                │
  │                     B가 올 때까지 전체 대기    │
  │                                               │
  │                     HTTP/2 입장:              │
  │                     - 스트림 2, 3 완료 가능    │
  │                     TCP 입장:                 │
  │                     - 전부 대기                │

HTTP/2는 스트림 ID로 응답을 구분할 수 있습니다. 스트림 2, 3의 데이터가 도착했으니 먼저 처리하면 될 것 같습니다.

하지만 E와 H는 아직 TCP 수신 버퍼에 있습니다. TCP는 순서대로 전달해야 하므로 B가 도착할 때까지 E, H도 HTTP/2에 넘기지 않습니다. E가 스트림 2, H가 스트림 3이라서 B와 무관하다는 사실을 TCP는 모릅니다.


연결 설정 지연

Part 1에서 살펴본 것처럼, 새 연결을 설정하려면 TCP 핸드셰이크와 TLS 핸드셰이크가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
클라이언트                                  서버
    │                                        │
    │ ─── TCP SYN ──────────────────────────►│
    │ ◄── TCP SYN-ACK ────────────────────── │  1 RTT
    │ ─── TCP ACK ──────────────────────────►│
    │                                        │
    │ ─── TLS ClientHello ──────────────────►│
    │ ◄── TLS ServerHello, 인증서 ────────── │  1~2 RTT
    │ ─── TLS 완료 ─────────────────────────►│
    │                                        │
    │ ─── HTTP 요청 ────────────────────────►│  ← 여기서야 데이터 전송
    │                                        │
    |←────────── 2~3 RTT ──────────→|

모바일 네트워크에서 RTT가 100ms라면, 첫 HTTP 요청을 보내기까지 200~300ms가 걸립니다. 사용자가 링크를 클릭하고 0.2~0.3초 동안 아무 일도 일어나지 않는 것입니다.


연결 마이그레이션 불가

TCP 연결은 4개의 값으로 식별됩니다: 출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트.

1
TCP 연결 = (출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트)

이 중 하나라도 바뀌면 다른 연결입니다.

1
2
3
4
5
6
7
8
9
10
Wi-Fi 연결 중 (파일 다운로드 50% 진행):
클라이언트 (192.168.1.10:54321) ◄────► 서버 (93.184.216.34:443)

Wi-Fi → LTE 전환:
클라이언트 (10.0.0.5:60000) ◄────► 서버 (93.184.216.34:443)
            ↑         ↑
         IP 변경   포트 변경

→ 서버 입장: "이 연결은 처음 보는 연결이다"
→ 다운로드 중단, 새로 연결, 처음부터 다시 시작

지하철에서 영상을 보다가 역에 도착해 Wi-Fi가 잡히면, 스트리밍이 끊기고 다시 로딩됩니다. 네트워크가 바뀔 때마다 이 문제가 발생합니다.


QUIC: UDP 위의 새로운 전송 계층

TCP의 한계를 해결하려면 TCP를 수정해야 합니다. 하지만 TCP는 운영체제 커널에 구현되어 있습니다. Windows, macOS, Linux, iOS, Android 등 전 세계 모든 운영체제를 업데이트해야 합니다. 현실적으로 불가능합니다.

Google은 다른 접근을 택했습니다. TCP 대신 UDP를 사용하는 것입니다.

UDP는 포트 번호와 체크섬만 제공합니다. 신뢰성 보장, 순서 보장, 혼잡 제어가 없습니다. 그래서 UDP 위에 원하는 기능을 자유롭게 구현할 수 있습니다. 그리고 이 구현은 커널이 아닌 애플리케이션 레벨에서 동작합니다. 브라우저나 서버 소프트웨어만 업데이트하면 됩니다.

이렇게 만들어진 것이 QUIC(Quick UDP Internet Connections)입니다. 2012년부터 개발되어 2021년 RFC 9000으로 표준화되었습니다.

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은 TCP처럼 신뢰성, 흐름 제어, 혼잡 제어를 제공합니다. 동시에 앞서 살펴본 TCP의 세 가지 한계를 해결합니다:

  • HOL Blocking: 스트림을 프로토콜 수준에서 지원하여 스트림 간 독립성 보장
  • 연결 설정 지연: TLS 1.3을 내장하여 핸드셰이크 통합
  • 연결 마이그레이션 불가: IP/포트 대신 Connection ID로 연결 식별

QUIC의 스트림 독립성

앞서 TCP에서는 패킷 B가 손실되면 D, E, G도 애플리케이션에 전달되지 않았습니다. TCP는 스트림을 모르기 때문입니다.

QUIC은 다릅니다. 스트림을 프로토콜 수준에서 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
서버                                         클라이언트
  │                                               │
  │ ─── [A:스트림1] ───►                          │ → 도착
  │ ─── [B:스트림1] ───X (손실)                   │
  │ ─── [D:스트림2] ───►                          │ → 도착
  │ ─── [E:스트림2] ───►                          │ → 도착
  │ ─── [G:스트림3] ───►                          │ → 도착
  │                                               │
  │                     QUIC 동작:                │
  │                     - 스트림 1: B 재전송 대기  │
  │                     - 스트림 2: D, E 즉시 전달 │
  │                     - 스트림 3: G 즉시 전달    │

QUIC은 D가 스트림 2, G가 스트림 3이라는 것을 압니다. B는 스트림 1이므로 D, G와 무관합니다. 스트림 1만 보류하고, 스트림 2, 3은 바로 애플리케이션에 전달합니다.


TCP와 비교:

  TCP QUIC
스트림 개념 없음 (하나의 바이트 흐름) 있음 (프로토콜 수준 지원)
순서 보장 전체 데이터에 대해 각 스트림 내에서만
패킷 손실 시 모든 데이터 대기 해당 스트림만 대기

무선 네트워크처럼 패킷 손실이 잦은 환경에서 이 차이가 큽니다. TCP는 손실이 발생할 때마다 모든 스트림이 멈추지만, QUIC은 손실된 스트림만 영향받습니다.


0-RTT 연결 설정

앞서 TCP + TLS는 연결 설정에 2~3 RTT가 필요하다고 했습니다. TCP 핸드셰이크가 끝나야 TLS 핸드셰이크를 시작할 수 있기 때문입니다.

QUIC은 TLS 1.3을 프로토콜에 내장했습니다. TCP 핸드셰이크 없이 첫 패킷부터 TLS 핸드셰이크를 시작합니다.


첫 연결: 1 RTT

1
2
3
4
5
6
7
8
클라이언트                                  서버
    │                                        │
    │ ─── ClientHello + 키 공유 ────────────►│
    │                                        │
    │ ◄── ServerHello + 키 공유 + 인증서 ─── │
    │                                        │
    │ ─── 완료 + HTTP 요청 ─────────────────►│ ← 1 RTT 후 데이터 전송
    │                                        │

TCP + TLS는 “TCP 연결 설정 → TLS 협상 → 데이터 전송” 순서였습니다. QUIC은 연결 설정과 암호화 협상을 동시에 진행합니다. 1 RTT 만에 암호화된 연결이 완료됩니다.


재연결: 0 RTT

첫 연결에서 클라이언트는 서버와 협상한 암호화 키를 저장해 둡니다. 같은 서버에 다시 연결할 때 이 키를 재사용합니다.

1
2
3
4
5
6
7
클라이언트                                  서버
    │                                        │
    │ ─── ClientHello + HTTP 요청 ──────────►│ ← 첫 패킷에 데이터 포함
    │     (저장해 둔 키로 암호화)              │
    │                                        │
    │ ◄── ServerHello + HTTP 응답 ────────── │
    │                                        │

첫 패킷에 HTTP 요청이 포함됩니다. 서버 응답을 기다리지 않고 바로 데이터를 보낼 수 있어서 “0 RTT”입니다. 1 RTT 연결에서는 요청을 보내기 전에 서버 응답(ServerHello)을 한 번 기다려야 했습니다.


0-RTT의 제약:

0-RTT는 핸드셰이크가 완료되기 전에 데이터를 보냅니다. 서버는 이 요청이 새 요청인지, 공격자가 이전 패킷을 캡처해서 다시 보낸 것인지 구분할 수 없습니다.

1
2
3
4
5
6
7
8
정상 요청:
클라이언트 ─── [0-RTT: 100원 송금] ───► 서버  → 100원 송금

리플레이 공격:
공격자 ─── [0-RTT: 100원 송금] (복사) ──► 서버  → 100원 송금
공격자 ─── [0-RTT: 100원 송금] (복사) ──► 서버  → 100원 송금
  ...
→ 10번 재전송하면 1000원 송금

그래서 0-RTT는 멱등성(idempotent) 요청에만 사용해야 합니다. 여러 번 실행해도 결과가 같은 요청입니다.

  • 사용 가능: GET /news (뉴스 조회), GET /profile (프로필 조회)
  • 사용 불가: POST /transfer (송금), POST /order (주문)

연결 마이그레이션

앞서 TCP는 IP 주소와 포트가 바뀌면 연결이 끊어진다고 했습니다. QUIC은 Connection ID라는 별도의 식별자를 사용해서 이 문제를 해결합니다.

1
2
3
4
5
6
7
TCP 연결 식별:
(출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트)
→ IP나 포트가 바뀌면 다른 연결

QUIC 연결 식별:
Connection ID (예: 0x1234abcd)
→ IP나 포트가 바뀌어도 같은 연결
1
2
3
4
5
6
7
8
9
10
Wi-Fi 연결 중 (파일 다운로드 50%):
클라이언트 (192.168.1.10:5000) ◄──► 서버
Connection ID: 0x1234abcd

Wi-Fi → LTE 전환:
클라이언트 (10.0.0.5:6789) ◄──► 서버
Connection ID: 0x1234abcd

→ 서버: "Connection ID가 같으니 같은 연결이다"
→ 다운로드 50%부터 계속 진행

TCP였다면 연결이 끊기고 처음부터 다시 시작해야 했습니다.


네트워크 전환은 모바일 환경에서 자주 발생합니다. 지하철에서 역마다 Wi-Fi가 바뀌거나, 건물에 들어가면서 LTE에서 Wi-Fi로 전환되거나, 이동 중에 기지국이 바뀌는 경우입니다. QUIC은 이런 상황에서 연결을 유지합니다.


HTTP/3

HTTP/3은 QUIC 위에서 동작하는 HTTP입니다. 2022년 RFC 9114로 표준화되었습니다.

1
2
3
4
5
6
7
8
9
HTTP/1.1 → HTTP/2 → HTTP/3
   │          │         │
   └──────────┴─────────┘
   의미(메서드, 상태 코드, 헤더)는 같음

전송 방식만 다름:
- HTTP/1.1: TCP + 텍스트
- HTTP/2:   TCP + 이진 프레이밍
- HTTP/3:   QUIC + 이진 프레이밍

GET, POST, 200, 404 같은 HTTP의 의미는 그대로입니다. 전송 계층이 TCP에서 QUIC으로 바뀌었습니다.


QPACK: HTTP/3의 헤더 압축

Part 1에서 HPACK의 동적 테이블을 살펴보았습니다. 헤더를 보내면서 테이블에 추가하고, 다음 요청에서 인덱스로 참조합니다.

1
2
3
HPACK (HTTP/2):
요청 1: Cookie: abc... → 테이블에 저장 (인덱스 62)
요청 2: [62]           → 인덱스로 참조

이 방식은 요청 1이 먼저 도착해야 합니다. 요청 2가 먼저 도착하면 인덱스 62가 뭔지 모릅니다.

TCP는 순서를 보장하므로 문제가 없었습니다. 하지만 QUIC의 스트림은 독립적입니다. 스트림 2가 스트림 1보다 먼저 도착할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
QPACK (HTTP/3):
┌─────────────────────────────────────────────┐
│ Encoder 스트림 (테이블 업데이트 전용)         │
│   → Cookie: abc... 를 인덱스 62로 저장       │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 요청 스트림 1: [62] 사용                     │
│ 요청 스트림 2: [62] 사용                     │
│ 요청 스트림 3: [62] 사용                     │
└─────────────────────────────────────────────┘

QPACK은 테이블 업데이트를 별도의 스트림으로 분리합니다. 요청 스트림들은 이 테이블을 참조만 합니다.

요청 스트림 2가 인덱스 62를 참조하는데, Encoder 스트림이 아직 도착하지 않았다면? 요청 스트림 2는 Encoder 스트림에서 인덱스 62가 정의될 때까지 대기합니다.

HPACK에서는 요청 1이 도착해야 요청 2를 처리할 수 있었습니다. QPACK에서는 Encoder 스트림만 도착하면 됩니다. 요청 스트림들 사이에는 순서 의존성이 없습니다.


HTTP/3 vs HTTP/2

지금까지 살펴본 내용을 정리하면:

특성 HTTP/2 HTTP/3
전송 프로토콜 TCP QUIC (UDP 기반)
HOL Blocking TCP 레벨에서 발생 없음 (스트림 독립)
연결 설정 2~3 RTT 1 RTT, 재연결 시 0 RTT
암호화 TLS 별도 TLS 1.3 내장
연결 마이그레이션 불가 가능 (Connection ID)
헤더 압축 HPACK QPACK

HTTP/3이 모든 면에서 개선되었지만, 항상 HTTP/3이 더 좋은 것은 아닙니다.


HTTP/3이 유리한 환경:

환경 HTTP/3의 이점
높은 지연 (모바일, 위성) 2~3 RTT → 1 RTT. RTT가 200ms면 400ms 절약
패킷 손실이 잦음 (무선) 손실된 스트림만 대기, 나머지는 계속 진행
네트워크 전환 (이동 중) Wi-Fi ↔ LTE 전환해도 연결 유지


HTTP/2가 적합한 환경:

환경 이유
안정적인 유선 네트워크 패킷 손실이 거의 없으면 HOL Blocking도 드묾
UDP 차단 환경 일부 기업 방화벽이 UDP 트래픽 차단
레거시 시스템 오래된 브라우저, QUIC 미지원 서버


대부분의 브라우저는 HTTP/3을 먼저 시도하고, 실패하면 HTTP/2로 자동 전환합니다.


WebSocket: 양방향 실시간 통신

HTTP는 요청-응답 모델입니다. 클라이언트가 요청해야 서버가 응답합니다. 서버가 먼저 데이터를 보낼 수 없습니다.

채팅 앱을 생각해 보세요. 상대방이 메시지를 보내면 내 화면에 바로 나타나야 합니다. 서버가 “새 메시지가 왔어”라고 먼저 알려줘야 합니다. 하지만 HTTP에서는 클라이언트가 물어봐야 알 수 있습니다.


폴링(Polling)

클라이언트가 주기적으로 서버에 요청합니다.

1
2
3
4
5
6
7
8
9
10
클라이언트                              서버
    │                                    │
    │ ─── 새 메시지 있어? ───►           │
    │             ◄─── 없어 ──────────── │
    │        (5초 대기)                  │
    │ ─── 새 메시지 있어? ───►           │
    │             ◄─── 없어 ──────────── │
    │        (5초 대기)                  │
    │ ─── 새 메시지 있어? ───►           │
    │             ◄─── 있어! [메시지] ── │

5초마다 요청하면 1분에 12번, 1시간에 720번 요청합니다. 사용자 1000명이면 시간당 72만 건입니다. 대부분 “없어”라는 응답을 받습니다. 서버 부하와 네트워크 낭비입니다.

그리고 메시지가 도착해도 최대 5초를 기다려야 알 수 있습니다. 실시간이 아닙니다.


롱 폴링(Long Polling)

폴링의 문제는 “없어”라는 헛된 응답이 많다는 것입니다. 롱 폴링은 메시지가 올 때까지 응답을 보류합니다.

1
2
3
4
5
6
7
8
9
10
클라이언트                              서버
    │                                    │
    │ ─── 새 메시지 있어? ───►           │
    │                                    │ (대기...)
    │                                    │ (30초 동안)
    │                                    │ 메시지 도착!
    │             ◄─── 있어! [메시지] ── │
    │                                    │
    │ ─── 새 메시지 있어? ───►           │ ← 즉시 다시 요청
    │                   ...              │

“없어”라는 헛된 응답이 없어졌습니다. 메시지가 오면 바로 받을 수 있습니다.

하지만 여전히 HTTP입니다:

  • 메시지를 받을 때마다 새 요청을 보내야 함
  • 요청마다 HTTP 헤더(수백 바이트)가 반복됨
  • 서버가 연결을 오래 열어두어야 함 (리소스 소모)

WebSocket 프로토콜


WebSocket 프로토콜

폴링과 롱 폴링은 HTTP 안에서 해결하려는 시도였습니다. WebSocket은 다른 접근입니다. HTTP의 요청-응답 모델을 버리고, 양방향 통신이 가능한 별도의 프로토콜을 사용합니다.

WebSocket은 2011년 RFC 6455로 표준화되었습니다.


핸드셰이크: HTTP에서 WebSocket으로

WebSocket은 HTTP 요청으로 시작합니다. 기존 웹 인프라(프록시, 방화벽)를 통과하기 위해서입니다.

1
2
3
4
5
6
7
8
9
10
11
클라이언트                              서버
    │                                    │
    │ ─── GET /chat HTTP/1.1 ───────────►│
    │     Upgrade: websocket             │
    │     Connection: Upgrade            │
    │                                    │
    │ ◄── HTTP/1.1 101 Switching ─────── │
    │     Protocols                      │
    │                                    │
    │ ══════ WebSocket 연결 ════════════ │
    │        (더 이상 HTTP 아님)          │

핵심 헤더:

  • Upgrade: websocket: “WebSocket으로 전환하자”
  • 101 Switching Protocols: “좋아, 프로토콜 바꾸자”

이 응답 이후 같은 TCP 연결이 WebSocket 프로토콜로 전환됩니다.


양방향 통신

핸드셰이크 이후, HTTP의 요청-응답 구조가 사라집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP:
클라이언트 ─── 요청 ───► 서버
           ◄── 응답 ────
           ─── 요청 ───►
           ◄── 응답 ────
(항상 클라이언트가 먼저, 서버는 응답만)

WebSocket:
클라이언트 ─── 메시지 ──► 서버
           ◄── 메시지 ───
           ◄── 메시지 ───  ← 서버가 먼저 전송
           ─── 메시지 ──►
           ◄── 메시지 ───
(어느 쪽이든 언제든 전송 가능)

HTTP에서 서버는 클라이언트 요청이 있어야만 응답할 수 있었습니다. WebSocket에서는 서버가 클라이언트의 요청 없이 먼저 데이터를 보낼 수 있습니다. 채팅에서 상대방이 보낸 메시지, 주식 앱에서 변동된 시세를 서버가 즉시 전달할 수 있습니다.


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비트): 메시지의 마지막 프레임인지 표시. 큰 메시지는 여러 프레임으로 나눠 보낼 수 있음
  • RSV1, RSV2, RSV3 (각 1비트): 예약 필드. 확장 기능에서 사용하며, 기본값은 0
  • Opcode (4비트): 프레임 유형
    • 0x1: 텍스트 데이터
    • 0x2: 바이너리 데이터
    • 0x8: 연결 종료 요청
    • 0x9: Ping (연결 확인)
    • 0xA: Pong (Ping 응답)
  • MASK (1비트): 페이로드 마스킹 여부. 클라이언트→서버는 필수
  • Payload length (7비트): 데이터 길이. 126 이상이면 뒤따르는 2바이트 또는 8바이트에 실제 길이 표시
  • Masking-key (4바이트): MASK가 1이면 존재. 페이로드를 XOR 연산으로 마스킹하는 데 사용


마스킹

클라이언트에서 서버로 보내는 모든 프레임은 마스킹이 필수입니다. 서버에서 클라이언트로 보내는 프레임은 마스킹하지 않습니다.

마스킹은 중간에 있는 프록시가 WebSocket 데이터를 HTTP로 잘못 해석하는 것을 방지합니다. 악의적인 클라이언트가 HTTP처럼 보이는 데이터를 보내 프록시 캐시를 오염시키는 공격을 막기 위한 것입니다.

1
2
3
4
5
6
7
8
마스킹 계산:
masked[i] = original[i] XOR masking_key[i % 4]

예시 (masking_key = [0x37, 0xfa, 0x21, 0x3d]):
원본:    H    e    l    l    o
        0x48 0x65 0x6c 0x6c 0x6f
XOR:    0x37 0xfa 0x21 0x3d 0x37  (키 반복)
결과:   0x7f 0x9f 0x4d 0x51 0x58

WebSocket 사용 사례

WebSocket은 양쪽이 빈번하게 메시지를 주고받는 경우에 적합합니다. 앞서 살펴본 폴링은 주기적으로 요청해야 하고, 롱 폴링도 메시지를 받을 때마다 새 요청이 필요했습니다. WebSocket은 한 번 연결하면 추가 요청 없이 계속 주고받을 수 있습니다.

사용 사례 예시 WebSocket이 적합한 이유
실시간 채팅 카카오톡, 슬랙 메시지 송신과 수신이 모두 빈번. 폴링으로는 지연 발생, 롱 폴링으로는 요청이 너무 많음
실시간 게임 브라우저 게임 플레이어 위치, 행동을 초당 수십 번 동기화. 매번 HTTP 요청은 오버헤드가 큼
협업 도구 Google Docs 편집 내용을 즉시 전송하고, 다른 사람의 편집도 즉시 수신
주식/가상화폐 시세 거래소 앱 초당 여러 번 가격 업데이트. 단방향이지만 빈도가 높아 SSE보다 효율적일 수 있음

Server-Sent Events (SSE)

서버에서 클라이언트로만 데이터를 보내면 되는 경우가 있습니다. 뉴스 피드, 알림, 로그 스트리밍 같은 경우입니다. 양방향 통신이 필요 없다면 WebSocket 대신 SSE(Server-Sent Events)를 사용할 수 있습니다.

SSE는 일반 HTTP 응답입니다. WebSocket처럼 프로토콜을 전환하지 않습니다. 응답이 끝나지 않고 계속 스트리밍된다는 점만 다릅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
클라이언트                              서버
    │                                    │
    │ ─── GET /events ───────────────►   │
    │                                    │
    │             ◄─── HTTP 200 ──────── │
    │                  Content-Type:     │
    │                  text/event-stream │
    │                                    │
    │             ◄─── data: 새 글 ───── │
    │                  (시간 경과)        │
    │             ◄─── data: 댓글 ────── │
    │                  (시간 경과)        │
    │             ◄─── data: 좋아요 ──── │
    │                   ...              │

HTTP를 그대로 사용하므로 기존 프록시, 로드밸런서, 인증 시스템을 별도 설정 없이 활용할 수 있습니다.


WebSocket vs SSE:

  WebSocket SSE
방향 양방향 서버 → 클라이언트만
프로토콜 WebSocket (별도 프로토콜) HTTP
데이터 형식 텍스트, 바이너리 텍스트만
재연결 직접 구현 필요 브라우저가 자동 처리
인프라 호환성 프록시/방화벽 설정 필요할 수 있음 기존 HTTP 인프라 그대로 사용

선택 기준:

  • 클라이언트도 데이터를 보내야 하면 → WebSocket
  • 서버에서 클라이언트로만 보내면 → SSE가 더 간단

HTTP의 미래

HTTP/3 보급

HTTP/3은 이미 널리 사용되고 있습니다. Google, Cloudflare, Meta 등 주요 서비스가 HTTP/3을 지원하고, Chrome, Firefox, Safari, Edge 등 주요 브라우저도 기본 활성화되어 있습니다. 브라우저는 HTTP/3을 먼저 시도하고, 실패하면 HTTP/2로 자동 전환합니다.


WebTransport

WebSocket은 TCP 위에서 동작합니다. 앞서 살펴본 TCP HOL Blocking 문제가 WebSocket에도 적용됩니다. 여러 메시지를 동시에 보내도 하나가 손실되면 나머지가 대기합니다.

WebTransport는 QUIC 위에서 양방향 통신을 제공합니다. QUIC의 스트림 독립성 덕분에 메시지 간 독립적 전송이 가능합니다. 연결 마이그레이션도 지원하여 네트워크 전환 시에도 연결이 유지됩니다. WebSocket의 다음 세대라고 볼 수 있습니다.


마무리

이 글에서 살펴본 내용:

기술 핵심
TCP의 한계 HOL Blocking, 연결 설정 지연(2~3 RTT), IP 변경 시 연결 끊김
QUIC UDP 위에 구현. 스트림 독립성, 1/0 RTT 연결, Connection ID
HTTP/3 QUIC 위의 HTTP. 전송 방식만 다르고 의미는 동일
WebSocket HTTP 업그레이드 후 양방향 통신. 별도 프로토콜
SSE HTTP로 서버→클라이언트 단방향 스트리밍

TCP는 40년 넘게 인터넷의 기반이었습니다. 하지만 모바일과 무선 네트워크가 보편화되면서 TCP의 한계가 드러났고, 이를 해결하기 위해 QUIC이 등장했습니다. WebSocket도 마찬가지입니다. TCP 위에서 동작하는 WebSocket의 한계를 넘어 WebTransport가 등장하고 있습니다.

네트워크 환경이 변하면 프로토콜도 변합니다.



관련 글

Tags: HTTP3, HTTP, QUIC, WebSocket, 네트워크

Categories: ,