develog
HTTP/0.9부터 HTTP/3까지. QUIC란? 본문
이전 글에서 빈약했던 내용을 보충하고자 한다.
HTTP에 왜 이렇게 집착하는지 궁금할 수 있다.
프론트엔드 개발자더라도, 웹의 근본인 HTTP의 굴레에서 벗어날 수 없을 뿐더러, HTTP에 대해 잘 알고 있으면 웹 성능 최적화에도 도움이 될 수 있다.
조금만 더 공부하면 HTTP를 손바닥 안에 두고 쉽게 가지고 놀 수 있다.
이번엔 순차적으로 짚어보고자 한다.
내용이 많이 길수도 있다. 하지만 이 문서만 다 이해하면 HTTP에 대해서는 빠삭해질 수 있도록 글을 구성해 보았다. 아직 HTTP 간단하게 설명한 글을 이해하지 못했다면, 추후에 읽는 것을 추천한다.
HTTP/0.9
0.9란 버전은 사실 나중에 지어진 것이고, 처음 나왔을 땐 그냥 나왔다.
이 때엔 상태코드나 Header 같은건 전혀 없고 단순히 GET만 존재했다.
Header가 없으니 HTML 전송만 가능했고, 당시엔 이정도로 충분하긴 했다.
내가 알기론 초기에 웹은 단순히 교수들이 논문을 올리는 정도로 활용됐어서 동적인 요소가 전혀 필요
없었던 것으로 안다. 이런 니즈를 충족하기에 HTTP/0.9은 충분했다고 본다.
GET /example.html
HTTP/1.0
이때부턴 요청(Request)에 버전 정보가 붙어서 전송되기 시작했다.
또 상태코드가 처음으로 도입된 HTTP다.
가장 중요한 Header도 이때 추가됐으며, Header가 있기 때문에 이런 저런 타입의 문서를 전달할 수 있게 되고 확장성도 높였다.
HTTP/1.1
1.0이 나오고 몇 달 지나지 않아서 바로 1.1이 발표 되었다.
HTTP/1.1은 HTTP/3까지 나온 지금도 매우 높은 사용자를 보유하고 있다.
이 때엔 좀 변화가 크다. 어떤 변화들이 있었는지, 장단점과 함께 살펴보자.
컨넥션 재활용
한번 연결을 맺고 나면, 이 연결을 다시 재사용할 수 있게 되었다.
이게 이전 시간에 설명한 Persistance Connection(지속 연결) 이다.
지속 연결을 이용하지 않으면, 특정 요청을 보내고 얼마 지나지 않아 다시 요청을 보낼 때 또 3-Handshake 를 해줘야한다는 단점이 생긴다.
당연히 매우 귀찮고 시간도 잡아먹을 수 밖에 없다.
이를 지속 연결을 통해 해결할 수 있다.
한번 요청 후에 바로 연결을 끊어버리는 것이 아니라, 일정 시간동안 연결을 유지하는 것이다.
우리도 자주 Header에서 본 keep-alive가 이를 위한 것이다.
물론 장점만은 아니다. 서버 리소스를 더 많이 잡아먹고 디도스 공격에 취약해질 수 있다는 단점도 있지만 이는 넘어가겠다.
파이프라이닝 추가
이를 통해서 요청을 보내고 그 요청에 대한 응답이 오지 않더라도, 계속 요청을 연속적으로 보낼 수 있게 되었다.
이 기능으로, 네트워크 latency를 줄일 수 있게 되었다.
그치만 이 친구로 인해서, 그 유명한 HOL Blocking 이 발생하게 된다...
HOL Blocking은 Head of Line Blocking의 줄임말로, 직렬적으로 요청이 오기 때문에 앞의 요청에서 문제가 생기면, 뒤에까지 모두 지연되버리는 현상을 의미한다.
예시로, 이미지를 세번 연속해서 요청을 보냈다고 생각해보자.
모종의 이유로 두번째 이미지에 대한 응답이 지연되면 세번째 이미지도 지연되게 된다.
여기서 굉장히 이상하고 비효율적인 것 같다..
"아니 실패한 것만 빼고 세번째 이미지 먼저 주면 되는거 아냐?"
서버는 TCP에서 요청을 받은 순서대로 줘야만 한다. 이는 깰 수 없는 규칙
이다.
못믿을 수 있는 사람이 있어서, RFC 문서에서 발췌한 증거도 가져왔다.

