QUIC: UDP 위에 신뢰성을 새로 쌓은 전송 프로토콜

QUIC: UDP 위에 신뢰성을 새로 쌓은 전송 프로토콜

새 전송 프로토콜을 만든다는 건 웬만해선 벌이지 않는 일입니다. TCP는 40년 넘게 인터넷을 떠받쳐 왔고, 그동안 쌓인 개선안도 한둘이 아니거든요. 그런데도 구글은 2012년에 TCP를 고치는 대신 UDP 위에 새 프로토콜을 얹기 시작했고, 그게 지금의 QUIC가 되었습니다.

HTTP 글에서 HTTP/3를 다루며 QUIC를 잠깐 언급했는데요. 이번에는 QUIC 자체를 놓고, 왜 굳이 바닥부터 새로 만들었는지 그리고 그래서 무엇이 달라졌는지를 짚어보겠습니다.

왜 TCP를 고치지 못했을까

TCP에 없는 기능을 넣자는 제안은 늘 있었습니다. 문제는 제안이 아니라 배포였습니다.

TCP는 운영 체제 커널 안에 있습니다. 새 기능을 쓰려면 서버와 클라이언트의 커널이 모두 업데이트되어야 하죠. 데스크톱은 그렇다 쳐도, 오래 쓰는 라우터나 임베디드 기기까지 따라오려면 십 년 단위의 시간이 걸립니다.

더 큰 걸림돌은 중간에 있었습니다. 방화벽, NAT 장비, 로드 밸런서처럼 경로 위에 놓인 장비를 미들박스(middlebox)라고 부르는데요. 이들이 TCP 헤더를 들여다보고 자기 나름의 판단을 내립니다. 처음 보는 플래그가 켜진 패킷을 그냥 버리는 장비도 흔했고요. 그래서 TCP에 새 옵션을 추가하면 “표준에는 있는데 현실에서는 절반쯤 안 통하는” 상태가 됩니다. 프로토콜이 굳어버린 이 현상을 화석화(ossification)라고 부릅니다.

실제 사례가 있습니다. 연결을 맺는 첫 왕복에 데이터를 함께 실어 보내자는 TCP Fast Open이 2014년에 표준이 되었는데요. 경로에 놓인 장비 중 일부가 데이터가 실린 SYN 패킷을 비정상으로 보고 버리는 바람에, 십 년이 지나도록 기본으로 켜진 곳을 찾기 어렵습니다. QUIC의 0-RTT가 같은 발상인데도 무리 없이 동작하는 건, 그 내용이 미들박스에게는 그저 UDP 페이로드로만 보이기 때문입니다.

QUIC가 UDP를 고른 건 UDP가 좋아서가 아닙니다. 미들박스가 UDP 안쪽은 잘 들여다보지 않으니, 그 안에서는 무엇을 하든 자유롭기 때문입니다. 운영 체제 입장에서 보면 QUIC는 그냥 UDP 트래픽이라, 커널을 건드리지 않고 응용 프로그램 안에서 프로토콜을 통째로 구현할 수 있습니다. 브라우저를 업데이트하면 전송 프로토콜도 함께 업데이트되는 셈이죠.

HTTP만을 위한 프로토콜은 아니다

이름 때문에 오해하기 쉬운데, QUIC는 HTTP 전용이 아닙니다. TCP가 그렇듯 무엇이든 실어 나를 수 있는 전송 계층 프로토콜입니다. HTTP/3는 그 위에 올라간 첫 응용일 뿐이죠.

구분해서 보면 규격도 층으로 나뉘어 있습니다. RFC 9000이 전송 프로토콜 자체를 정의하고, RFC 9001이 TLS 1.3을 어떻게 끼워 넣을지를, RFC 9002가 손실을 감지하고 혼잡을 제어하는 방법을 맡습니다. HTTP를 QUIC 위에 얹는 방법은 RFC 9114로 따로 나와 있고요.

