[F-Lab 모각코 챌린지 32일차] HTTP 변천사

부추·2023년 7월 2일
0

F-Lab 모각코 챌린지

목록 보기
32/66

TIL

http 변천사



초간단하게 알아보는 HTTP/0.9부터 HTTP/3까지의 내용 !!! 머릿속에 남겨놓고 싶어서 이런저런 자료를 찾아보았다.

1. HTTP/0.9 : 원시 HTTP

HTTP란 Hyper Text Transfer Protocol로써, www를 통해 hyper text를 요청 - 응답 구조로 교환하기 위한 프로토콜이다. 최초의 HTTP는 정말 단순히 "요청"에 대한 "응답"만을 받기 위한 프로토콜이었다. 요청은 <GET 요청경로> 형태로 단 한 줄 만이 존재했고 응답 역시 단순 HTML만이 있을 뿐이었다. 헤더? 이딴거 없었다. 또한 응답을 받으면 TCP 커넥션을 바로 끊어버렸기 때문에 사용자가 서버에 여러번 요청을 날려도 매 요청마다 핸드셰이킹 과정을 거쳐야했다.

버전 0.9라는 것도, 지금 와서 최초의 HTTP를 표현하기 위해 만든 숫자일 뿐이지 그 때는 이것이 유일한 HTTP였다.



2. HTTP/1.0 : 표준화 시도 start

1) HTTP 메세지 변화

HTTP 버전을 1.0으로 설정하고 표준화가 시작됐다. 이제 요청라인이 <HTTP메소드 경로 버전>으로 갖춰졌다.

GET / HTTP/1.0

응답 라인에 상태코드가 추가되었다. 상태 코드는 서버 입장에서 클라이언트의 요청이 어떻게 처리되었는지 나타내주는 표식이다.

HTTP/1.0 200 OK

요청 HTTP 메소드인 HEAD, POST가 추가되었다. POST는 서버에 어떤 리소스 자원을 제출할 때, HEAD는 응답 HTTP 메세지의 헤더만을 보고 싶을 때 사용했다. 헤더?

그래. 헤더가 추가되었다. 헤더는 "항목이름:항목" 행 단위로 HTTP 요청/응답라인 바로 밑에 존재하는 놈들인데, HTTP 데이터에 대한 여러 메타데이터를 담고 있다.

불-편
그러나 여전히, HTTP/1.0은 Connection 헤더를 통해 Keep-alive 설정을 하지 않으면 TCP 연결이 해제되어버리고(default가 closed), 여러 부분에서 완전한 표준화가 이뤄지지 않았다는 한계점이 있었다.



3. HTTP/1.1 : 마침내 표준화

HTTP/1.0이 나온지 얼마되지 않아, 현재의 www에서도 여전히 널리 이용되는 HTTP/1.1 프로토콜이 탄생했다. 버전 1.1은 이전 버전들이 가진 한계를 극복하기 위해 많은 변화가 생겼다. 굵직한 것들만 알아보자!

1) 커넥션 재사용과 파이프라이닝

HTTP/1.0에서는 Connection 헤더에 Keep-alive를 지정해주지 않으면 요청-응답 사이클이 끝날 때마다 TCP 연결이 끊긴다고 언급한 바 있다. TCP는 신뢰성 있는 통신을 지향하기 때문에 연결과 패킷 교환 과정이 느리다. 연결 시작 과정에서만 2RTT가 소요되고.. 끊는데도 4-way handshaking 과정을 거쳐야한다.

HTTP/1.1에서는 Connection:Keep-alive가 default이다. timeout의 시간과 max값의 req 이내라면 기존의 TCP 연결을 끊지 않고 같은 클라이언트-서버 트랜잭션이 유지된다. 거기다가 "파이프라이닝"이라는 것도 가능해졌는데, 아래를 보자!

사용자의 다음 요청이 서버의 응답을 받은 뒤에 보내진다면 부정적 경험이다. 웹페이지를 로딩하는데 HTML 이외에 CSS, JS파일, 그리고 다른 서버에서 받아와야하는 리소스들(광고 등)에 대한 요청이 필요한데 그것들이 모두 하나하나 응답을 받은 뒤 요청이 나간다면 페이지 로딩이 굉~장히 느려질 것이다. (사진 왼쪽)

