2000년대 후반부터 구글의 Make The Web Faster로 대표될 수 있는 인터넷과 웹 페이지 속도 향상을 위한 일련의 성과가 있습니다. 이런 노력들은 실로 다방면에 걸쳐 있는데, 웹 브라우저 (Chrome), 압축 이미지 포맷 (WebP), 페이지 최적화 (PageSpeed), 데이터 압축 (Brotli)과 같은 어플리케이션 계층의 성과도 있지만 전송 프로토콜 자체에도 많은 개량이 이루어 집니다.

성능이나 보안 관련한 것들을 나열해 보면 전부는 아니지만 다음과 같은 것들이 있습니다.

위 항목 하나하나도 큰 주제이긴 하지만, 이 글에서는 위에서 언급된 여러가지 기능을 하나로 모아서 새로 만들어진 UDP기반의 새로운 전송 프로토콜인 QUIC 그리고 조금씩 이야기가 들려오고 있는 http3 에 대해서 간단히 다루어 보도록 하겠습니다.

UDP 기반으로 전송 프로토콜을 만드는 이유

이미 TCP가 잘 동작하고 있고 UDP는 DNS나 스트리밍 등에 쓰이는 것으로 아는데 왜 UDP 기반으로 전송 프로토콜을 만들까요? 그 전에 널리 사용되고 있는 TCP의 한계점, 그리고 UDP가 아니라 새로운 프로토콜을 정의했을 때의 한계와 문제점에 대해서 먼저 생각해 봅니다.

UDP로 전송 프로토콜을 만드는 것은 아주 새로운 아이디어는 아닙니다. 기존의 RTP(Real-time Transport Protocol)과 같이 실시간 미디어 전송을 위한 프로토콜이 UDP기반으로 동작하고 있지만, QUIC은 웹 페이지와 같은 보다 일반적인 사용을 목표로 하고 있습니다.

TCP의 한계와 문제점

TCP는 70년대 개발된 이래 인터넷의 중추적인 전송 프로토콜 역할을 하고 있습니다. 설계 특성상 신뢰성 있는 전송과 혼잡 제어를 통해서 대역폭을 사전에 알 수 없고 가변적인 구간에서도 자동적으로 최적의 대역폭으로 데이터를 양방향 전송 가능한 것이 최대의 특징인데, 그러다 보니 FTP, HTTP, SMTP 등의 상위 프로토콜을 전송하는 기반으로 사용 됩니다. 인터넷 되는 운영체제라고 하면 모두 기본적으로 TCP 와 BSD 소켓 구현이 포함되어 있습니다.

첫번째 한계는 오래전에 설계된 프로토콜인 관계로 확장성이 부족하다는 점입니다. 아래 그림은 TCP 헤더 포맷을 나타내고 있습니다.

tcp Wikipedia - TCP

위 그림에서 알 수 있는것은 어떤 기능 확장을 위해서는 Options 필드를 사용해야 하는데 Options 필드의 최대 크기는 320비트 즉 40바이트로 정해져 있습니다. 현재 이미 MSS, Window Scale, Selective ACK, TCP timestamp 등의 거의 기본으로 사용되는 옵션들이 모두 사용 중이고, 그 중 크기를 가장 많이 차지하는 것이 Selective ACK인데 Options 필드의 크기 제약으로 인해서 최대 4 구간 (1 구간이 8바이트를 사용)만을 전달할 수 있고, 여기에 TCP timestamp 를 포함시키면 3구간만 전달 가능할 정도로 추가 정보의 전달에 이미 한계가 온 상황입니다.

물론 RACK, TLP와 같은 기능들은 이러한 추가 데이터 전달 없이 전송의 안정성과 속도를 향상시키려는 노력의 산물입니다만 그러다 보니 구현상 제약이 생겨나고 있습니다.

두번째로는 너무 널리 사용되고 있고 헤더 자체가 암호화가 되지 않은 상태로 전달되다 보니 외부에서 관찰하기가 쉽고 표준이 이미 정해진 관계로 외부에서 패킷에 손을 대는 일이 가능하다는 것입니다. 가령 TCP패킷을 캡처할 수 있다면 TCP 패킷을 분석해서 시작과 끝, 현재의 전송 상태 등을 유추하는 일이 가능해지고 이걸 기반으로 다양한 처리가 가능해 집니다. 일반적인 방화벽을 생각하면 되는데 가령 상태 기반 방화벽 (stateful) 이라는 것이 바로 TCP 연결 상태를 추적하는 것으로 생각하면 됩니다. TCP 연결 추적을 위해서는 위 헤더에 있는 시퀀스, ACK 번호와 SYN, FIN등의 플래그를 이용해서 추적을 하는데 없던 플래그를 추가 하거나 기존 동작을 바꾸게 되면 어떻게 될까요?