서버는 반드시 응답을 요청의 순서에 맞춰 전달해야한다
이 외에도
- 캐시 제어 메커니즘 제공
- 언어, 인코딩 타입에 따른 Contents 제공
- 같은 IP 주소에 여러 도메인을 호스트로 둘 수 있도록 제공한 HOST header
등이 있지만 내 생각엔 많이 중요한 것 같지 않아 넘어가겠다.
아래 그림으로 한눈에 볼 수 있다.

왼쪽부터 (1.0의 비연결성, 지속연결, 파이프라이닝)
단점 정리
- HOL Blocking
위에서 설명했다.
- 너무 큰 머리
Header가 너무 크다는 것이다. 쿠키, Header의 특정 정보들이
매 요청마다 중복되더라도, 그냥 멍청하게 계속 똑같이 보낸다. 불필요한 정보 전송으로
네트워크 자원이 소비되는 것이다.
- 너무 많아지는 Connection
웹이 점점 발전하면서, 웹 페이지를 렌더링할 때 필요한 리소스의 종류가 너무 많아졌다.
js, css, html, image .... 등등 계속 증가하면서 더 많은 동시 처리를 원하게 되었다.
따라서 TCP 연결을 병렬적으로 수행하게 되었고, 연결 비용이 당연하게도 늘어났다.
심지어 각 도메인마다 가능한 Connection의 수가 한정되어 있어서 완변한 해결도 아니였다.

HTTP/2
이런 1.1의 단점들을 보완하고자 나온 것이 HTTP/2이다.
HTTP/2 Binary Framing Layer
어플리케이션 층(7-Layer)에 새로운 Layer(HTTP/2 Framing Layer)가 도입된다.
HTTP/2에서는 Message와 Frame을 잘게 조개면서, Binary 형식으로 인코딩하도록 설계되었다.
이 때문에, HTTP/1.1과 HTTP/2 를 혼용해서 사용한다면 서로 이해하지 못하는 상황이 발생한다.
다행인건, 이런 프레이밍 과정을 브라우저와 서버가 알아서 해주기 때문에 우리는 할게 없다.
그렇다면, Stream, Frame, Message는 뭘까
Stream, Frame, Message

간소화해서 정리하면 다음과 같다.
- Stream: Connection 내에서 전달되는 바이트의 양 방향 흐름
- Frame: 통신의 최소 단위이며 최소 단위에는 하나의 프레임 헤더가 포함
- Message: 요청, 응답에 매핑되는 프레임의 전체 시퀀스
Frame < Message < Stream
으로 볼 수 있다.
위 개념들은 아래와 같은 관계를 가지고 있다.
- 모든 통신은 단일 TCP Connection을 통해 이루어지며, 양방향 Stream 수는 제한이 없다.
- Stream에는 양방향 메시지 전달에 사용되는 고유 식별자와 우선순위 정보가 있다.
- 각 메시지는 하나의 논리적 HTTP 메시지이며 하나 이상의 프레임으로 구성됩니다.
- 각 프레임의 헤더에 삽입된 스트림 식별자를 통해 이 프레임을 다시 조립할 수 있습니다.
각 프레임의 헤더에 삽입된 스트림 식별자를 통해 이 프레임을 다시 조립할 수 있습니다.
이 말은 곧, 왠지 HOL Block 문제를 해결해줄 수 있을 것만 같다.
멀티플렉싱
1.1의 파이프라이닝을 개선한 것인데, 하나의 Connection으로도 연속적인 요청을 할 수 있게 해준다.
이 때, Stream을 이용하는 것이다.

한쪽에서 프레임을 분리해서 하나의 Connection을 통해 전달하면, 다른쪽에서 끼워맞추는 식이다.
이로 인해, 여러 요청이나 응답을 병렬적으로 보낼 수 있고, 이것이 하나의 Connection 위에서 가능해진다.
또, HTTP/1.1을 어찌저찌 최적화 해보기 위해서 사용했던 여러 좋지 않은 방식들(concatenated files, image sprites, domain sharding )을 제거할 수 있게 해주고, 오버헤드를 줄여 페이지 로드 시간을 줄이는데 한 몫할 수 있다.
아래 애니메이션을 보면 더 도움이 될 것이다.

)

Stream Prioritization
HTML, CSS, 10개의 IMG를 요청했을 때, IMG를 먼저 내려주느라 CSS, HTML을 내려주지 못하면 렌더링이 늦춰져 사용자가 화면을 보게되는 시간이 더 지체될 것이다.
이런 상황을 HTTP/2에서 제공하는 Stream Prioritization로 해결할 수 있다.
각 스트림에 우선순위(1 ~ 256까지 정수 할당)를 부여해서 서버에서 응답을 보낼 순서를 지정해줄 수 있다.
서버 푸쉬
클라이언트에서 따로 요청을 하지 않아도, 클라이언트가 특정 요청을 했을 때, 클라이언트에 필요할 데이터를 서버에서 푸쉬해줄 수 있는(밀어 넣어줄 수 있는) 기능이다.
예를 들어서, 클라이언트에서 HTML만 요청했는데, js css까지 알아서 밀어넣어주는 형식이다.
그치만 사용하기가 매우 어렵고, 사용하는 사람이 많지 않아 조만간 없어질 기능이다.

