QUIC과 전송 계층의 진화
HTTP/2가 TCP 위에서 부딪힌 HOL 블로킹을 손실 상황의 모형으로 확인하고, QUIC이 UDP 위에 스트림과 TLS 1.3, 손실 복구, 연결 이동을 함께 담아 이를 줄이는 방식을 정리합니다. 연결 수립의 왕복 수와 0-RTT의 위험, HTTP/3를 확인하는 방법, QUIC을 쓸 때 따져 볼 점과 네 가지 전송 방식도 함께 봅니다.
TCP는 수십 년 동안 인터넷의 데이터를 실어 날라 왔습니다. 하지만 웹 페이지 하나가 수십 개의 파일을 동시에 받고, 스마트폰이 와이파이와 LTE를 오가는 시대가 되면서 TCP 위에 기능을 덧붙이는 방식으로는 풀기 어려운 문제가 드러났습니다. QUIC은 이를 풀려고 UDP 위에 새로 만든 전송 프로토콜입니다. UDP를 쓰지만 "UDP 위에 다시 만든 TCP"에 그치지 않고, 여러 스트림, TLS 1.3 암호화, 손실 복구, 혼잡 제어, 연결 이동을 하나로 묶었습니다.
HTTP가 TCP에 부딪힌 한계
HTTP/1.1은 연결 하나에서 요청을 하나씩 차례로 처리하므로, 브라우저는 서버마다 연결을 여러 개 열어 병렬로 받았습니다. HTTP/2는 연결 하나 안에 여러 스트림을 섞어 실어 이 문제를 풀었습니다. 그런데 그 연결이 TCP라는 점은 그대로였습니다. 7장 2편에서 본 것처럼 TCP는 바이트 스트림 하나를 순서대로 넘기므로, 한 스트림의 데이터를 실은 세그먼트 하나가 사라지면 그 뒤에 도착한 다른 스트림의 데이터도 함께 기다려야 합니다. HTTP/2는 HTTP 수준의 줄 서기를 없앴지만, TCP 수준의 HOL 블로킹은 없애지 못한 것입니다.
QUIC은 스트림마다 따로 순서를 맞춥니다. 스트림 A의 앞부분이 사라지면 A는 기다리지만, 필요한 데이터가 다 도착한 스트림 B는 바로 애플리케이션에 넘길 수 있습니다.
한 스트림의 손실이 막는 전달 범위
앞 순서에 필요한 데이터가 손실됩니다. 독립적인 B의 필요 데이터가 모두 도착했습니다. 각 단계의 값과 전환 이유를 비교합니다.
QUIC · 손실의 전달 범위
한 스트림의 빈 구간이 다른 스트림의 전달까지 막을까
A의 앞 데이터가 손실되고 A의 뒤 데이터와 B의 필요한 데이터가 도착한 가정입니다. QUIC은 스트림별 순서를 유지하므로 A가 기다리는 동안에도 B는 전달될 수 있습니다.
점선은 앞 구간이 비어서 기다리는 관계, 실선은 도착한 바이트와 전달 상태의 관계입니다. 패킷 이동 경로가 아닙니다. B는 필요한 바이트가 갖춰지고 A의 업무 결과에 의존하지 않는 조건입니다.
| 같은 손실 가정 | HTTP/2 over TCP | HTTP/3 over QUIC |
|---|---|---|
| A 앞 데이터 손실 | TCP 바이트 순서의 빈 구간이 생김 | A 스트림의 offset 빈 구간이 생김 |
| A 뒤 데이터 도착 | 손실 복구까지 순서 전달 대기 | A의 손실 복구까지 순서 전달 대기 |
| B 데이터 도착 | 같은 TCP의 빈 구간 뒤 바이트라면 함께 대기 | B에 필요한 데이터가 갖춰지면 전달 가능 |
- A 앞 데이터 손실
- HTTP/2 over TCP: TCP 바이트 순서의 빈 구간이 생김HTTP/3 over QUIC: A 스트림의 offset 빈 구간이 생김
- A 뒤 데이터 도착
- HTTP/2 over TCP: 손실 복구까지 순서 전달 대기HTTP/3 over QUIC: A의 손실 복구까지 순서 전달 대기
- B 데이터 도착
- HTTP/2 over TCP: 같은 TCP의 빈 구간 뒤 바이트라면 함께 대기HTTP/3 over QUIC: B에 필요한 데이터가 갖춰지면 전달 가능
가정한 전달 순서의 비교이며 실행 추적이 아닙니다. 한 QUIC 패킷에 여러 스트림 데이터가 들어 있으면 그 패킷 손실은 모두에 영향을 주며, 혼잡 제어와 애플리케이션 의존성도 남습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 A 앞 손실부터 B 필요 데이터까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | TCP에서는 앞의 빈 바이트 구간 때문에 뒤에 도착한 A와 B 바이트의 순서 전달이 대기합니다. QUIC에서는 A의 빈 offset 구간이 A 전달을 막지만 필요 바이트가 갖춰진 독립 B 스트림은 전달 가능합니다. 상자는 버퍼 상태이고 화살표는 조건에 따른 앱 전달 관계이며 실제 네트워크 홉이 아닙니다. |
| 표의 값 | A 앞 데이터 손실 — HTTP/2 over TCP: TCP 바이트 순서의 빈 구간이 생김; HTTP/3 over QUIC: A 스트림의 offset 빈 구간이 생김. A 뒤 데이터 도착 — HTTP/2 over TCP: 손실 복구까지 순서 전달 대기; HTTP/3 over QUIC: A의 손실 복구까지 순서 전달 대기. B 데이터 도착 — HTTP/2 over TCP: 같은 TCP의 빈 구간 뒤 바이트라면 함께 대기; HTTP/3 over QUIC: B에 필요한 데이터가 갖춰지면 전달 가능. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| A 앞 손실 앞 순서에 필요한 데이터가 손실됩니다. |
|
|
| A 뒤 도착 A의 뒷부분이 와도 앞의 빈 구간은 남습니다. |
|
|
| B 필요 데이터 독립적인 B의 필요 데이터가 모두 도착했습니다. |
|
|
한 스트림의 손실이 막는 전달 범위
앞 순서에 필요한 데이터가 손실됩니다. 독립적인 B의 필요 데이터가 모두 도착했습니다. 각 단계의 값과 전환 이유를 비교합니다.
QUIC · 손실의 전달 범위
한 스트림의 빈 구간이 다른 스트림의 전달까지 막을까
A의 앞 데이터가 손실되고 A의 뒤 데이터와 B의 필요한 데이터가 도착한 가정입니다. QUIC은 스트림별 순서를 유지하므로 A가 기다리는 동안에도 B는 전달될 수 있습니다.
점선은 앞 구간이 비어서 기다리는 관계, 실선은 도착한 바이트와 전달 상태의 관계입니다. 패킷 이동 경로가 아닙니다. B는 필요한 바이트가 갖춰지고 A의 업무 결과에 의존하지 않는 조건입니다.
| 같은 손실 가정 | HTTP/2 over TCP | HTTP/3 over QUIC |
|---|---|---|
| A 앞 데이터 손실 | TCP 바이트 순서의 빈 구간이 생김 | A 스트림의 offset 빈 구간이 생김 |
| A 뒤 데이터 도착 | 손실 복구까지 순서 전달 대기 | A의 손실 복구까지 순서 전달 대기 |
| B 데이터 도착 | 같은 TCP의 빈 구간 뒤 바이트라면 함께 대기 | B에 필요한 데이터가 갖춰지면 전달 가능 |
- A 앞 데이터 손실
- HTTP/2 over TCP: TCP 바이트 순서의 빈 구간이 생김HTTP/3 over QUIC: A 스트림의 offset 빈 구간이 생김
- A 뒤 데이터 도착
- HTTP/2 over TCP: 손실 복구까지 순서 전달 대기HTTP/3 over QUIC: A의 손실 복구까지 순서 전달 대기
- B 데이터 도착
- HTTP/2 over TCP: 같은 TCP의 빈 구간 뒤 바이트라면 함께 대기HTTP/3 over QUIC: B에 필요한 데이터가 갖춰지면 전달 가능
가정한 전달 순서의 비교이며 실행 추적이 아닙니다. 한 QUIC 패킷에 여러 스트림 데이터가 들어 있으면 그 패킷 손실은 모두에 영향을 주며, 혼잡 제어와 애플리케이션 의존성도 남습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 A 앞 손실부터 B 필요 데이터까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | TCP에서는 앞의 빈 바이트 구간 때문에 뒤에 도착한 A와 B 바이트의 순서 전달이 대기합니다. QUIC에서는 A의 빈 offset 구간이 A 전달을 막지만 필요 바이트가 갖춰진 독립 B 스트림은 전달 가능합니다. 상자는 버퍼 상태이고 화살표는 조건에 따른 앱 전달 관계이며 실제 네트워크 홉이 아닙니다. |
| 표의 값 | A 앞 데이터 손실 — HTTP/2 over TCP: TCP 바이트 순서의 빈 구간이 생김; HTTP/3 over QUIC: A 스트림의 offset 빈 구간이 생김. A 뒤 데이터 도착 — HTTP/2 over TCP: 손실 복구까지 순서 전달 대기; HTTP/3 over QUIC: A의 손실 복구까지 순서 전달 대기. B 데이터 도착 — HTTP/2 over TCP: 같은 TCP의 빈 구간 뒤 바이트라면 함께 대기; HTTP/3 over QUIC: B에 필요한 데이터가 갖춰지면 전달 가능. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| A 앞 손실 앞 순서에 필요한 데이터가 손실됩니다. |
|
|
| A 뒤 도착 A의 뒷부분이 와도 앞의 빈 구간은 남습니다. |
|
|
| B 필요 데이터 독립적인 B의 필요 데이터가 모두 도착했습니다. |
|
|
다음 코드는 이 차이를 모형으로 확인합니다. 스트림 A의 앞부분, A의 뒷부분, 스트림 B를 각각 실은 패킷 세 개 가운데 1번이 사라졌습니다. TCP는 셋을 하나의 바이트 순서로 싣고, QUIC은 스트림마다 스트림 안의 위치(offset)로 순서를 맞춥니다.
packets = [ # (패킷 번호, 스트림, 스트림 안의 offset, 데이터)
(1, "A", 0, b"A-head|"),
(2, "A", 7, b"A-tail"),
(3, "B", 0, b"B-all"),
]
def tcp_deliverable(arrived):
stream = b""
for num, _, _, data in packets: # TCP는 모든 스트림을 하나의 바이트 순서에 싣는다
if num not in arrived:
break # 빈 곳이 생기면 그 뒤는 모두 기다린다
stream += data
return stream
def quic_deliverable(arrived):
result = {}
for name in ["A", "B"]:
expected, data = 0, b""
chunks = sorted((off, d) for num, s, off, d in packets if s == name and num in arrived)
for off, d in chunks:
if off != expected:
break # 이 스트림 안에서만 기다린다
data += d
expected += len(d)
result[name] = data
return result
arrived = {2, 3} # 1번 패킷이 사라졌다
print("TCP가 넘길 수 있는 것:", tcp_deliverable(arrived))
print("QUIC이 넘길 수 있는 것:", quic_deliverable(arrived))
arrived.add(1) # 1번 패킷의 내용이 다시 보내져 도착했다
print("재전송 뒤 TCP:", tcp_deliverable(arrived))
print("재전송 뒤 QUIC:", quic_deliverable(arrived))TCP가 넘길 수 있는 것: b''
QUIC이 넘길 수 있는 것: {'A': b'', 'B': b'B-all'}
재전송 뒤 TCP: b'A-head|A-tailB-all'
재전송 뒤 QUIC: {'A': b'A-head|A-tail', 'B': b'B-all'}1번 패킷이 사라진 동안 TCP는 아무것도 넘기지 못하지만, QUIC은 A만 기다리게 하고 B는 바로 넘깁니다. 재전송이 도착하면 둘 다 모든 데이터를 넘기므로, 차이는 손실을 복구하는 동안 누가 기다리느냐에 있습니다. 다만 QUIC 패킷 하나에 여러 스트림의 데이터가 함께 실렸다면 그 패킷의 손실은 모든 스트림에 영향을 줍니다. 또 RFC 9000 §2.2에 따르면 QUIC 스트림도 TCP처럼 바이트 스트림이어서, 메시지의 경계는 여전히 애플리케이션이 정해야 합니다.
QUIC은 무엇을 하나로 묶었나
RFC 9000은 QUIC의 전송 기능을, RFC 9001은 TLS를 쓰는 방법을, RFC 9002는 손실 복구와 혼잡 제어를 정의하며, 그 위의 HTTP/3는 RFC 9114에 있습니다.
| 기능 | TCP + TLS | QUIC |
|---|---|---|
| 연결 수립 | TCP 핸드셰이크가 끝난 뒤 TLS 핸드셰이크 | 전송과 TLS 1.3의 핸드셰이크를 한 번에 |
| 다시 연결할 때 | 다시 핸드셰이크 | 조건이 맞으면 첫 패킷부터 데이터를 보내는 0-RTT |
| HOL 블로킹 | 세그먼트 하나의 손실이 모든 데이터를 막는다 | 손실된 데이터가 속한 스트림만 기다린다 |
| 암호화 범위 | TCP 헤더는 그대로 보이고 데이터만 보호 | 패킷 번호를 포함한 대부분이 보호되고 일부 헤더만 보인다 |
| 연결 식별 | IP와 포트의 4-tuple | Connection ID |
| 구현 위치 | 주로 운영체제 커널 | 주로 애플리케이션의 라이브러리 |
마지막 두 행이 QUIC이 UDP 위에 만들어진 이유와 이어집니다. TCP는 운영체제 커널에 들어 있어 새 기능을 넣으려면 전 세계의 운영체제가 바뀌어야 하고, 중간의 방화벽과 NAT 장비가 TCP 헤더를 들여다보며 낯선 옵션을 지우거나 패킷을 버리기 때문에 새 기능이 퍼지기 어렵습니다. QUIC은 대부분의 망에서 통과되는 UDP 위에서 애플리케이션이 직접 구현하므로 브라우저나 서버 라이브러리만 바꾸면 되고, 헤더 대부분을 암호화해 중간 장비가 들여다보고 굳혀 버릴 여지를 줄였습니다.
더 깊이: QUIC이 굳지 않게 하는 장치
중간 장비가 헤더의 모양에 기대 동작하는 바람에 프로토콜을 바꿀 수 없게 되는 현상을 경직화(ossification)라고 합니다. QUIC은 이를 되풀이하지 않으려고, 버전이 바뀌어도 변하지 않는 부분을 RFC 8999에 따로 적어 두었습니다. 첫 바이트의 맨 위 비트로 긴 헤더와 짧은 헤더를 가르는 것, 4바이트 버전 필드, 길이가 정해지지 않은 Connection ID, 버전 필드가 0인 버전 협상 패킷 정도가 전부입니다. 문서는 이 밖의 모든 것이 버전마다 바뀔 수 있고, 버전 1을 보고 세운 가정은 틀릴 수 있다고 분명히 적어 둡니다.
이 약속을 실제로 시험하려고 만든 것이 QUIC 버전 2(RFC 9369)입니다. 기능은 버전 1과 같지만, 첫 패킷을 보호하는 키를 만들 때 쓰는 솔트(salt)와 레이블, 긴 헤더의 패킷 종류 번호를 바꾸어 두었습니다. 버전 번호도 2가 아니라 해시에서 얻은 0x6b3343cf로, 숫자를 예측해 특별히 다루는 장비가 생기지 않게 했습니다. 버전 1의 모양에만 맞춰 만든 장비는 버전 2에서 문제를 드러내므로, 버전을 바꾸는 길이 굳기 전에 실제로 쓰이게 하려는 것입니다. 두 버전은 RFC 9368의 호환 버전 협상으로 왕복을 더 쓰지 않고 바꿀 수 있습니다.
연결 수립의 왕복 수
TCP 위의 TLS 1.3은 먼저 TCP 핸드셰이크로 한 번, 이어서 TLS 핸드셰이크로 한 번 왕복한 뒤에야 첫 요청을 보낼 수 있습니다. QUIC은 첫 패킷에 TLS의 첫 메시지를 함께 실어 한 번의 왕복으로 둘을 끝냅니다. 같은 서버에 다시 연결할 때는 이전 연결에서 받아 둔 정보로 첫 패킷부터 요청을 실어 보내는 0-RTT도 쓸 수 있습니다. DNS 조회, 인증서 검증, 서버의 처리 시간을 빼고 왕복만 세면 다음과 같습니다.
| 방식 | 첫 요청을 보내기까지 | 첫 응답을 받기까지 | RTT 40ms일 때 | RTT 200ms일 때 |
|---|---|---|---|---|
| TCP + TLS 1.3 | 2 RTT | 3 RTT | 120ms | 600ms |
| QUIC 새 연결 | 1 RTT | 2 RTT | 80ms | 400ms |
| QUIC 0-RTT | 0 RTT | 1 RTT | 40ms | 200ms |
RTT가 긴 해외 서버나 모바일 망일수록 차이가 커집니다. 다만 0-RTT의 데이터에는 위험이 있습니다. 0-RTT로 보낸 패킷은 공격자가 가로채 서버에 다시 보내는 재전송 공격(replay attack)을 막을 수 없으므로, 같은 요청이 두 번 처리되어도 문제없는 요청에만 써야 합니다. 서버는 이른 데이터(early data)의 처리를 핸드셰이크가 끝날 때까지 미루거나, RFC 8470의 425 Too Early 응답으로 거절할 수 있습니다. 0-RTT라는 이름은 응답이 0ms에 온다는 뜻이 아니라, 요청을 보내기 전에 기다리는 왕복이 없다는 뜻입니다.
연결 이동
TCP 연결은 4-tuple로 구별되므로, 스마트폰이 와이파이에서 LTE로 바뀌어 IP 주소가 달라지면 연결이 끊기고 처음부터 다시 맺어야 합니다. QUIC은 연결을 Connection ID로 구별하므로, 주소가 바뀌어도 같은 연결의 패킷임을 알아볼 수 있습니다. 새 경로로 패킷이 오면 서버는 그 주소가 정말 응답할 수 있는지 PATH_CHALLENGE와 PATH_RESPONSE 프레임으로 확인하고, 확인을 마치기 전에는 그 주소에서 받은 양의 세 배까지만 보냅니다(RFC 9000 §8.2, §9.3). 남의 주소를 사칭해 트래픽을 그쪽으로 쏟아지게 하는 공격을 막기 위해서입니다.
경로를 옮길 때는 Connection ID도 상대가 미리 나누어 준 새 값으로 바꿔야 합니다(RFC 9000 §9.5). 같은 ID를 계속 쓰면 바깥에서 두 경로의 트래픽이 같은 사용자의 것임을 알아챌 수 있기 때문입니다. 연결 이동은 이렇게 ID를 바꿔 가며 이루어지며, 서버 구현이나 중간의 로드 밸런서가 이를 지원하지 않으면 연결은 여전히 끊길 수 있습니다.
HTTP/3 확인하기
브라우저는 처음에 TCP로 접속했다가, 서버가 응답 헤더의 Alt-Svc(RFC 7838)로 HTTP/3를 쓸 수 있다고 알려 주면 다음 연결부터 QUIC을 시도합니다. DNS의 HTTPS 레코드(RFC 9460)로 미리 알려 두었다면 TCP를 거치지 않고 첫 연결부터 QUIC을 시도할 수도 있습니다. HTTP/3를 지원하는 서버는 다음과 같은 헤더를 돌려줍니다. h3는 HTTP/3를, ":443"은 UDP 443번 포트를, ma는 이 정보를 기억해 둘 초를 뜻합니다.
alt-svc: h3=":443"; ma=86400명령줄에서는 HTTP/3를 지원하도록 빌드한 curl로 확인합니다. 세 줄은 차례로 서버가 alt-svc로 HTTP/3를 알리는지, HTTP/3를 시도했을 때 실제로 어떤 버전으로 응답을 받았는지, HTTP/3만 허용했을 때도 성공하는지를 봅니다. HTTP/3 지원 없이 빌드된 curl은 --http3 옵션을 받아들이지 않고 오류를 내므로, curl -V의 Features 줄에 HTTP3가 있는지 먼저 확인합니다.
curl -sI https://www.example.com | grep -i '^alt-svc' # 서버가 HTTP/3를 알리는지
curl -s -o /dev/null -w '%{http_version}\n' --http3 https://www.example.com # 실제로 쓴 HTTP 버전
curl -s -o /dev/null -w '%{http_version}\n' --http3-only https://www.example.com # HTTP/3가 안 되면 실패--http3는 HTTP/3 연결이 실패하거나 느리면 이전 버전으로 넘어가므로, 명령이 성공했다고 HTTP/3를 쓴 것은 아닙니다. -w '%{http_version}'이 찍는 값이 3인지 확인하거나, 넘어가지 않는 --http3-only로 시험합니다. 브라우저에서는 개발자 도구의 네트워크 탭에 프로토콜 열을 켜면 h3인지 볼 수 있습니다.
QUIC을 쓸 때 따져 볼 것
QUIC이 언제나 TCP보다 빠른 것은 아닙니다.
| 환경 | 기대할 수 있는 효과 | 주의할 점 |
|---|---|---|
| RTT가 긴 경로 | 핸드셰이크의 왕복이 줄어든 효과가 크다 | 0-RTT는 재전송 공격에 안전한 요청에만 쓴다 |
| 손실이 있는 망 | 스트림끼리 서로 막지 않는다 | 손실 복구와 혼잡 제어 구현의 품질에 따라 다르다 |
| 와이파이와 LTE를 오가는 모바일 | 연결 이동으로 끊김이 줄어든다 | 서버와 로드 밸런서가 연결 이동을 지원해야 한다 |
| RTT가 짧은 데이터센터 안 | 왕복이 줄어든 효과가 작다 | 사용자 공간 구현과 패킷마다의 암호화로 CPU를 더 쓸 수 있다 |
| 회사나 학교의 망 | 해당 없음 | UDP 443이 막히거나 속도가 제한될 수 있어 TCP로 넘어가는 길이 필요하다 |
운영 쪽의 부담도 있습니다. 헤더가 암호화되어 있어 패킷 캡처만으로는 재전송이나 지연을 분석하기 어려우므로, 구현이 남기는 qlog 같은 기록이나 TLS 키를 따로 받아 복호화하는 방법을 써야 합니다. 로드 밸런서도 4-tuple 대신 Connection ID를 보고 같은 서버로 보내야 연결 이동이 동작합니다.
누구에게 보내는가
마지막으로 전송 계층과 함께 자주 나오는 네 가지 전송 방식을 정리합니다. 전송 계층만의 기능이라기보다 IP 주소와 라우팅, 링크 계층이 함께 정하는 방식입니다.
| 방식 | 받는 쪽 | 쓰는 곳 |
|---|---|---|
| 유니캐스트(unicast) | 특정한 하나 | 웹, 이메일, 파일 전송 등 대부분의 통신 |
| 브로드캐스트(broadcast) | 같은 네트워크의 모두 | ARP, DHCP의 서버 찾기 |
| 멀티캐스트(multicast) | 그룹에 가입한 장치들 | IPTV, 사내망의 실시간 배포 |
| 애니캐스트(anycast) | 같은 주소를 쓰는 여러 곳 가운데 라우팅이 고른 하나 | DNS 루트 서버, CDN |
브로드캐스트와 멀티캐스트는 한 번 보내 여러 곳에 닿아야 하므로 연결이 없는 UDP와 함께 쓰입니다. TCP는 양 끝 둘 사이의 연결이라 이런 방식에 맞지 않습니다. 멀티캐스트는 같은 데이터를 네트워크가 갈라지는 곳에서만 복제하므로 효율적이지만, 공용 인터넷에서는 대부분 지원하지 않아 주로 한 기관의 망 안에서 씁니다. 애니캐스트는 5장 2편의 BGP로 같은 프리픽스를 여러 곳에서 알려, 사용자가 라우팅상 가장 가까운 곳으로 가게 하는 방식으로, 다음 장의 DNS에서 다시 만납니다.
정리
- HTTP/2는 TCP 연결 하나에 여러 스트림을 실었지만, TCP가 바이트 스트림 하나를 순서대로 넘기므로 세그먼트 하나의 손실이 모든 스트림을 막는 HOL 블로킹이 남았습니다.
- QUIC은 UDP 위에서 스트림마다 따로 순서를 맞추므로, 손실된 데이터가 속한 스트림만 기다리고 나머지는 바로 넘깁니다.
- QUIC은 전송과 TLS 1.3의 핸드셰이크를 합쳐 새 연결에서 1 RTT, 다시 연결할 때는 0-RTT로 요청을 보낼 수 있지만, 0-RTT는 재전송 공격 때문에 중복 처리되어도 되는 요청에만 씁니다.
- Connection ID와 경로 확인으로 주소가 바뀌어도 연결을 이어 갈 수 있고, 사용자 공간 구현과 헤더 암호화로 새 기능을 빠르게 배포할 수 있습니다.
- HTTP/3는
Alt-Svc나 HTTPS 레코드로 알려지며, curl의%{http_version}이나 브라우저의 프로토콜 열로 실제 사용 여부를 확인합니다. - UDP가 막힌 망, CPU 비용, 분석의 어려움 때문에 QUIC을 쓰는 서비스는 TCP로 넘어가는 길을 함께 둡니다.
연습 문제
1. 한 패킷에 두 스트림이 실렸다면
손실된 QUIC 패킷 하나에 스트림 A의 앞부분과 스트림 B의 앞부분이 함께 실려 있었고, 그 뒤 A와 B의 뒷부분만 도착했습니다. "스트림이 다르므로 B는 바로 넘길 수 있다"고 말할 수 있나요?
해설 보기
말할 수 없습니다. 이 경우에는 B의 앞부분도 사라졌으므로 B도 그 구간이 재전송될 때까지 기다립니다. 스트림을 나누었다고 해서 같은 패킷에 실린 데이터의 손실까지 피할 수 있는 것은 아닙니다. QUIC이 줄여 주는 것은 다른 스트림의 손실 때문에 기다리는 일이며, 본문의 모형처럼 B 자체에는 빈 곳이 없을 때 B를 바로 넘길 수 있습니다. 혼잡 제어는 연결 전체에 하나이므로, 손실로 줄어든 전송 속도는 모든 스트림이 함께 겪습니다.
2. 0-RTT에 실어도 되는 요청
다시 방문한 사용자의 브라우저가 0-RTT로 (가) 뉴스 목록을 가져오는 GET 요청과 (나) 계좌 이체를 실행하는 POST 요청을 보내려 합니다. 각각을 0-RTT로 받아들여도 되나요? 서버는 어떻게 대응할 수 있나요?
해설 보기
0-RTT 데이터는 공격자가 복사해 다시 보낼 수 있습니다. (가)는 두 번 처리되어도 같은 목록을 한 번 더 돌려줄 뿐이므로 받아들여도 대개 괜찮습니다. (나)는 다시 보내진 요청이 그대로 처리되면 이체가 두 번 일어날 수 있으므로 0-RTT로 처리하면 안 됩니다. 서버는 이런 요청의 처리를 핸드셰이크가 끝난 뒤로 미루거나 425 Too Early로 거절해, 클라이언트가 핸드셰이크를 마친 뒤 다시 보내게 합니다. 다만 메서드 이름만으로 판단하면 안 됩니다. GET이라도 부작용이 있는 요청이라면 같은 기준으로 막아야 합니다.
3. UDP가 막힌 망
RTT가 80ms인 경로에서 새 연결의 첫 응답을 받기까지, TCP + TLS 1.3과 QUIC 새 연결은 각각 얼마나 걸리나요? UDP가 막힌 회사 망에서 클라이언트가 QUIC 응답을 300ms 기다린 뒤에야 TCP로 넘어간다면 어떻게 되나요?
해설 보기
본문의 왕복 수로 계산하면 TCP + TLS 1.3은 3 RTT로 240ms, QUIC 새 연결은 2 RTT로 160ms입니다. UDP가 막힌 망에서는 QUIC을 300ms 기다리며 시간만 쓰고, 그 뒤 TCP로 다시 시작해 240ms가 더 걸리므로 첫 응답까지 540ms가 됩니다. 처음부터 TCP를 썼을 때보다 300ms 느립니다. 그래서 브라우저는 QUIC과 TCP를 거의 동시에 시도해 먼저 성공한 쪽을 쓰고, 한 번 실패한 망에서는 한동안 QUIC을 시도하지 않는 방식으로 이 손해를 줄입니다.
다음 장 DNS의 구조와 동작에서는 브라우저에 도메인 이름을 입력했을 때 그 이름이 IP 주소로 바뀌기까지 어떤 서버들이 어떤 순서로 답하는지를 따라갑니다.