실제 네트워크 상에는 방화벽 이외에도 TCP 연결 자체를 사용자 모르게 가로채어 중간에서 연결을 종료 (termination 한다고 합니다. 연결을 끊는다는 의미가 아니라 논리적인 TCP연결이 되는 곳을 이야기 합니다) 하고 새 연결을 사용자과 다시 맺도록 하는 장비들도 많이 존재 합니다. 이런 것들은 주로 TCP로 전송되는 데이터 내용을 관찰하거나 변조, 접근 제어, 캐싱, 가속 등을 위한 목적으로 이용 됩니다.

미들박스와 고착화 문제

이러한 상황이다 보니 프토토콜을 쉽게 확장할 수 없는 또 다른 이유도 되는데, TCP의 동작 자체가 이미 잘 알려져 있다 보니 이러한 중간 장비(middlebox)가 알려져 있는 TCP 동작 기반으로 기능을 수행하도록 만들어진 것들이 많기 때문입니다. 이 경우 프로토콜 스펙이 변하는 경우 대응하지 못하기도 합니다.

가령 TCP Fast Open 의 예를 들 수 있는데, Fast Open은 TCP 연결을 맺기 위해 수행되는 3단계 핸드쉐이크를 축약해서 1단계만으로 TCP연결을 가능하게 합니다. 즉 일반적으로 알고 있는 SYN -> SYN+ACK -> ACK 을 거쳐서 데이터 연결을 맺는 것이 아니라 바로 SYN + 데이터 로 바로 연결이 시작이 되는데, 가령 중간에 있는 상태 기반 방화벽 장비가 Fast Open 에 대해서 알지 못하면 오동작하거나 연결을 끊게 되는 문제가 발생하게 됩니다.

이러한 문제는 고착화(Ossification)이라고 불리기도 하는데, 오랜 기간 동안 널리 사용되는 것들의 경우 다들 사용하는 관계로 오히려 확장이 어려워지는 역설적인 문제가 존재합니다.

그럼 새 프로토콜을 정의하면 안되나요?

프로토콜은 어떤 수준에서 정의되어 있는지에 따라 계층이 다릅니다. 가령 TLS나 HTTP와 같은 프토토콜은 신뢰성 있는 전송 계층 위에서 동작할 수 있고 일반적으로는 TCP가 그 역할을 수행 합니다.

그럼 TCP는 어디서 정의되어 있을까요? TCP 패킷인지의 여부는 해당 패킷의 IP 헤더 포맷의 Protocol 필드의 값을 보면 알 수 있습니다.

ipv4 Wikipedia - IPv4

Protocol 필드는 1바이트인데 IANA에서 정한 프로토콜 번호 목록 중에서 사용하게 됩니다. MacOS나 FreeBSD, Linux 등을 사용하고 있다면 /etc/protocols파일을 열어 보면 쉽습니다.

% grep tcp /etc/protocols
tcp	6	TCP		# transmission control protocol

UDP도 같은 방법으로 찾아볼 수 있습니다.

% grep udp /etc/protocols
udp	17	UDP		# user datagram protocol
crudp	127	CRUDP		# Combat Radio User Datagram
udplite	136	UDPLite		# The UDP-Lite Protocol

해당 파일을 열어 보면 처음 보는 프로토콜들이 많을 것입니다. 그만큼 특수 목적으로 사용되거나 정의만 되었지 실제 사용되지 않는 프로토콜이 대부분이고 실제 인터넷 트래픽은 TCP와 UDP가 거의 전부라고 해도 과언은 아닐 것입니다. 가령 HTTP는 TCP 위에서 동작하고 DNS는 UDP나 TCP위에서 동작 합니다.

이러다 보니 TCP나 UDP가 아니면 아예 통과하지 못하는 라우터나 방화벽들도 존재하고 방화벽 설정을 할 때 TCP나 UDP 트래픽이 아니면 모두 막도록 설정하는 경우도 많습니다. 또한 가정용 라우터와 같이 NAT환경을 기본적으로 만드는 경우 TCP나 UDP가 아니라면 제대로 주소 변환이 안 될 수도 있습니다.

이것이 어떤 문제를 일으키는가 하면 만약 여러분이 멋진 새 프로토콜을 하나 정의했다고 해도 그것이 실험실 밖으로 나왔을 때 인터넷상에서 항상 전송 가능한지 보장이 안된다는 것입니다.

실제로 SCTP라는 TCP의 제약을 일부 개선한 프로토콜이 있습니다만 만약 이것으로 클라이언트와 서버를 둘 다 만들었다고 해도 언제 어디서 SCTP패킷이 통과될지는 잘 보장되지 않는다는 현실적인 문제가 보급의 장애가 되고 있습니다.

다만 잘 정의된 제한적인 용도라면 큰 문제가 아닐 수도 있습니다. 예를 들어 SCTP는 LTE와 같은 모바일 코어 네트워크 내에서 사용되고 있습니다.

프로토콜 스택은 역시 커널에 만들어야지!