헤더 압축
HTTP/1.1에서 Header가 너무 커지는 단점을 보완하기 위해 나온 기능이다.
HPACK 압축 형식을 이용해서 Header를 줄인다.
기존과 동일한 Header 목록은 생략
하며 허프만 코드로 인코딩
하여 성능을 개선하였다.
이렇게 많이 좋아졌지만, 1.1도 크게 문제가 느껴지지 않아 아직까지도 많은 사용자를 보유하고 있다.
HTTP/2는 과연 HOL Blocking 문제를 해결한 것일까?
실제로 해결했는가? 이에 대해선 사람마다 말이 다 다르다.
따라서 주관적으로 이해한 바에 대해서 쓰겠다. 틀린게 있을 수도 있다.
HOL Block은 TCP 의 고질병이다. TCP를 사용한 통신에서 패킷은 무조건 정확한 순서대로 처리되어야 한다.
따라서, 첫 연결 시 3-handshake 과정이나, 우선순위가 부여된 상황에선 특정 패킷 손실에 대해 Latency가 발생할 수 밖에 없을 것 같아 완전하게 HOL Blocking 문제가 해결되었다고 생각하지 않는다. (주관적인 생각이라 자신없다)
또한, HTTP/2는 단일 연결이라서, 이런 패킷 손실이 빈번하게 일어날 경우에 최대 6개까지 연결을 허용해준 HTTP/1.1보다 성능이 안좋기도 하다.
하지만, 멀티플렉싱으로 특정 스트림에서 문제가 발생했을 때, 다른 스트림에는 전혀 영향을 미치지 않기 때문에 HTTP/1.1에서 주된 목적으로 말한, HOL Blocking 문제는 해결됐다고 본다.
HTTP/3
이제 대망의 HTTP/3이다.
HTTP는 살짝 충격적이다. HTTP/2가 등장한지 4년정도 만에 새롭게 나왔다. 너무 빠르다.
지금도 HTTP/1.1을 이용하는 사람이 많고, HTTP/2도 제대로 정착되지 않은 상태인데 나왔다.
심지어 너무 파격적이다.
TCP 프로토콜을 버리고 UDP 프로토콜을 사용하였는데, 너무 파격적이지만 사실 알고보면 합리적이다.
지금부터 왜 UDP 프로토콜을 사용했으며, 어떤 점들이 개선되었는지 살펴보자.
QUIC
사실 정확하게는, UDP 프로토콜 기반이라기보단, UDP 프로토콜을 기반으로 만들어진 QUIC 기반이다.
하지만 QUIC가 UDP를 사용하므로 HTTP/3가 UDP를 사용한다고 하겠다.
우선 QUIC에 대해서 더 알아보기 전에 UDP와 TCP에 대해서 다시 짚어보자.
우선 간단하게 이 글을 읽고 개념을 적시는 것을 추천한다.
크게 TCP와 UDP의 차이는 아래와 같다.
TCP | UDP | |
---|---|---|
연결 방식 | 연결형 | 비연결형 |
패킷 교환 | 가상 회선 방식 | 데이터그램 방식 |
전송 순서 보장 | 보장 | 보장 X |
신뢰성 | 높음 | 낮음 |
전송 속도 | 느림 | 빠름 |
헤더 | 무거움 | 가벼움 |
커스터마이징 | 어려움 | 쉬움 |
여기서 신뢰성이 의미하는 바는, 전송한 패킷 순서나 유실된 패킷들을 검사해서 송신 측에서 보낸 데이터가 모두 수신측으로 잘 전달 되는가에 대한 것이다.
데이터그램 방식은 패킷간에 순서가 전혀 필요없는 독립적인 패킷을 사용한다는 것만 알아두자.
이런 신뢰성 있는 통신을 위해서 TCP는 3-way handshake를 이용하는 것이다.
하지만 이로인해 당연히 Latency가 생기게 되고, 통신의 성능을 향상시키기 위해선, 회선의 대역폭을 늘린다던가, 인프라에 돈을 더 쏟아붇는다던가.. 등등 TCP에서 정의한 부분 외적으로 건드려야한다. 하지만 이런 부분에선 성능 향상에 한계가 있기 마련이다.
HTTP/3에서는 이 때문에, 프로토콜 자체를 갈아버린 것이다.
TCP에서 UDP로 넘어오면서, 3-way handshake 과정을 날려버렸다. 또, TCP의 고질병인 HOL Blocking 문제도 날려버렸다.
"그럼 신뢰성도 버려버린건가..?"
아니다. 다른 방식으로 신뢰성을 확보하고 전송 속도도 얻어낸 것이다. 이게 어떻게 가능했는지 알아보자.
UDP 방식은 위 표에서 볼 수 있듯이, 헤더가 가볍다. 헤더가 가볍다라는 말은 무엇일까?
아래는 TCP의 헤더이다.