그러나? 파이프라이닝이 이뤄진다면? 서버 응답이 도착하기 전에 페이지 로딩을 위한 요청을 와바박! 보내서 요청에 대한 응답을 와바박! 받으면 된다. 도착 순서는 어차피 전송 계층에서 알아서 맞춰서 응용계층으로 올려보내줄테니 사실 응용 계층에서만 보자면 문제 없다. 그런데 문제 있다. HOLB(Head Of Line Blocking)이라는 문제인데.. 이것은 2.0을 알아볼 때 다시 언급하겠다.


2) 캐시 제어

같은 문서에 대해 여러번 요청이 오거나 읽기 전용 문서에 많은 요청이 올 때, 원서버가 모든 요청에 대한 응답을 처리하기 힘들다. 이 때 캐시 서버에 사본을 만들어 대신 응답하게 하는데, 이것이 캐시다.

서버는 mutable한 캐시 데이터에 대해선 캐시 서버의 If-None_Match 항목과 원서버의 ETag 헤더를 이용해 두 값을 비교함으로써 리소스의 "신선함"을 확인한다. 재검사를 통과했을 경우 원서버는 캐시 서버에게 304 Not Modified응답을, 실패했을 경우 200 OK와 함께 변경된 리소스를 응답 body에 내려 보내준다.


3) 여러가지 헤더들!

HTTP/1.0에는 여러가지 헤더들이 추가됨으로써 조금 더 유연한 HTTP 동작이 가능해졌다. Host 헤더를 통해 가상호스팅 서버가 동작 가능하도록 했고,(가상 호스팅 서버 : 하나의 IP주소에 여러 서버를 host) Accept 헤더를 통해 클라이언트나 서버가 선호하는 Content-type을 지정할 수 있도록 했다. 이는 서버와 클라이언트가 콘텐츠 협상을 가능하도록 하여 서로가 원하는 타입의 데이터로 원활한 정보 교환 및 기능 동작이 가능하도록 해주었다.


4) 요청 메소드 더욱 확장

기존의 GET/POST/HEAD를 넘어 PUT, PATCH, DELETE 등의 메소드 역시 추가되었다.


# BUT... HOL Blocking & Too many Headers

커넥션 재사용에서 언급했던 내용인데, TCP 자체가 가진 한계점 때문에 HTTP/1.1은 한계를 가진다. Head Of Line Blocking은 신뢰성 있는 연결을 지향하는 TCP에서 어.쩔.수.없.이. 발생한다.

TCP 환경에서 서버가 1,2,3번 패킷을 순서대로 클라이언트에게 보냈다고 하자. 클라이언트는 1,3번 패킷을 받았다. TCP 헤더의 sequence number를 비교하고 누락을 캐치한 클라이언트는 서버에게 2번 패킷을 다시 보내라는 요청을 한다. 그 뒤, 서버에게서 2번 패킷을 다시 받을 때까지 이미 들어온 3번 패킷에 대한 처리를 하지 않는다. 이것이 HOL Blocking, 앞 패킷(Head Of Line)의 문제로 뒤 패킷이 전부 대시 상태가 되어버리는(Blocking)문제다.

HTTP/1.1에선 여러 TCP 커넥션을 지원하는 것으로 해당 문제를 어느정도 해결하고 있지만, 트랜잭션을 늘리는 것은 서버와 클라이언트 모두에게 부담이 되는 작업이다.

또한! 여러가지 헤더들이 추가된 덕에 확실히 정보 교환 및 HTTP 운용에 편리함이 생긴 것은 사실이었지만 HTTP 메세지의 헤더가 차지하는 공간이 너무 많아져 "뚱뚱한 헤더"문제가 제기되었다. 가볍게 보낼 수 있는 요청과 응답에 불필요한 수십 개 단위의 헤더가 추가되는건 .. 확실히 문제로 보인다.



4. HTTP/2.0 : 더욱 나아질거야!!!

HTTP/1.1을 조금 더 발전시켜보자? 하고 나온 프로토콜이다. HTTP/2.0에선 어떻게 통신할까?

1) Binary Framing Layer

TCP/IP 3-4계층 사이에 존재하는 framing 레이어다. HTTP/1.1에서 header와 body로 나뉘었던 HTTP 메세지는 "frame"이라는 바이너리 데이터로 변환된다. 스트림과 프레임을 이용한 HTTP/2.0의 통신을 설명하기 위해 통신 과정에서 사용되는 용어를 알아보자.