새 프로토콜을 만들었을 때 일반적으로 TCP 구현처럼 커널에 구현되는 것이 대부분입니다. 하지만 아무리 모듈로 만든다고 해도 커널에 새 프로토콜을 구현하는 것은 일반 어플리케이션 대비 쉬운 일이 아니며, 만들었다고 해도 실제 사용을 위해서는 해당 서버와 클라이언트가 모두 해당 프로토콜이 구현되어 있는 커널 내지는 커널 모듈을 설치해야 한다는 현실적인 문제가 존재 합니다.

특히 인터넷 상에서 일반적으로 사용하기 위해서는 여러가지 클라이언트를 지원해야 하는데, PC는 물론이고 모바일 클라이언트의 경우에는 시스템 권한을 가질 수 없는 경우가 대부분이기 때문에 새로 프로토콜을 만들었다고 해도 보급하는 것은 OSS 운영체제가 아닌 경우 OS벤더가 아니면 어렵다고 봐야 할 것입니다.

UDP 기반 전송 프로토콜의 대두

위에서 언급한 제반 사정이 있다 보니 그럼 TCP 기반이 아닌 새 프로토콜을 만들어서 인터넷 상에서 보급하고자 한다면 선택이 매우 제한적일 수 밖에 없는데, 따라서 UDP를 어떻게 활용하는지가 매우 중요하게 됩니다.

UDP는 신뢰성이 없다고 하던데..

인터넷 네트워크를 배운 적이 있다면 TCP는 신뢰 가능한 전송 프로토콜, UDP는 신뢰성이 없다고 배웠을 것입니다. 신뢰성이 없다는 의미를 더 생각해 보면, UDP는 그냥 데이터를 실어 보낼 수 있을 뿐 그 이외의 기능은 아무것도 정의해 놓지 않았습니다. UDP는 User Datagram Protocol의 약자인데 U를 Unreliable이라고 생각하는 사람도 적지 않을 것입니다.

udp Wikipedia - UDP

위 그림은 UDP 헤더 포맷입니다. 전송 순서를 알기 위해서 시퀀스나 ACK 번호를 헤더에 정의해 놓은 TCP와는 달리 UDP 헤더에는 포트 번호와 패킷 크기, 체크섬만 있고, 체크섬은 굳이 채워넣지 않아도 됩니다. 따라서 신뢰성을 보장할 수단이 존재하지 않는 것입니다.

다만 달리 생각하면 이건 백지와도 같은 것입니다. User Datagram 이라는 의미가 바로 사용자가 정의해서 사용하라는 의미입니다. 즉 여기에 원하는 기능을 얼마든지 얹으면 되고 서버와 클라이언트의 구현에 따라서 그 동작은 매우 달라질 수 있습니다. 즉 TCP와 같은 신뢰성 있는 전송을 원한다면 TCP와 같이 시퀀스 번호 등을 정의하고 서버와 클라이언트 간에 패킷 유실이나 순서 바뀜에 어떻게 대처할 것인지를 상호 정의 한다면 UDP기반의 신뢰성 있는 전송 프로토콜을 만들 수 있습니다. UDT와 같은 UDP기반의 파일 전송 어플리케이션을 생각해 보면 됩니다.

사용자 수준에서 프로토콜 스택이 작성 가능

UDP 기반으로 전송 프로토콜을 만들게 되는 또 다른 이유는 바로 사용자 수준 네트워크 프로토콜 스택을 만드는 것이 크게 어렵지 않기 때문입니다. 만약 IP에 새 프로토콜 번호를 정의해서 프로토콜을 만든다면 일반 사용자 권한 만으로 서버와 클라이언트를 만드는 것은 거의 불가능 합니다만 (raw socket을 사용해도 그렇습니다) UDP라면 이미 거의 모든 OS에서 소켓 API를 통해서 송수신이 가능 하므로, 간단한 UDP 소켓 기반 서버를 작성하고 그 안의 로직을 채워 넣는 것으로 시작할 수 있습니다.

또한 이 경우 OS의 커널에 접근할 필요도 없고 1024번 이하 포트를 사용하지 않는다면 관리자 권한도 필요 없으므로 일반 사용자 권한 만으로 서버와 클라이언트 모두 작성할 수 있다는 장점이 있습니다. 웹 브라우저의 WebRTC는 이러한 특성을 이용해서 UDP도 사용하고 있다는 점도 참고할 수 있을 것입니다.

정리하며

위에서 언급한 여러가지 이유로 인해 TCP의 한계를 극복하는 새로운 전송 프로토콜을 만들고자 한다면 UDP기반으로 하는 것이 최적의 선택이라는 점을 알 수 있습니다.

QUIC(Quick UDP Internet Connections 이라고 처음에 불렸는데 지금은 어떤 것의 약어는 아닙니다)은 이러한 기반을 갖고 기존의 TCP, TLS, HTTP, 심지어 HTTP2의 여러가지 단점을 극복하고자 만들어진 프로토콜입니다. 다음 편에서는 QUIC이 해결하고자 하는 문제점에 대해서 조금 더 자세히 다루어 보도록 하겠습니다.