아래는 UDP의 헤더이다.

TCP를 커스터마이징 하려고 보자.. 너무 꽉 차있어서 가장 아래 Options 필드만 사용할 수 있는데, 무한정 사용할수는 없으니 또, 크기를 320bits로 한정지어 버렸다.
이마저도 사실 TCP 보완을 위해 나온 다른 옵션들이 차지하고 있어서 사실 우리가 사용할 수 있는 부분은 거의 없다.
UDP는 너무 깨끗하다. 그냥 데이터를 전송하는 것에만 초점을 맞추고 있다.
UDP 헤더에서 차지하고 있는 정보는 출발지, 목적지, 패킷의 크기, 체크섬이 있다.
체크섬은 패킷의 무결성 확인을 위한 필드인데, UDP에서는 TCP와 다르게 Required가 아니다.
이렇기 때문에 UDP는 TCP보다 신뢰성도 적으며 흐름 제어에서 불리하지만, 사용자가 어떻게 커스터마이징 하냐에 따라 TCP와 비슷하게 사용할 수 있다.
이러한 특징 때문에, 구글에서 Latency를 최소화하기 위해 TCP에서 머리 싸매고 고민하는 방향보단, "UDP에서 최적화된 신뢰성 확보를 구현해보자!"로 방향을 튼 것이다.
그렇다면, 진짜로 빨라졌을까?
QUIC Overview of Chromium Projects 에 의한 내용을 정리해 보겠다.
클라이언트에서 요청을 보내고 다시 클라이언트에 응답이 도달하기까지의 Cycle을 RTT(Round Trip Time)
라고 한다.
TCP + TLS 방식에선 3RTT가 필요하지만, QUIC에선 1RTT가 필요하다.