여기까지 오는 데 십 년 가까이 걸렸습니다. 구글이 2012년에 자체 구현을 크롬과 자사 서비스에 넣어 돌려보기 시작했고, 실제 트래픽에서 얻은 결과를 들고 2016년에 표준화 논의가 열렸습니다. 표준안이 확정된 건 2021년입니다. 이미 수년간 대규모로 돌려본 뒤에 규격을 다듬은 셈이라, 표준이 나왔을 때는 검증도 어느 정도 끝나 있었습니다.

HTTP 말고도 쓰임이 늘고 있습니다. DNS 질의를 QUIC 위로 보내는 방식이 표준이 되었고, 윈도우는 파일 공유 프로토콜을 QUIC 위에 얹어 VPN 없이 외부에서 접근할 수 있게 만들었습니다. TCP와 UDP 사이에서 고민하던 자리에 선택지가 하나 늘어난 셈입니다.

핸드셰이크를 한 번의 왕복으로

HTTPS로 페이지 하나를 여는 데 왕복이 몇 번 필요한지 세어보면 QUIC가 노린 지점이 보입니다.

TCP + TLS 1.3
1왕복  SYN → SYN-ACK → ACK          (TCP 연결)
2왕복  ClientHello → ServerHello    (TLS 협상)
3왕복  HTTP 요청 → 응답

TCP 연결에 한 번, TLS 협상에 또 한 번을 쓰고 나서야 요청을 보낼 수 있습니다. 두 절차가 하는 일은 다르지만 순서가 강제되어 있어서, 앞의 왕복이 끝나야 뒤가 시작됩니다.

QUIC는 이 둘을 하나로 합쳤습니다. 전송 계층의 협상과 암호 협상이 같은 패킷에 실려 나갑니다.

QUIC
1왕복  Initial(암호 협상 포함) → 응답
2왕복  HTTP 요청 → 응답

왕복 한 번을 아꼈습니다. 지연이 50밀리초인 회선이면 100밀리초, 국제 구간이라 200밀리초쯤 되면 400밀리초를 버는 셈이죠. 한 번 접속했던 서버에 다시 붙을 때는 0-RTT라는 방식으로 첫 왕복에 데이터를 함께 실어 보낼 수도 있습니다. 다만 0-RTT로 보낸 데이터는 공격자가 그대로 복사해 다시 보내는 재생 공격에 취약해서, 조회처럼 여러 번 실행해도 결과가 같은 요청에만 씁니다.

암호 협상이 선택이 아니라는 점도 짚어둘 만합니다. QUIC에는 평문 모드가 없습니다. TLS 1.3이 프로토콜 안에 박혀 있어서, 암호화를 끄는 선택지 자체가 존재하지 않습니다.

스트림이 서로를 기다리지 않는다

HTTP/2가 한 연결에 여러 요청을 동시에 흘려보내는 멀티플렉싱(multiplexing)을 도입했는데요. 그런데도 성능이 기대만큼 안 나오는 경우가 있었습니다. 아래에 깔린 TCP 때문이었습니다.

TCP는 바이트 스트림 하나만 압니다. 중간의 한 조각이 사라지면, 그 뒤에 도착한 데이터가 멀쩡해도 응용에 전달하지 않고 붙잡아 둡니다. 순서대로 넘겨준다는 약속을 지켜야 하니까요. 그 결과 이미지 하나를 받다가 패킷을 잃으면, 아무 상관 없는 CSS 파일까지 함께 멈춰 섭니다. 줄 맨 앞이 막혀 뒤가 못 나가는 이 현상을 head-of-line blocking이라고 부릅니다.

QUIC는 스트림을 전송 계층이 직접 압니다. 패킷마다 어느 스트림의 어느 위치인지가 적혀 있어서, 잃어버린 조각이 속한 스트림만 기다리게 만들 수 있죠.

패킷 3번을 잃었을 때
TCP    [1][2][ ✕ ][4][5]  → 4, 5도 응용에 못 올라감
QUIC   [1][2][ ✕ ][4][5]  → 3번이 속한 스트림만 대기, 나머지는 즉시 전달

