이 글은 HTTP에 대한 지식이 있다는 전제하에 진행되는 글입니다.
HTTP에 대한 내용은 링크를 참고하세요.
Intro
HTTP/3는 HTTP(Hypertext Transfer Protocol)의 세 번째 Major 버전으로, 기존의 HTTP/1, HTTP/2와는 다르게 UDP 기반의 프로토콜인 QUIC을 사용하여 통신하는 프로토콜이다.
TOC
HTTP/1.1
HTTP 명세서의 HTTP/1.1 개정판은 'keep-alive' 연결의 개념을 도입한 것으로 이전에 요청을 보내기 전 TCP와 TLS Handshake가 완료되어야 하는 문제를 해결하려 했다. 클라이언트가 TCP 연결을 재사용하는 것을 허용하여 복수의 요청에서 발생할 수 있는 초기 연결 확립과 슬로우 스타트에 드는 비용을 줄여준다.
복수의 요청이 같은 연결을 공유할 수 있으나, 하나씩 순서대로 보내야 하므로 각 연결에서는 한 번에 하나의 요청 또는 응답 교환만을 처리할 수 있다.
웹이 진화하면서, 각 웹 사이트에서 필요한 자원(CSS, 자바스크립트, 이미지 등등)의 양이 갈수록 증가함에 따라 웹 페이지를 다운로드받고 렌더링하기 위해 브라우저는 더 많은 동시성을 필요하게 되었다. 하지만 HTTP/1.1에서는 클라이언트가 한 번에 하나의 HTTP 응답/요청만을 해야 하므로, 네트워크 계층에서 동시성을 높일 수 있는 유일한 방법은 동일 오리진에 대해 복수의 TCP 연결을 병렬로 만드는 것인데, 이 경우 'keep-alive' 연결의 장점이 사라지게 된다. 일정 수준(하지만 낮은 쪽)으로 연결은 재사용 되지만, 다시 처음 문제로 돌아가게 되는 것이다.
HTTP/2
HTTP/2가 발표되고 여러 가지 개선되었으나, 그중 HTTP Stream
의 개념이 가장 눈에 띈다. HTTP 구현이 동일한 TCP 연결에 복수의 HTTP 응답/요청 교환을 동시 다중 송신할 수 있도록 하여, 브라우저가 TCP 연결을 더 효율적으로 재사용할 수 있도록 한 것이다.
HTTP/2는 복수의 요청/응답이 동시에 같은 연결 위에서 전송될 수 있도록 하여 단일 TCP 연결의 사용 비효율성이라는 최초의 문제를 해결하였다.
하지만 모든 응답과 요청은 패킷 손실에 대해서 하나의 요청에만 관계된 것이라도 동등하게 영향을 받는다. HTTP/2 계층이 서로 다른 HTTP 응답/요청 교환을 별개의 스트림으로 분리하였지만, TCP는 그러한 추상화에 대해 알지 못하고 바이트로만 보기 때문이다.
TCP의 역할은 전체 데이터를 순서대로 한 곳에서 다른 곳으로 전달하는 것이다. 데이터 일부를 담고 있는 TCP 패킷이 네트워크 경로상에서 손실되면 데이터 스트림에 누락 구간이 생기고 TCP는 손실이 탐지되었을 때 영향받은 패킷만을 재전송한다.
그러는 동안 분실과 관계없는 완전히 독립된 HTTP 요청에 속하는 경우, 분실된 데이터 이후에 성공적으로 받은 데이터는 어플리케이션으로 전달되지 못한다. TCP는 잃어버린 데이터 없이 어플리케이션이 진행 가능한지 알지 못하므로 불필요한 지연이 발생하게 된다. 이는 HoLB(head-of-line blocking) 이라고 알려진 문제이다.
HTTP/3
HTTP/3은 여러 특징 중 전송 계층에 스트림을 기본 구조로 도입한 QUIC(Quick UDP Internet Connection) 을 사용한다. QUIC 스트림은 동일 QUIC 연결을 공유하므로 새 스트림을 만들 때 추가적인 Handshake나 슬로우 스타트가 필요하지 않다. QUIC 스트림은 독립적으로 전달되어 어떤 스트림에 패킷 손실이 있는 경우에도 다른 스트림에는 영향이 없다. 이는 QUIC 패킷이 UDP 데이터그램 위에 캡슐화되어 있기 때문이다.
UDP를 사용하는 것은 TCP보다 더 유연하며 QUIC 구현을 완전히 사용자 수준 구현으로 할 수 있게 한다. 프로토콜의 구현 업데이트는 TCP의 경우처럼 운영체제 업데이트에 묶여있지 않다.
QUIC은 TCP의 전형적인 3 Way Handshake와 TLS 1.3의 Handshake를 결합했다. 이런 결합은 암호화와 인증이 기본으로 제공되고 연결을 더 빨리 만들 수 있게 한다. 즉 HTTP의 첫 요청에 필요한 QUIC 연결을 새로 만들 때에도 데이터 전송 시작까지 걸리는 지연 시간은 TCP상의 TLS보다 작다
HTTP/3가 UDP를 사용하는 이유
HTTP/3는 QUIC을 기반으로 돌아가는 프로토콜이기 때문에 우리가 HTTP/3를 이해하려면 QUIC에 초점을 맞춰야 한다. QUIC은 TCP의 문제들을 해결하고 레이턴시(latency)의 한계를 뛰어넘고자 구글이 개발한 UDP 기반의 프로토콜이다.
QUIC은 처음부터 TCP의 Handshake 과정을 최적화하는 것에 초점을 맞추어 설계되었고, UDP를 사용함으로써 이를 실현해냈다.
TCP Header
TCP의 경우 워낙 오래전에 설계되기도 했고, 여러 기능이 많이 포함된 프로토콜이다 보니 이미 헤더가 거의 꽉 찼다. TCP에 기본적으로 정의된 기능 외에 다른 추가 기능을 구현하고 싶다면 가장 하단에 있는 옵션(Options) 필드를 사용해야 하는데, 옵션 필드도 무한정 배당해 줄 수는 없으니 최대 크기를 320 bits
로 정해놓았다.
그러나 TCP의 단점을 보완하기 위해 나중에 정의된 MSS(Maximum Segment Size), WSCALE(Window Scale factor), SACK(Selective ACK) 등 많은 옵션들이 이미 옵션 필드를 차지하고 있기 때문에 실질적으로 사용자가 커스텀 기능을 구현할 수 있는 자리는 거의 남지도 않았다.
UDP Header
UDP의 헤더에는 출발지와 도착지, 패킷의 길이, 체크섬밖에 없다. 이때 체크섬은 패킷의 무결성을 확인하기 위해 사용되는데, TCP의 체크섬과는 다르게 UDP의 체크섬은 사용해도 되고 안 해도 되는 옵션이다.
즉, UDP 프로토콜 자체는 TCP보다 신뢰성이 낮기도 하고 흐름 제어도 안되지만, 이후 개발자가 어플리케이션에서 구현을 어떻게 하냐에 따라서 TCP와 비슷한 수준의 기능을 가질 수도 있다.
물론 TCP가 신뢰성을 확보하기 위해 여러 기능을 제공해주는 것이 개발자 입장에서는 편하고 좋지만, 한 가지 슬픈 점은 이 기능들이 프로토콜 자체에 정의된 필수 과정이라서 개발자가 커스터마이징 할 수 없다는 것이다. 결국 여기서 발생하는 레이턴시들을 어떻게 더 줄여볼 시도조차 하기 힘들다.
QUIC
UDP는 User Datagram Protocol
이라는 이름에서도 알 수 있듯이 데이터그램 방식을 사용하는 프로토콜이기 때문에 애초에 각각의 패킷 간의 순서가 존재하지 않는 독립적인 패킷을 사용한다. 또한 데이터그램 방식은 패킷의 목적지만 정해져있다면 중간 경로는 어딜 타든 신경쓰지 않기 때문에 종단 간의 연결 설정을 하지 않는다. 즉, Handshake 과정이 필요없다는 것이다.
결론적으로 UDP는 TCP가 신뢰성을 확보하기 위해 거치던 많은 과정을 거치지 않기 때문에 속도가 더 빠를 수 밖에 없다는 것인데, 그렇다면 UDP를 사용하게되면 기존의 TCP가 가지던 신뢰성과 패킷의 무결함도 함께 사라지는 걸까?
UDP를 사용하더라도 기존의 TCP가 가지고 있던 기능을 전부 구현할 수 있다. UDP의 진짜 장점은 바로 커스터마이징이 용이하다는 것이기 때문이다.
HTTP/3가 UDP를 사용함으로써 기존 프로토콜보다 나아진 점
지금까지 HTTP/3의 뼈대로 사용되는 QUIC이 왜 TCP가 아닌 UDP를 사용했는지 간략하게 알아보았다. 그렇다면 실제로 UDP를 사용함으로써 얻는 이득에는 무엇이 있을까? 진짜로 HTTP/3는 UDP를 사용함으로써 기존의 HTTP+TCP+TLS를 사용했던 방법보다 더 좋아진 것일까?
연결 설정 시 지연시간 감소
QUIC은 TCP를 사용하지 않기 때문에 통신을 시작할 때 번거로운 3 Way Handshake 과정을 거치지 않아도 된다. 클라이언트가 보낸 요청을 서버가 처리한 후 다시 클라이언트로 응답해주는 사이클을 RTT(Round Trip Time)이라고 하는데, TCP는 연결을 생성하기 위해 기본적으로 1 RTT가 필요하고, 여기에 TLS를 사용한 암호화까지 하려고 한다면 TLS의 자체 Handshake까지 더해져 총 3 RTT가 필요하다.
반면 QUIC은 첫 연결 설정에 1 RTT만 소요된다. 클라이언트가 서버에 어떤 신호를 한번 주고, 서버도 거기에 응답하기만 하면 바로 본 통신을 시작할 수 있다는 것이다. 즉, 연결 설정에 드는 시간이 반 정도밖에 안 된다.
어떻게 이게 가능한 걸까? 첫번째 Handshake를 거칠 때, 연결 설정에 필요한 정보와 함께 데이터도 보내버리는 것이다. TCP+TLS는 데이터를 보내기 전에 신뢰성 있는 연결과 암호화에 필요한 모든 정보를 교환하고 유효성을 검사한 뒤에 데이터를 교환하지만, QUIC은 묻지도 따지지도 않고 그냥 바로 데이터부터 보내기 시작한다.
결국 말하고자 하는 것은 TCP+TLS는 서로 자신의 세션 키를 주고받아 암호화된 연결을 성립하는 과정을 거치고 나서야 세션 키와 함께 데이터를 교환할 수 있지만, QUIC은 서로의 세션 키를 교환하기도 전에 데이터를 교환할 수 있기 때문에 연결설정이 더 빠르다는 것이다.
단, 클라이언트가 서버로 첫 요청을 보낼 때는 서버의 세션 키를 모르는 상태이기 때문에 목적지인 서버의 Connection ID를 사용하여 생성한 특별한 키인 초기화 키(Initial Key)를 사용하여 통신을 암호화한다.
그리고 한번 연결에 성공했다면 서버는 그 설정을 캐싱해놓고 있다가, 다음 연결 때는 캐싱해놓은 설정을 사용하여 바로 연결을 성립시키기 때문에 0 RTT만으로 바로 통신을 시작할 수도 있다. 이런 점들 때문에 QUIC은 기존의 TCP+TLS 방식보다 레이턴시를 더 줄일 수 있다.
TCP SYN 패킷은 한 패킷당 약 1460 Byte
만 전송할 수 있도록 제한하지만 QUIC은 데이터 전체를 첫 번째 라운드 트립에 포함해서 전송할 수 있기 때문에 주고받아야 할 데이터가 큰 경우에는 여전히 QUIC가 유리하다고 할 수 있다.
패킷 손실 감지에 걸리는 시간 단축
QUIC도 TCP와 마찬가지로 전송하는 패킷에 대한 흐름 제어를 해야 한다. 왜냐하면 QUIC, TCP 모두 결국 본질적으로는 ARQ 방식을 사용하는 프로토콜이기 때문이다. 통신 과정에서 발생한 에러를 어떻게 처리할 것인지를 이야기하는 것인데, ARQ 방식은 에러가 발생하면 재전송을 통해 에러를 복구하는 방식을 말한다.
TCP는 여러 ARQ 방식 중에서 Stop and Wait ARQ
방식을 사용하고 있다. 이 방식은 송신 측이 패킷을 보낸 후 타이머를 사용하여 시간을 재고, 일정 시간이 경과해도 수신 측이 적절한 답변을 주지 않는다면 패킷이 손실된 것으로 판단하고 해당 패킷을 다시 보내는 방식이다.
TCP에서 패킷 손실 감지에 대한 대표적인 문제는 송신 측이 패킷을 수신 측으로 보내고 난 후 얼마나 기다려줄 것인가, 즉 타임아웃을 언제 낼 것인가를 동적으로 계산해야한다는 것이다. 이때 이 시간을 RTO(Retransmission Time Out)라고 하는데, 이때 필요한 데이터가 바로 RTT(Round Trip Time)들의 샘플들이다.
한번 패킷을 보낸 후 잘 받았다는 응답을 받을 때 걸렸던 시간들을 측정해서 동적으로 타임 아웃을 정하는 것이다. 즉, RTT 샘플을 측정하기 위해서는 반드시 송신 측으로 부터 ACK를 받아야하는데, 정상적인 상황에서는 딱히 문제가 없으나 타임 아웃이 발생해서 패킷 손실이 발생하게 되면 RTT 계산이 애매해진다.
이때 이 ACK가 어느 패킷에 대한 응답인지 알기 위해서는 타임 스탬프를 패킷에 찍어주는 등 별도의 방법을 또 사용해야 하고, 또 이를 위한 패킷 검사도 따로 해줘야 한다. 이를 재전송 모호성(Retransmission Ambiguity)이라고 한다.
이 문제를 해결하기 위해 QUIC는 헤더에 별도의 패킷 번호 공간을 부여했다. 이 패킷 번호는 패킷의 전송 순서 자체만을 나타내며, 재전송 시 같은 번호가 전송되는 시퀀스 번호와는 다르게 전송마다 모노토닉하게 패킷 번호가 증가하기 때문에, 패킷의 전송 순서를 명확하게 파악할 수 있다.
TCP의 경우 타임스탬프를 통해 패킷의 전송 순서를 파악하거나 타임스탬프를 사용할 수 없는 상황이라면 시퀀스 번호에 기반하여 암묵적으로 전송 순서를 추론했다. 이에 반해 QUIC는 패킷마다 고유한 패킷 번호를 이용함으로써 앞서 봤던 불필요한 과정을 생략하고 패킷 손실 감지에 걸리는 시간을 단축할 수 있다.
이 외에도 QUIC는 대략 5가지 정도의 기법을 사용하여 이 패킷 손실 감지에 걸리는 시간을 단축했는데, 자세한 내용은 QUIC Loss Detection and Congestion Control의 2.1 Relevant Differences Between QUIC and TCP 챕터를 한번 읽어보는 것을 추천한다.
멀티플렉싱을 지원
멀티플렉싱(Multiplexing)은 위에서 TCP의 단점으로 언급했던 HOLB(Head of Line Blocking)을 방지하기 때문에 매우 중요하다. 여러 개의 스트림을 사용하면, 그중 특정 스트림의 패킷이 손실되었다고 하더라도 해당 스트림에만 영향을 미치고 나머지 스트림은 사용할 수 있기 때문이다.
참고로 멀티플렉싱은 여러 개의 TCP 연결을 만든다는 의미가 아니라, 단일 연결 안에서 여러 개의 데이터를 섞이지 않게 보내는 기법이다. 이때 각각의 데이터의 흐름을 스트림이라고 하는 것이다.
HTTP/1의 경우는 하나의 TCP 연결에 하나의 스트림만 사용하기 때문에 HOLB 문제에서 벗어날 수 없었다. 또한 한 번의 전송이 끝나게 되면 연결이 끊어지기 때문에 다시 연결을 만들기 위해서는 번거로운 Handshake 과정을 또 겪어야 했다.
비록 keep-alive 옵션을 통해 어느 정도의 시간 동안 연결을 유지할 수는 있지만 결국 일정 시간 안에 액세스가 없다면 연결이 끊어지게 되는 것은 똑같다.
그리고 HTTP/2는 하나의 TCP 연결 안에서 여러 개의 스트림을 처리하는 멀티플렉싱 기법을 도입하여 성능을 끌어올린 케이스이다. 이 경우 한번의 TCP 연결로 여러 개의 데이터를 전송할 수 있기 때문에 Handshake 횟수도 줄어들게 되어 효율적인 데이터 전송을 할 수 있게 된다.
HTTP/3도 HTTP/2와 같은 멀티플렉싱을 지원한다.
QUIC 또한 HTTP/2와 동일하게 멀티플렉싱을 지원하기 때문에, 이런 이점을 그대로 가져가고 있다. 혹여나 하나의 스트림에서 문제가 발생한다고 해도 다른 스트림은 지킬 수 있게 되어 이런 문제에서 벗어날 수 있다.
클라이언트의 IP가 바뀌어도 연결이 유지
TCP의 경우 소스의 IP 주소와 포트, 연결 대상의 IP 주소와 포트로 연결을 식별하기 때문에 클라이언트의 IP가 바뀌는 상황이 발생하면 연결이 끊어져 버린다. 연결이 끊어졌으니 다시 연결을 생성하기 위해 결국 3 Way Handshake 과정을 다시 거쳐야 한다는 것이고, 이 과정에서 다시 레이턴시가 발생한다.
요즘에는 모바일로 인터넷을 사용하는 경우가 많아서 Wi-Fi에서 셀룰러로 전환되거나 그 반대의 경우, 혹은 다른 Wi-Fi로 연결되는 경우와 같이 클라이언트의 IP가 변경되는 일이 굉장히 잦다.
반면 QUIC은 Connection ID를 사용하여 서버와 연결을 생성한다. Connection ID는 랜덤한 값일 뿐, 클라이언트의 IP와는 전혀 무관한 데이터이기 때문에 클라이언트의 IP가 변경되더라도 기존의 연결을 계속 유지할 수 있다. 이는 새로 연결을 생성할 때 거쳐야하는 Handshake 과정을 생략할 수 있다.