"빨라진건 알겠는데, 이렇게 해서 신뢰성을 어떻게 확보해?"
QUIC 방식에선 처음 요청에 데이터와 연결 설정에 필요한 데이터도 싸그리 한번에 보내버린다.
TCP는 너 확실히 맞아? 가능해? 물어보고 확신이 생겨야 데이터를 보내지만
QUIC는 그냥 다 보내고 너 알아서 판단해라는 방식인 것이다.
"TCP에서는 세션키를 서로 주고 받아서 그걸로 데이터를 암호화해서 보내니까 안전한건데, QUIC은 그럼 그냥 암호화도 안하고 보내는거야? 한다면 암호화는 어떻게 해"
개발자 도구 탭에서 네트워크 탭을 보면 Connection ID라는 것이 있다. 이를 이용해서 암호화한다. 과정은 다음과 같다.
- 처음에 목적지(서버)의 Connection ID로 특별한 키인
초기화 키
를 생성한다. 이걸로 통신을 암호화한다. - 서버에서 그 설정을 캐싱해둔다.
- 이후의 연결에 대해선 캐싱해둔 설정을 이용해서 바로 연결을 성립시킨다. (0RTT)
이런 방식으로 UDP위에서 신뢰성을 보장받을 수 있게 된다.
패킷 손실 감지 시간 단축
기존에 TCP에서는 패킷이 손실되었다는 것을 어떻게 알았을까?
클라이언트 쪽에서 패킷을 딱 보내면, 타이머를 킨다. 그러고 일정 시간이 지났는데도 서버쪽에서 대답이 안오면, 패킷이 손실됐다고 간주하고 다시 패킷을 재전송한다. 이런 방식으로 패킷 손실에 대한 처리가 이루어졌다.
이때에 문제점은 일정 시간이 지나서 손실됐다고 판단하고 재전송을 했을 때, ACK이 왔다고 치자.
그럼 헷갈린다. 처음에 보낸거에 대한 ACK인가.. 재전송한거에 대한 ACK인가...
이런 경우엔, 각 시퀀스 번호를 이용해서 언제의 패킷인지 추론을 했어야했다.
시퀀스 번호란 3-way handshake시 어떤 소통인지를 알기 위해 다루는 번호이다.
1. 첫번째 악수: 클라이언트가 서버로 시퀀스 번호를 seq 필드에 담아 보냄
2. 두번째 악수: 서버는 클라이언트가 보내준 시퀀스 번호를 1 증가시켜서 ack 필드에 담아 보냄
3. 세번째 악수: 클라이언트는 다시 서버로부터 받은 시퀀스 번호를 1 증가시켜서 자신의 ack 필드에 담아 보냄
이로써 어떤 요청에 대한 패킷인지 알 수 있다.
이런 혼란을 방지하기 위해 QUIC는 헤더에 패킷 번호 공간을 부여했다. 여기에는 패킷의 전송 순서가 담겨있다.
이 때, 재전송이 또 이루어지는 경우에는 패킷 번호를 증가시켜서 언제 보낸 패킷인지를 제대로 알 수 있게 하였다.
이를 통해 추론이 필요한 부분을 아예 제거해버려 패킷 손실 감지 시간 단축
을 이뤄낸 것이다.
이 외에도 추가된 방법이 4가지정도 더 있지만 문서만 던져 놓겠다.
멀티플렉싱
당연하게도 HTTP/2에서 지원했던 멀티플렉싱을 제공한다.
스트림간 서로 영향을 미치지 않기 때문에 HOL Block에서 HTTP/3도 자유롭다고 할 수 있다.
심지어 HTTP/3는 핸드쉐이킹 과정도 없다.
각 스트림간 독립적으로 움직이기 때문에, 순서대로 도착한다는 보장이 없다는 단점이 발생했지만, 이는 다른 방식으로 해결했다.
QPACK
헤더 압축을 위해 HTTP/2에서는 HPACK 기능을 이용했다.
이를 HTTP/3에서는 QPACK으로 교체하였다.
기존에 TCP 위에서 HPACK을 사용하다보니 순서에 의존되어 구현되었었다.
하지만 QUIC는 순서를 보장할 수 없어, 순서에 상관없이 도착하더라도 제대로 동작할 수 있도록 개선한 것이 QPACK이라고 한다.
IP가 바뀌어도 연결이 유지된다.
TCP의 경우엔, IP와 포트번호를 기준으로 연결을 판단한다. 따라서 IP가 바뀌면 다시 3-way handshaking을 해야했다. 즉, wifi를 사용하다가 다시 셀룰러를 썼다가 다시 wifi를 썼다가 할때마다 맨날 악수를 해야했던 것이다..
QUIC는 Connection ID를 사용하여 서버와 연결을 생성한다. Connection ID는 IP와는 완전 별개로 그냥 랜덤한 값일 뿐이다. 따라서 IP가 바뀌더라도 연결을 계속 유지하고 있을 수 있다.
이렇게 HTTP/0.9 부터 HTTP/3까지 알아보았다.
확실히 좋아진 면이 분명하지만, 우리나라처럼 인프라가 좋은 환경에서는 사실 크게 와닿을만한 개선은 아닐 것 같다.
하지만 인프라가 좋지 않은 나라에서는 꽤나 큰 차이가 느껴질 수 있는 개선이다라는 것 또한 사실이다.
HTTP/2가 나왔을 때, 보안적으로 큰 결함이 발견된 적이 있다. HTTP/3도 분명 완벽한 기술은 아닐 것이다.
이 때문에, HTTP/3가 표준적으로 사용되려면 조금 시간이 걸릴 것 같다는 생각이다.
하지만 현재 빠르게 서빙이 필요한 CDN, 크롬, 엣지 브라우저, curl, 유투브, 페이스북 등에서는 사용중이라고 한다.
여기에 나온 내용에 더불어 HTTP 메서드만 추가적으로 공부해준다면, HTTP에 관해선 충분하다고 생각한다. 혹시 부족한 부분이 있다고 생각하면 계속해서 추가하겠다.
내용 자체가 기존에 풀어서 간단하게 설명한 것보다 복잡하고 어렵다.
다른 곳도 참고해가며 최대한 이 글을 모두 이해하는 것을 추천한다.
참고 링크
'CS' 카테고리의 다른 글
HTTPS 란? (0) | 2022.02.01 |
---|---|
HTTP Method 정리 (0) | 2022.01.29 |
Stateful vs Stateless (HTTP) 에 대해서 알아보자 (2) | 2022.01.22 |
HTTP란? (0) | 2022.01.22 |
Protocol (프로토콜) 은 무엇이며, TCP는 무엇일지 간단하게 알아보자 (0) | 2022.01.22 |