같은 멀티플렉싱이라도 TCP 위에서는 절반만 동작하고, QUIC 위에서는 온전히 동작합니다. HTTP/2와 HTTP/3의 차이가 사실상 여기서 갈립니다.

연결을 주소가 아니라 ID로 식별한다

TCP 연결은 출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트 네 값으로 식별됩니다. 그래서 지하철에서 나오며 와이파이가 끊기고 이동통신으로 넘어가면, IP가 바뀌는 순간 연결이 죽습니다. 영상 통화가 잠깐 멎거나 다운로드가 처음부터 다시 시작되는 이유죠.

QUIC는 연결마다 연결 식별자(Connection ID)를 따로 둡니다. IP가 바뀌어도 같은 식별자를 들고 있으면 서버는 같은 연결로 알아봅니다. 암호 키도 그대로 쓰니 다시 협상할 필요가 없고요.

여기에는 사생활 문제가 따라붙습니다. 식별자가 고정되어 있으면 네트워크를 옮겨 다녀도 같은 사용자로 추적할 수 있으니까요. 그래서 QUIC는 식별자를 여러 개 미리 발급해 두고, 경로가 바뀔 때마다 쓰지 않은 것으로 갈아탑니다.

같은 패킷 번호를 두 번 쓰지 않는다

작지만 영리한 설계가 하나 있습니다. 패킷 번호를 절대 재사용하지 않는다는 규칙입니다.

TCP는 재전송할 때 원래의 일련번호를 그대로 씁니다. 그러면 확인 응답이 돌아왔을 때 그게 처음 보낸 패킷의 응답인지 재전송의 응답인지 구분할 방법이 없습니다. 왕복 시간을 재야 혼잡 제어가 동작하는데, 정작 그 측정이 흐려지는 것이죠. TCP는 이 모호함을 피하려고 재전송한 패킷의 왕복 시간을 아예 측정에서 빼는 방식을 씁니다.

QUIC는 재전송할 때 새 번호를 붙입니다. 내용은 같아도 번호가 다르니 어느 쪽의 응답인지 항상 분명하죠. 덕분에 왕복 시간을 정확히 재고, 혼잡 제어도 더 촘촘하게 반응할 수 있습니다.

헤더까지 암호화한 진짜 이유

TLS는 내용을 보호하지만 TCP 헤더는 그대로 노출됩니다. 일련번호도 플래그도 경로 위에서 다 보이죠. QUIC는 페이로드뿐 아니라 헤더 대부분까지 암호화합니다. 패킷 번호도 감춥니다.

기밀성 때문만은 아닙니다. 앞서 이야기한 화석화를 막으려는 목적이 큽니다. 미들박스가 볼 수 있으면 언젠가는 그 값을 근거로 판단을 내리기 시작하고, 그러면 그 필드는 다시는 바꿀 수 없게 됩니다. 아예 안 보이게 만들면 아무도 의존할 수 없으니 프로토콜을 계속 고칠 수 있습니다.

같은 이유로 QUIC는 협상 과정에 의도적으로 쓰지 않는 값을 섞어 보내기도 합니다. “모르는 값이 오면 무시하라”는 규칙을 구현이 실제로 지키는지 상시 검증하는 셈이죠. 표준을 지키지 않는 구현이 조용히 늘어나는 상황을 미리 막는 장치입니다.

브라우저는 HTTP/3를 어떻게 발견할까

궁금한 지점이 하나 남습니다. 브라우저는 어떤 사이트가 HTTP/3를 지원하는지 어떻게 알까요? UDP 443번 포트로 일단 찔러보고 안 되면 되돌아오는 방식은 너무 느립니다.

가장 널리 쓰이는 방법은 서버가 응답 헤더로 알려주는 것입니다.

응답 헤더 확인
curl -sI https://daleseo.com | grep -i alt-svc
결과
alt-svc: h3=":443"; ma=86400

