HTTP 버전별 차이 — 프레이밍을 다시 푸는 일의 반복
첫 글에서 HTTP가 하부에 무관심한 구조 덕분에 메시지 형식은 그대로 두고 전송 방식만 갈아끼울 수 있었다고 적었다. 이 글이 그 실제 사례다. 다만 "버전이 올라가며 빨라졌다"는 식으로 늘어놓으면 아무것도 남지 않는다. 세 번의 개정을 하나의 사슬로 꿰는 문장은 이것이다.
HTTP의 진화는 매번 프레이밍을 다시 푸는 일이었다.
프레이밍은 바이트 스트림 위에서 메시지의 경계와 소속을 어떻게 표시할 것인가의 문제다. 연결을 타고 흘러오는 것은 그저 바이트의 나열이고, 어디까지가 하나의 메시지이며 그것이 누구 것인지는 프로토콜이 따로 표시해주지 않으면 알 수 없다. HTTP는 이 표시 방법을 바꿀 때마다 새 버전이 됐다.
- 1.1 — "끝을 표시할 장치를 만들었다"의 결과가 Keep-Alive다
- 2.0 — "메시지에 소속을 적을 자리를 만들었다"의 결과가 멀티플렉싱이다
- 3.0 — "통과 가능한 봉투를 골랐다"의 결과가 QUIC이다
각 절은 무엇이 문제였나 → 어떻게 풀었나 → 그 해법이 무엇을 남겼나 순으로 간다. 마지막 항목이 다음 절의 문제가 된다.
앞의 두 글을 전제로 한다. HTTP 메시지의 구조와 계층 경계는 HTTP — 무엇을 정하는 규약인가, 비연결성·무상태는 HTTP의 특징에서 다뤘다.
1. 정의 — 버전이 바뀐다는 것은 무엇이 바뀐다는 뜻인가
먼저 바뀌지 않은 것부터 확인하는 편이 빠르다.
GET은 세 버전 내내 GET이다. 200 OK도, 404도, Content-Type도 그대로다. 헤더의 이름과 의미, 메서드의 의미, 상태 코드의 분류 — 우리가 평소에 "HTTP를 안다"고 할 때 아는 것들은 거의 그대로 남아 있다. HTTP/3으로 요청을 보내도 애플리케이션 코드는 여전히 GET /posts/42를 쓰고 200을 받는다.
그럼 무엇이 달라졌는가.
달라진 것은 그 메시지를 어떻게 쪼개서 어떻게 실어 나르는가다.
| 버전 | 다시 푼 프레이밍 문제 | 답 |
|---|---|---|
| 1.0 | 메시지의 끝을 어떻게 아는가 | 연결을 닫아서 알린다 |
| 1.1 | 연결을 안 닫는다면 끝을 어떻게 아는가 | Content-Length / chunked |
| 2.0 | 한 연결에 섞여 오는 바이트가 누구 것인지 어떻게 아는가 | 바이너리 프레임 + 스트림 ID |
| 3.0 | 그 스트림들을 무엇 위에 실을 것인가 | QUIC (UDP 위) |
네 줄이 전부 같은 종류의 질문이다. 경계와 소속을 표시하는 방법을 바꾼 것이지, 주고받는 내용의 의미를 바꾼 게 아니다.
이것이 가능했던 이유는 첫 글에서 정리한 그대로다. HTTP가 자기 책임을 "메시지의 형식과 절차"로 좁게 잡았기 때문에, 그 아래를 통째로 교체해도 위쪽이 흔들리지 않았다. 본체가 전송 방식에 묶여 있지 않았다는 설계상의 여유가 세 번의 개정을 가능하게 했다.
2. 이유 — 해법이 다음 문제를 만든 사슬
2-1. HTTP/1.0 — 연결 하나에 요청 하나
초기 HTTP의 프레이밍은 극단적으로 단순했다. 응답을 다 보내면 연결을 닫는다. 연결이 닫히는 순간이 곧 "메시지 끝"이었으므로 길이를 알려줄 필요도, 경계를 표시할 필요도 없었다.
문서 하나가 곧 페이지의 전부였던 시절에는 충분했다. 문제는 페이지가 CSS·JS·이미지를 거느리기 시작하면서 드러났다. 리소스 하나를 받을 때마다 연결을 새로 맺고 끊어야 한다. 리소스가 30개면 연결도 30번이다.
연결을 만드는 비용은 공짜가 아니다. 왕복이 필요하고, 그 왕복은 서버가 아무리 빨라도 줄일 수 없는 물리적 지연이다. 받는 데 5밀리초 걸리는 작은 아이콘 하나를 위해 그보다 몇 배 긴 준비 시간을 매번 다시 낸다.
남긴 것 — 연결을 유지하고 싶다. 그런데 연결을 안 닫으면 메시지의 끝을 무엇으로 알리는가?
2-2. HTTP/1.1 — 끝을 표시할 장치를 만들다
여기서 순서를 바로잡아야 한다. 흔히 "1.1은 연결을 유지하기로 했다"고 요약하는데, 인과가 반대다.
연결을 유지하기로 해서 끝 표시가 필요해진 게 아니라, 끝을 표시할 장치가 생겨서 연결을 유지할 수 있게 된 것이다.
그 장치가 Content-Length와 Transfer-Encoding: chunked다. 메시지가 스스로 "여기까지"를 말할 수 있게 되자, 연결 종료라는 신호가 더 이상 필요 없어졌다. 그래서 연결을 계속 열어두고 다음 요청에 재사용할 수 있게 됐다. Keep-Alive는 원인이 아니라 결과다.
두 장치의 동작과 "연결이 닫혀도 아무것도 잃지 않는다"는 성질은 HTTP의 특징에서 다뤘다. 여기서는 그 위에서 이야기를 이어간다.
Host 헤더 — 또 하나의 소속 표시
1.1이 도입한 것 중 프레이밍만큼 중요한 게 하나 더 있다. 1.0의 요청은 이렇게 생겼다.
GET /index.html HTTP/1.0
User-Agent: Mozilla/2.0
요청 어디에도 어느 사이트에 온 요청인지가 없다. 서버는 자기 IP로 들어온 요청이니 당연히 자기 사이트라고 가정했다. 즉 IP 하나에 사이트 하나였다.
1.1은 Host를 필수로 만들었다.
GET /index.html HTTP/1.1
Host: a.example.com
이 한 줄 덕분에 a.example.com과 b.example.com이 같은 IP를 쓰면서도 서버가 둘을 구분할 수 있게 됐다. 가상 호스팅이 성립한 지점이고, 오늘날 리버스 프록시와 CDN의 라우팅이 전부 이 헤더 위에 서 있다. 어느 백엔드로 넘길지, 어느 프로젝트의 배포본을 내려줄지를 결정하는 근거가 결국 Host 한 줄이다.
여기서 한 가지 문제가 파생된다. HTTPS에서는 TLS 협상이 HTTP보다 먼저 일어난다. 그 시점에는 아직 HTTP 메시지가 없으니 Host도 없고, 서버는 어느 사이트의 인증서를 내밀어야 할지 알 수 없다. 그래서 TLS 쪽에 목적지 이름을 미리 알려주는 확장(SNI)이 필요해졌다. 상세는 HTTPS와 TLS Handshake에서 다룬다.
남긴 것 — 연결은 재사용하게 됐지만, 한 연결은 여전히 한 번에 한 요청씩이다.
2-3. 왜 요청 두 개를 그냥 섞어 보낼 수 없었나
여기서부터가 이 글의 본론이다. 연결이 열려 있는데, 요청 두 개를 그냥 연달아 보내고 응답도 오는 대로 받으면 안 되는가?
먼저 뭉개지기 쉬운 두 축을 갈라두자.
| 연결 재사용 | 한 연결의 동시 처리 | |
|---|---|---|
| 질문 | 요청마다 연결을 새로 맺어야 하는가 | 한 연결에 요청 여러 개를 동시에 진행할 수 있는가 |
| 1.0 | 안 된다 | 안 된다 |
| 1.1 | 된다 | 안 된다 |
| 2.0 | 된다 | 된다 |
"1.1은 응답이 와야 다음 요청이 가능하다"와 "1.1은 연결도 매번 새로 맺는다"는 자주 한 덩어리로 뭉쳐지는데, 서로 다른 축이고 해결된 시점도 다르다.
그럼 1.1은 왜 동시 처리를 못 했는가. 답은 프레이밍에 있다.
Content-Length와chunked는 "메시지 하나가 어디서 끝나는지"만 말해준다. 메시지가 섞이는 상황은 상정된 적이 없다.
1.1 메시지에는 "나는 어느 요청 소속인가"를 적을 자리가 없다. 응답 두 개의 바이트가 섞여 도착하면 클라이언트는 어느 바이트가 어느 요청의 것인지 구분할 방법이 없다. 그래서 순서에 기댈 수밖에 없었다.
순서가 곧 신원이었다.
파이프라이닝 — 스펙에는 있었지만 실제로는 없었다
1.1에도 요청을 연달아 보내는 기능 자체는 있었다. 다만 순서가 신원이므로 "응답은 반드시 보낸 순서대로" 라는 제약이 따라붙었다. 첫 요청이 무거운 API라면, 뒤의 가벼운 응답들이 이미 준비돼 있어도 첫 응답이 끝날 때까지 줄을 서야 했다.
이득은 적고 중간 프록시들의 구현은 불안정했다. 결국 브라우저들이 기본 비활성으로 두면서 명세에는 존재하지만 실제로는 쓰이지 않는 기능이 됐다. "파이프라이닝은 1.1에 없었다"가 아니라 "있었지만 안 쓰였다"가 정확하다.
그래서 브라우저는 연결 수를 늘렸다
프로토콜을 고칠 수 없으니 밖에서 우회했다. 오리진(scheme + host + port)마다 TCP 연결을 6개쯤 열어두고 병렬로 받는 방식이다. 프로토콜의 한계를 연결 개수로 산 것이다.
값이 만만치 않았다.
- 핸드셰이크 비용이 6배. 연결마다 준비 과정을 처음부터 다시 한다.
- 혼잡 제어가 연결마다 따로 논다. 각 연결이 저마다 작은 속도에서 시작해 조금씩 올리므로, 여섯 번을 전부 처음부터 다시 올린다.
- 자기 페이지의 리소스끼리 대역폭을 경쟁한다. 여섯 연결이 서로 남남이라 양보가 없다.
- 무엇이 급한지 말할 창구가 없다. CSS는 렌더링을 막고 이미지는 안 막는데, 그 차이를 프로토콜로 표현할 방법이 없다.
- 헤더 반복을 줄일 방법이 없다. 매 요청이 자기소개를 다시 하는 비용을 그대로 낸다.
- 6은 상한이다. 리소스가 50개면 여섯 개씩 아홉 번 줄을 선다.
남긴 것 — 우회로는 한계가 뚜렷하다. 프로토콜 안에 "소속을 적을 자리"를 만들어야 한다.
2-4. HTTP/2 — 자리를 만들다
HTTP/2의 핵심은 하나다. 메시지를 프레임으로 쪼개고, 모든 프레임 헤더에 스트림 ID를 박았다.
<!-- IMAGE: 위쪽은 HTTP/1.1이 오리진당 연결 6개로 병렬성을 사는 모습과 그 값(핸드셰이크 6배·slow start 6번·대역폭 경쟁·우선순위 불가·헤더 반복·6은 상한). 아래쪽은 HTTP/2에서 프레임마다 스트림 ID가 붙어 한 연결 안에서 인터리빙되는 모습 -->스트림 ID가 생기자 순서가 신원을 대신할 필요가 없어졌다. 응답 A의 프레임과 응답 B의 프레임이 뒤섞여 도착해도, 각 프레임이 자기 소속을 들고 있으니 받는 쪽에서 다시 갈라 담으면 된다. 준비된 응답부터 먼저 내보낼 수 있게 된 것이다.
왜 텍스트를 버리고 바이너리로 갔는가
이 자리를 만들려면 텍스트 형식으로는 곤란했다. 줄바꿈으로 구분되는 형식은 경계를 찾으려면 바이트를 훑어야 하고, 스트림 ID를 항상 같은 위치에 박아둘 자리가 없다. 메시지가 섞여 오는 상황에서 매번 훑어가며 파싱하는 것은 비효율적일 뿐 아니라 구현마다 해석이 갈릴 여지도 크다.
HTTP/2 프레임은 앞 9바이트가 항상 고정된 배치를 갖는다.
| 바이트 | 필드 | 크기 | 뜻 |
|---|---|---|---|
| 0–2 | Length | 3 | 이 프레임의 페이로드 길이 |
| 3 | Type | 1 | HEADERS, DATA, SETTINGS … |
| 4 | Flags | 1 | 끝 표시 등 |
| 5–8 | Stream ID | 4 | 어느 스트림 소속인가 |
9바이트만 읽으면 이게 무엇이고, 어디까지이고, 누구 것인지를 즉시 안다. 훑을 필요도 추측할 여지도 없다. 텍스트를 버린 것은 미적 취향이 아니라 이 성질을 얻기 위한 대가였다.
왜 연결을 다시 하나로 줄였나
스트림 ID가 있으면 연결을 여러 개 열 이유가 없어진다. 그리고 하나로 줄이자 2-3절에서 나열한 값이 그대로 뒤집힌다.
- 핸드셰이크는 한 번이면 된다.
- 혼잡 제어가 하나로 합쳐진다. 여러 연결이 각자 작은 속도에서 출발하는 대신, 한 연결이 대역폭을 제대로 쓴다.
- 리소스끼리 경쟁하는 대신 스트림에 가중치와 의존성을 매겨 우선순위를 표현할 수 있다. CSS를 먼저, 접히는 곳 아래의 이미지는 나중에.
- 그리고 헤더 압축(HPACK)이 비로소 유효해진다.
HPACK은 연결마다 헤더 테이블을 두고, 이미 보낸 헤더를 다음부터는 인덱스 번호로 대체한다. 여기서 중요한 건 "연결마다" 라는 조건이다. 연결이 6개면 테이블도 6개이고, 각 테이블이 따로따로 처음부터 학습해야 한다. 같은 쿠키를 여섯 번 다시 배우는 셈이다. 연결을 하나로 줄인 덕분에 헤더 압축이 실질적인 이득을 내게 됐다.
앞 글에서 무상태의 대가로 "매 요청이 자기를 다시 소개해야 한다"를 짚고, 그 비용을 프로토콜이 어떻게 갚았는지는 이 글로 넘긴다고 했다. 그 답이 HPACK이다. 무상태라는 성질 자체는 손대지 않았다. 여전히 서버는 아무것도 기억하지 않고, 요청은 매번 필요한 정보를 전부 싣는다. 다만 같은 연결 안에서 두 번째부터는 그것을 인덱스로 줄여 보낼 뿐이다. 의미는 그대로 두고 표현만 압축한 것이다.
남긴 것 — 이제 모든 스트림이 TCP 연결 하나를 탄다.
2-5. HOL 블로킹은 두 겹이다
"HTTP/2가 HOL 블로킹을 해결했다"는 서술을 자주 본다. 절반만 맞다. HOL 블로킹은 층이 다른 두 개이고, HTTP/2가 푼 것은 위쪽 하나뿐이다.
<!-- IMAGE: 3단 구조. HTTP/1.1의 애플리케이션 계층 HOL(느린 응답 A가 B·C를 막음) / HTTP/2가 그것을 풀었지만 패킷 유실 시 무관한 스트림까지 멈추는 전송 계층 HOL / HTTP/3이 순서 보장을 스트림 단위로 옮겨 A만 멈추는 모습 -->- 애플리케이션 계층 HOL — 앞 응답이 끝나야 다음 응답이 나갈 수 있다. 순서가 신원이라 뒤바꿀 수 없었던 1.1의 문제이고, 스트림 ID가 이것을 풀었다.
- 전송 계층 HOL — 앞 패킷이 유실되면, 뒤 패킷이 이미 도착해 있어도 커널이 응용 프로그램에 올려주지 않는다. HTTP/2는 여기에 손댈 수 없다.
원인은 TCP가 제공하는 보장의 단위에 있다. TCP는 연결 단위로 순서를 보장하며, 자기가 나르는 바이트가 어느 스트림 것인지 알지 못한다. 알 이유도 없다. 스트림이라는 개념 자체가 그 위층의 발명이기 때문이다.
그래서 스트림 A에 속한 패킷 하나가 유실되면, 이미 온전히 도착해 있는 스트림 B와 C의 바이트까지 전부 대기 상태가 된다. 재전송된 A의 패킷이 도착해 구멍이 메워져야 비로소 순서대로 올라간다. B와 C는 A와 아무 상관이 없는데도 그렇다.
여기서 아이러니가 생긴다.
연결을 하나로 줄인 대가로 패킷 손실에 더 취약해졌다.
연결이 6개이던 시절에는 하나가 막혀도 나머지 다섯은 그대로 흘렀다. 손실의 피해가 그 연결 안에 갇혔던 것이다. 연결을 하나로 합치면서 그 격리가 사라졌다. 2-4절의 해법이 그대로 2-5절의 문제를 만든 셈이다.
남긴 것 — TCP를 쓰는 한 이 문제는 풀 수 없다. 전송 계층 자체를 바꿔야 한다.
2-6. HTTP/3 — 통과 가능한 봉투를 골랐다
HTTP/3의 판단은 명확하다. TCP 안에서는 고칠 수 없으니 전송 계층을 통째로 교체한다. 그 자리에 들어온 것이 QUIC이다.
QUIC이 한 일의 핵심은 순서 보장의 단위를 연결에서 스트림으로 옮긴 것이다. 스트림 A의 패킷이 유실돼도 B와 C는 그대로 응용 계층에 올라간다. 멈추는 것은 A뿐이다. 2-5절의 전송 계층 HOL이 여기서 사라진다.
왜 하필 UDP인가
이 절의 진짜 질문은 이것이다. 성능만 놓고 보면 UDP는 좋은 선택이 아니다. UDP 헤더 8바이트는 QUIC 입장에서 거의 쓸모없는 낭비이고, TCP가 제공하던 신뢰성·순서·혼잡 제어를 전부 다시 구현해야 한다. 새 전송 프로토콜을 설계하는 편이 기술적으로 훨씬 깔끔하다.
그럼에도 UDP를 고른 이유는 성능이 아니라 배포 가능성이다.
핵심 개념은 프로토콜 경화(ossification) 다. IP 헤더에는 그 안에 든 것이 무엇인지 나타내는 프로토콜 번호가 있는데, 경로상의 NAT·방화벽·각종 미들박스가 실질적으로 6(TCP)과 17(UDP)만 안다. 새 번호를 붙인 패킷은 목적지에 도착하지 못하고 중간에서 버려진다. 악의가 아니라 그냥 오래된 펌웨어이고, 전 세계에 깔린 수억 대의 장비를 업데이트할 방법은 없다.
이게 이론이 아니라는 증거가 있다. SCTP다. 2000년에 표준화된 이 전송 프로토콜은 TCP의 결함을 상당수 고쳤고, 무엇보다 멀티스트리밍으로 전송 계층 HOL을 이미 해결해두었다. QUIC이 20년 뒤에 푼 문제를 그때 이미 푼 것이다. 그런데 공용 인터넷에서는 쓰이지 않는다. 미들박스를 통과하지 못하기 때문이다.
그래서 이렇게 정리할 수 있다.
QUIC이 한 일은 새로운 발명이라기보다, 이미 있던 해법을 통과 가능한 봉투에 넣은 것에 가깝다.
TCP와 UDP는 인터넷이 상용화되기 전인 1980~81년에 굳었다. 당시 인터넷에 연결된 호스트는 수백 대 규모였고 NAT도 방화벽도 없었다. 경화는 90년대 후반 NAT과 방화벽이 퍼지면서 함께 굳어졌다. 그 배경은 IP 주소와 서브넷, NAT에서 다룬다.
커널이라는 또 하나의 장벽
배포 문제는 경로 위에만 있는 게 아니다. TCP 구현은 커널 안에 있다. 혼잡 제어 알고리즘 하나를 바꾸려 해도 커널 업데이트가 필요하고, 그것이 전 세계 사용자 기기까지 퍼지는 데는 몇 년이 걸린다.
QUIC은 여기서도 다르다. UDP 소켓만 열고 나머지 로직을 유저스페이스에서 처리한다. 그래서 브라우저나 앱을 업데이트하는 것만으로 새 전송 로직이 배포된다. 개선의 반영 주기가 몇 년에서 몇 주로 줄어든다.
TLS를 얹지 않고 안에 넣은 이유
QUIC은 TLS를 별도 계층으로 올리지 않고 프로토콜 안에 통합했다. 보안 때문만이 아니다. 프로토콜의 내부가 평문으로 노출되면 미들박스가 그걸 들여다보고 "이해한 대로" 간섭하기 시작하고, 그 순간부터 프로토콜은 다시 굳는다. 경화를 겪고 나온 설계라 처음부터 그것을 방어한다.
덤으로 얻은 것도 있다. 연결 수립과 암호화 협상이 한 과정으로 합쳐지면서 준비에 드는 왕복이 줄었고, 한 번 붙었던 서버에는 곧바로 데이터를 실어 보내는 것도 가능해졌다. 연결을 IP·포트가 아니라 별도의 식별자로 구분하기 때문에 네트워크가 바뀌어도 연결이 유지되는 연결 마이그레이션도 이 설계에서 나온다.
3. 판단 — 그래서 무엇을 쓰는가
3-1. 새 버전이 나왔다고 옛 버전이 사라지지는 않는다
HTTP/1.1은 여전히 널리 쓰인다. 그럴 만한 이유가 있다.
서버 간 내부 통신이나 단순한 API 호출처럼 리소스 개수가 적고 연결 재사용만으로 충분한 경우, HTTP/2의 이득은 거의 없다. 멀티플렉싱은 동시에 받을 것이 많을 때 의미가 있는데, 요청 하나 보내고 응답 하나 받는 패턴에서는 섞을 것이 없다. 반대로 구현은 단순해서 디버깅과 중계가 쉽다. 텍스트라 눈으로 읽힌다는 것도 실무에서는 적지 않은 가치다.
3-2. HTTP/2의 이득은 작은 리소스가 많을수록 커진다
이건 2-3절과 2-4절을 그대로 뒤집어 읽으면 나온다. 6개 연결로 아홉 번 줄을 서야 했던 상황이 사라지는 것이므로, 리소스가 잘게 많이 쪼개져 있을수록 차이가 크다.
반대로 큰 파일 하나를 받는 경우에는 거의 차이가 없다. 스트림이 하나뿐이면 섞을 것도, 우선순위를 매길 것도, 반복해서 압축할 헤더도 없다. 멀티플렉싱의 이득은 스트림 개수에서 나온다.
3-3. HTTP/3의 이득은 손실이 있는 곳에서 나온다
HTTP/3이 푼 것은 전송 계층 HOL이었다. 그러므로 이득도 정확히 그 문제가 발생하는 환경에서만 나온다.
손실이 없으면 전송 계층 HOL도 없다.
모바일 네트워크나 불안정한 무선처럼 패킷 손실률이 높은 환경에서는 차이가 뚜렷하다. 반대로 데이터센터 내부나 안정적인 유선 환경에서는 HTTP/2와 큰 차이가 나지 않는다. 손실이 드물면 애초에 막힐 일이 없기 때문이다. 네트워크가 자주 바뀌는 이동 환경에서 연결이 유지된다는 점도 같은 맥락에서 모바일 쪽에 유리하게 작용한다.
3-4. 과거의 최적화가 뒤집힌다
가장 실질적인 판단 지점은 여기다. 1.1 시절에 정답이던 기법들이 2 이상에서는 오히려 해가 된다.
- 도메인 샤딩 — 리소스를
a.example.com,b.example.com으로 나눠 오리진을 늘리면 연결 상한도 6개에서 12개로 늘어난다. 1.1에서는 유효한 우회였다. 하지만 HTTP/2에서는 연결을 일부러 쪼개는 짓이 된다. 멀티플렉싱의 이득이 깎이고, 연결마다 혼잡 제어와 헤더 테이블이 따로 놀며, 도메인이 늘어난 만큼 준비 비용도 늘어난다. - 스프라이트 이미지, 파일 합치기 — 요청 횟수 자체가 비용이던 시절의 대응이다. 요청이 싸지면 합칠 이유가 줄어든다. 오히려 크게 합쳐두면 한 조각만 바뀌어도 전체를 다시 받아야 하고, 당장 필요 없는 부분까지 함께 받게 된다.
두 기법이 뒤집힌 이유는 같다. 둘 다 "요청과 연결이 비싸다"는 전제 위에 세워진 대응이었는데, 그 전제가 사라졌기 때문이다. 최적화 기법을 규칙으로 외우면 이런 역전을 놓친다. 어떤 제약을 우회하려던 것인지까지 함께 기억해야 그 제약이 없어졌을 때 손을 뗄 수 있다.
로딩 성능을 사용자 관점에서 측정하는 지표는 Core Web Vitals에서 다룬다.
3-5. 정리
세 번의 개정을 한 줄씩 다시 쓰면 이렇다.
- 1.1 — 메시지가 스스로 끝을 말하게 만들었다. 그래서 연결을 재사용할 수 있게 됐고, 한 번에 한 요청씩이라는 제약이 남았다.
- 2.0 — 프레임마다 소속을 적게 만들었다. 그래서 순서에서 풀려났고, 모든 스트림이 연결 하나를 타게 됐다.
- 3.0 — 순서 보장의 단위를 스트림으로 옮겼다. 그러려고 전송 계층을 갈아끼웠고, 갈아끼울 수 있는 봉투가 UDP뿐이었다.
세 줄 모두 경계와 소속을 어떻게 표시할 것인가에 대한 답이고, 각 답이 다음 문제를 만들었다. 그래서 이 글의 결론은 처음의 문장으로 돌아간다.
HTTP의 진화는 매번 프레이밍을 다시 푸는 일이었다. 그리고 그것이 가능했던 이유는 HTTP가 메시지의 의미와 나르는 방법을 처음부터 분리해두었기 때문이다.
관련 글
- HTTP — 무엇을 정하는 규약인가 — "하부에 무관심해서 전송 계층을 교체할 수 있었다"의 원문. 이 글이 그 사례다.
- HTTP의 특징 — 비연결성과 무상태 — 끝을 표시하는 두 장치, 그리고 HPACK이 갚은 무상태의 대가.
- TCP는 어떻게 신뢰성을 만드는가 — 전송 계층 HOL이 왜 TCP의 순서 보장에서 나오는지, UDP는 무엇을 보장하지 않는지.
- HTTPS와 TLS Handshake —
Host보다 먼저 필요한 이름(SNI)과 QUIC의 TLS 통합. - IP 주소와 서브넷, NAT — 경화를 만든 장비들.
- Core Web Vitals — 뒤집힌 최적화 기법들이 실제로 무엇을 겨냥했는지.