frame : HTTP/2.0의 Binary Framing 레이어에서 사용하는 통신 단위. TCP 전송계층의 바로 위, HTTP 응용 계층의 바로 밑에 존재하는 계층에서 사용된다. 고유의 스트림 ID를 가진다.
스트림 : 하나의 TCP 커넥션 안에서 생성되는 여러 개의 양방향 frame 통로. n번 스트림을 이용하는 프레임의 ID는 n번이다.(라고 생각해도 된다)
요컨데, 각 스트림을 사용하는 프레임들은 프레임의 스트림 ID로 구분된다. 아래 사진을 보면, 각 스트림(노란 통로)을 사용하는 프레임(초록 박스)에는 (stream N)이라는 스트림 번호가 붙여진 걸 알수 있다.

이렇게 구분되는 프레임들을 통해 멀티플렉싱으로 통신을 하게 된다. 멀티플렉싱?


2) 멀티플렉싱!

하나의 TCP 트랜잭션 안에서 "동시"에 요청을 보내고 "동시"에 응답받는 멋진 과정이다. 에, 어떻게? 스트림의 설명을 기억하자. 하나의 TCP 안에서 생성되는 여러 개의 양방향 frame 통로!!!

3개의 요청이 있다고 가정해보자. 3개의 요청은 각각 1번 스트림, 2번 스트림, 3번 스트림을 이용하게 된다. Binary Framing Layer에서 각 프레임들은 프레임이 가진 스트림 ID에 의해 구분된다. 때문에 해당 패킷들이 뭉텅이로 서버에 요청이 가도, 패킷을 까 프레임을 확인했을 때 스트림으로 구분이 되므로 한번에 온 여러 요청을 구분할 수 있는 것이다.

노랑, 주황, 빨강에 대한 요청이 동시에 가고, 각각의 응답이 여러 개의 프레임으로 분리되어 도착한다. 뒤죽박죽인 응답을 어떻게 각 요청에 맞게 매핑하지? 스트림ID이다. 스트림 ID는.. TCP 헤더의 목적지 포트 번호와 비슷하다는 생각이 든다.


3) Compress header

HTTP/1.1의 문제점으로 제기되었던 "뚱뚱한 헤더" 문제도 2.0으로 와서는 어느정도 해결되었다. HPACK이라는 기술을 이용해 헤더 압축이 이뤄졌다는데..... 이 알고리즘과 기술에 관한건 HTTP 역사를 이해하는데 필수적이진 않은 것 같아 패스.


4) Server push!

기존 HTTP에서, 페이지를 로드하기 위한 모든 리소스는 클라이언트가 요청해야했다. 페이지 골격 구성하기 위한 HTML, 레이아웃을 위한 CSS와 JS, 각종 광고와 이미지 로딩을 위한 또다른 요청.... 아무리 요청 파이프라인을 해도 클라이언트 입장에서 해당 요청들을 하나하나 보내는 것은 아무래도 많이 번거롭다. HTTP/2.0에서는 이를 개선하여 클라이언트의 추가 요청 없이도 웹페이지 로딩에 필요한 모든 리소스를 한번에 push할 수 있도록 했다.


# 여전히 느린 TCP

HTTP/2.0역시 TCP 위에서 도착한다. TCP는 순서를 지키는 프로토콜이고, 그 말은 역시나 .. 2.0 버전에서도 HOLB를 피할 수 없다는 의미이다! 앞서 설명한 binary framing layer는 3-4계층 사이에 있는 프로토콜이기 때문에, 전송 계층에서 일어나는 blocking 문제는 피할 수 없다. 아예 전송계층에 내려가서 이를 뜯어고치지 않는한, TCP 위에서 동작하는 모든 프로토콜은 HOLB를 겪을 수밖에 없다.

또한 현대에 들어선 거의 대부분의 상업 사이트들이 https를 도입하기 때문에 기존 TCP의 3-way handshake에 더해 TLS 핸드셰이크까지 거쳐야한다. 이에 관한 내용은 링크 참고. 이는 HTTP 트랜잭션을 위한 RTT 부담이 더욱 가중되는 결과를 불러와버렸다.



5. HTTP/3.0 : 이제 이별하자 TCP..

HTTP/3.0은 HOLB를 해결하기 위한 QUIC라는 프로토콜을 사용한다. QUIC는 UDP 기반 프로토콜로, TCP에서 이뤄지는 느린 연결을 개선한 HTTP/3.0의 프로토콜이라고 할 수 있다. 오늘날 웬만한 api, 혹은 웹 통신은 HTTP로 이뤄지고 있기 때문에 HTTP/3.0이 표준으로널리 정착하게 된다면 훨씬 빠른 네트워크 데이터 교환이 이뤄질 것이다!

1) QUIC가 빠른 이유 : 내장 TLS


전송계층 바로 위, 응용계층 바로 밑에 QUIC 계층이 위치한다. 기존의 TCP는 보안 연결을 위해 TCP 핸드셰이크를 거친 후 TLS 핸드셰이크를 추가로 진행해야했다. 그러나 QUIC는 QUIC 자체적으로 TLS 핸드셰이크 과정이 내장되어있다. 최초의 HTTP 요청을 보내기 위해 최소 3RTT가 소요되는 기존의 TCP와 다르게, QUIC는 핸드셰이크 과정에서 바로 키 교환까지 완료하므로 1RTT만에 연결 수립을 완료할 수 있다!


2) UDP 기반 : HOLB X

아무리 바이너리 프레임을 쓴다고 해도, 결국 TCP에서 올려주는 패킷을 받는 HTTP/2.0은 TCP 계층에서 발생하는 HOLB 문제를 피할 수 없었다. 다른 스트림의 프레임을 위해 상관도 없는 패킷들이 전송 계층 단에서 기다리고 있는 것은 HTTP 성능 저하를 일으킬 가능성이 굉장히 컸다.

QUIC에서 멀티플렉싱은 전송 계층에서 이뤄진다. 그 말은, 서버의 응답 메세지 프레임의 순서가 다르더라도 클라이언트는 스트림 ID에 맞춰 프레임을 재조립하기 때문에 상관없는 스트림의 프레임때문에 특정 응답 프레임들이 블락되는 걱정이 없다는 얘기다. 예시를 통해 이해를 깊이 해보자.

위처럼 TCP와 QUIC 상황에서 3개의 패킷이 클라이언트 응답으로 도착해야하는 상황에서, 2번 패킷이 누락되었다고 생각해보자.

# HTTP/2.0 TCP의 경우

  1. 1, 3번 패킷의 byte 까보고 스트림 2번 프레임의 누락을 확인
  2. 서버에 스트림 2번 패킷 재전송 요청
  3. 그동안 상관도 없는 스트림 1번의 450-999 프레임은 서버의 응답 순서에 맞추기 위해(TCP) 대기 -> HOLB@@!!!!

# UDP의 경우

  1. 1, 3번 패킷의 byte 까보고 스트림 2번 프레임의 누락을 확인
  2. 서버에 스트림 2번 패킷 재전송 요청
  3. 0-999 바이트가 모두 존재하는 스트림 1번의 프레임이 먼저 처리(순서 상관없는 UDP)

차이가 느껴지나? 서버가 보낸 순서대로 클라이언트에서 재조립 할 필요가 없다는게 핵심이다.


3) UDP는 신뢰성이 없는데?

喝!!

방금 QUIC에서 패킷 처리가 어떻게 이뤄졌는지 봤다면 이런 얘기를 할 수가 없다.

  • 스트림 ID 기반으로 각 요청에 대한 응답 분리
  • 프레임의 바이트 번호를 통해 패킷 누락 확인 및 오류 발견시 재전송 요청

QUIC는 위 과정이 가능하다. 때문에 서버 측에서 무식하게 데이터그램을 보내는 기존 UDP와는 다르게 스트림 별로 도착 순서가 보장되며, 재전송 과정도 존재한다. UDP와 TCP의 장점을 쏙쏙 골라가면서도, TLS 과정을 합쳐 더욱 더 빠른 HTTPS 데이터 교환이 가능하게 해주는 것이 바로! HTTP/3.0인 것이다.



REFERNCE

https://velog.io/@yesbb/HTTP3%EA%B9%8C%EC%A7%80-%EB%B2%84%EC%A0%84%EB%B3%84-%EB%B3%80%EC%B2%9C%EC%82%AC#1%EC%BB%A4%EB%84%A5%EC%85%98%EC%9D%98-%EC%9E%AC%EC%82%AC%EC%9A%A9-

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글