“나는 443번 포트에서 h3도 받는다, 이 정보는 86400초 동안 유효하다”는 뜻입니다. 브라우저는 이번 요청은 HTTP/2로 끝내고, 다음 접속부터 HTTP/3를 시도합니다.

구글은 여기에 값을 하나 더 붙여둡니다.

결과
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

h3-29는 표준이 되기 전 초안 29판을 가리킵니다. 표준판을 아직 모르는 오래된 클라이언트를 위해 남겨둔 것이죠.

이 방식에는 빈틈이 있습니다. 첫 접속은 어차피 TCP로 해야 한다는 점입니다. 그래서 DNS로 미리 알려주는 방법이 함께 쓰입니다. HTTPS 레코드라고 부르는 DNS 레코드 종류인데요.

HTTPS 레코드 조회
curl -s 'https://dns.google/resolve?name=daleseo.com&type=HTTPS' | python3 -m json.tool
결과 일부
{
  "name": "daleseo.com.",
  "type": 65,
  "TTL": 300,
  "data": "1 . alpn=h3,h2 ipv4hint=104.21.83.24,172.67.210.226 ..."
}

alpn=h3,h2가 핵심입니다. 이름을 찾는 과정에서 이미 HTTP/3 지원 여부를 알게 되니, 첫 접속부터 곧바로 QUIC로 갈 수 있습니다. 어차피 DNS 조회는 해야 하는 일이라 왕복이 늘지도 않고요.

공짜는 아니다

좋은 이야기만 늘어놓았으니 대가도 적어야 공평하겠죠.

우선 CPU를 더 씁니다. TCP는 커널과 네트워크 카드가 오랫동안 최적화해 온 경로를 타지만, QUIC는 사용자 공간에서 패킷을 하나하나 처리합니다. 회선이 빠를수록 이 차이가 두드러져서, 같은 대역폭에서 처리량이 오히려 밀린다는 측정도 여러 차례 보고되었습니다. 요즘은 여러 데이터그램을 한 번의 시스템 호출로 묶어 보내는 기법이 자리를 잡으면서 격차가 줄고 있습니다.

그리고 UDP가 막힌 곳에서는 아예 동작하지 않습니다. UDP를 통째로 차단하는 회사 방화벽이 아직 있어서, 이럴 때 브라우저는 조용히 HTTP/2로 되돌아갑니다. QUIC로 갈아탄다고 TCP 쪽 설정을 방치해도 된다는 뜻은 아닌 셈입니다.

마지막으로 들여다보기가 어렵습니다. 헤더가 암호화되어 있으니 패킷을 잡아도 내용을 알 수 없습니다. 디버깅하려면 서버나 브라우저가 키를 따로 내보내도록 설정해야 하는데, TCP 시절보다 손이 많이 갑니다.

마치며

QUIC를 한 줄로 줄이면 “TCP가 하던 일을 고칠 수 있는 자리로 옮겨놓은 것”입니다. UDP를 고른 이유도 UDP가 뛰어나서가 아니라, 아무것도 강요하지 않는 얇은 바닥이라 그 위에 원하는 것을 새로 쌓을 수 있어서였습니다.

핵심만 추리면 이렇습니다. 전송과 암호 협상을 합쳐 왕복을 한 번 줄였고, 스트림을 전송 계층이 직접 알아서 한 스트림의 손실이 다른 스트림을 붙잡지 않게 했습니다. 연결을 주소가 아니라 식별자로 묶어 네트워크가 바뀌어도 이어지게 했고, 헤더까지 감춰 앞으로도 계속 고칠 수 있는 여지를 남겼습니다.

지금 보고 있는 이 페이지도 브라우저 개발자 도구의 네트워크 탭에서 Protocol 열을 켜면 h3으로 표시될 겁니다. 직접 한 번 확인해 보시면 지금까지 읽은 내용이 훨씬 손에 잡힐 거예요. 🔍

규격이 어떻게 논의되고 다듬어졌는지 따라가 보고 싶다면 QUIC 워킹 그룹 사이트가 출발점입니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord