들어가며
현대 웹 기술은 대부분이 HTTP 위에서 동작한다. 웹 개발자에게 HTTP를 이해하는 것은 단순히 서버와 클라이언트가 통신하는 방식을 아는 것을 넘어, 웹 전체의 동작 원리를 이해하는 핵심이라고 할 수 있다. HTTP 프로토콜이 발전해온 역사와 각 버전이 릴리즈된 배경과 특징, 문제점을 알아보자.
HTTP
- 웹에서 데이터를 주고받기 위한 통신 프로토콜
- 사용자가 웹 주소를 입력하면 브라우저가 HTTP 요청을 보내고, 서버는 해당 요청에 대한 응답을 보내주는 '클라이언트-서버', '요청-응답' 모델로 작동
- 1989년 팀 버너 리(Tim Berners-LEE)에 의해 제안

HTTP/0.9
- TCP/IP 링크 위에서 동작하는 ASCII 프로토콜
- HTML을 가져오기만 하는 GET 메서드만 지원
- HTTP 헤더 X, 상태 코드 X
- 응답도 HTML 파일 자체만 보내줌
- 서버와 클라이언트 간의 연결은 모든 요청 후에 닫힘(closed)
HTTP/1.0
배경
- 1994년 W3C가 만들어지며 HTML의 발전을 도모하게 되었고, 이와 비슷하게 HTTP 프로토콜 개선에 초점을 맞추기 위해 HTTP-WG(HTTP Working Group)가 설립됨
- 웹 브라우저, 인터넷 인프라가 빠르게 진화하며 이제는 단순히 하이퍼텍스트 문서 뿐만 아니라 멀티미디어 데이터나 메타데이터 등 다양하고 상세한 컨텐츠가 필요해짐
- 1996년 HTTP-WG는 HTTP 1.0 프로토콜 통신 스펙에 관한 기술 문서인 RFC 1945를 발표
특징
- 기본적인 HTTP 메서드와 요청/응답 헤더 추가
- Request 메시지: GET 요청이 시작되는 줄에 Path와 HTTP 버젼, 다음 줄로 이어지는 헤더값
- Response 메세지: 200 OK 이후 응답 상태로 이어지는 응답 헤더값
- 상태 코드(status code)가 응답의 시작 부분에 붙어 전송되어, 브라우저가 요청에 대한 성공과 실패를 알 수 있고 그 결과에 대한 동작을 할 수 있게 됨
- 응답 헤더의 Content-Type 덕분에 HTML 파일 형식 외에 다른 문서들을 전송하는 기능 추가
- 단기커넥션: connection 하나당 1 Request & 1 Response 처리 가능
문제점
- 비연결성(connectionless)으로 인한 단기 커넥션(Short-lived connection)
- TCP 커넥션 하나당 하나의 요청 하나의 응답 처리 ⇒ 서버에 자원을 요청할때마다 매번 연결(Handshake)-응답-종료 반복 ⇒ 서버 부하 비용 증가, 성능 저하
HTTP/1.1
배경
- HTTP 1.0의 단점을 커버하기 위해 HTTP 1.0 출시 6개월 만인 1997년 1월에 릴리즈됨
- 현재 가장 많이 쓰이는 프로토콜 버전
특징
- 지속 연결(Persistent connection): 지정한 timeout 동안 연속적인 요청 사이에 커넥션을 닫지 않음. 기존 연결에 대해서 handshake 생략 가능
- 파이프라이닝(pipelining): 이전 요청에 대한 응답이 완전히 전송되기 전에 다음 전송을 가능하게 하여, 여러 요청을 연속적으로 보내 그 순서에 맞춰 응답을 받는 방식으로 지연 시간을 줄이는 방식
- HOST 헤더 추가: 하나의 IP나 서버에서 여러 도메인을 쓸 때 Host 헤더를 통해 어느 도메인으로 들어왔는지 알 수 있음
- Chunk Encoding 전송: 응답 조각
- 바이트 범위 요청
- 캐시 제어 메커니즘 도입
지속 연결(Persistent Connection; keep-alive)
- 지정한 timeout동안 한 번 맺어졌던 연결을 끊지 않고 지속적으로 유지하여 불필요한 Handshake를 줄여 성능을 개선
- 클라이언트 측에서 요청에 keep-alive 헤더 필요 (1.1에는 기본적으로 세팅됨. 1.0에서는 Connection: close → keep-alive 세팅 필요)
- 하나의 커넥션을 재사용하기 위해 특정 요청의 종료를 판단하기 위해 정확한 Content-length 헤더를 사용해야 함
- 서버가 Connection 헤더를 지원해야 함
- Keep-alive 응답 헤더
- max: keep-alive을 통해서 주고받을 수 있는 request의 최대 갯수
- timeout : keep-alive가 얼마동안 유지될 것인가를 의미함. 이 시간이 지날 동안 request가 없을 경우에 connection은 close된다
- 클라이언트나 서버 중 한쪽이 Connection: close 헤더를 부여하면 연결을 끊음
Pipelining
- 여러개의 요청을 보낼때 처음 요청이 응답될 때까지 기다리지 않고 바로 요청을 한꺼번에 보내는 것
- keep-alive를 전제함
- 요청이 들어온 순서대로(FIFO) 응답 반환
- 응답 순서를 지키려다 응답 처리가 미뤄지는 Head Of Line Blocking 문제로 인해 모던 브라우저에서는 대부분 사장되고 HTTP/2에서 멀티플렉싱 알고리즘으로 대체됨
Domain Sharding
- 파이프라이닝을 대체하기 위해 하나의 도메인에 대해 여러 개의 TCP Connection을 생성해서 병렬로 요청을 보내고 받는 방식
- DNS Lookup시간, 도메인당 연결 개수 제한, 리소스 낭비 등의 이유로 근본적인 해결책은 아니었음

문제점
- HOL Blocking (Head of Line Blocking)
- 파이프라이닝의 FIFO(보낸 요청 순서대로 응답) 규칙으로 인해, 첫 번째 데이터 응답 속도가 늦어지면 후순위의 데이터 응답 속도도 느려지는 문제
- RTT (Round Trip Time)
- 요청(SYN)을 보낼 때부터 요청에 대한 응답(SYN+ACK)을 받을 때까지의 왕복 시간
- keep-alive를 하더라도 결국 TCP상에서 동작하는 HTTP 특성상 Handshake로 인한 레이턴시가 지금처럼 컨텐츠가 많은 웹 환경에서는 부담스러워졌다
- 무거운 헤더 구조의 중복
- HTTP/1.1의 헤더에 저장되어 있는 많은 메타정보, Cookie 정보가 매 요청시마다 포함되어 전송되어 메모리 자원이 낭비됨
HTTP/2
배경
- 기존 HTTP 1.1 버전의 문제점 개선과 성능 향상에 초점을 맞추어 2015년에 등장
- 2009년 구글이 압축, 다중화, 우선순위 설정을 통한 레이턴시 감소를 목적으로 SPDY라는 비표준 프로토콜을 개발했으나 HTTP/2에 대부분의 장점이 포함되어 있어 2016년 지원 중단을 결정
특징
- HTTP/1.1의 HOLB 문제 해결을 위해 여러 요청을 한번에 병렬로 전송
- Binary Framing Layer, Stream, Multiplexing으로 구현
- 클라이언트 요청에 대해 가까운 미래에 요청될 것 같은 리소스를 미리 보냄 (Server Push)
- HTTP/1.1과 달리 HTTP 메시지 헤더를 압축하여 전송
- 이전 message의 헤더 내용 중 중복되는 필드를 재전송하지 않아 메모리와 데이터를 절약
- 웹 응답 속도를 HTTP/1.1 대비 15~50% 향상
멀티플렉싱(Multiplexing)
- HTTP 메세지를 Framing Layer에서 Binary 형태의 프레임으로 나누고, 하나의 커넥션으로 동시에 여러 개의 메세지 스트림을 응답 순서에 상관없이 주고 받는 것
- 응답 프레임들이 요청 순서에 상관없이 먼저 완료된 순서대로 클라이언트에 전달됨
- 여러 요청과 응답이 뒤섞여 패킷 순서가 흐트러지는 것을 방지하기 위해 스트림 우선순위 설정 ⇒ Stream Prioritization
- HTTP/1.1의 요청/응답은 통짜 Text Message 단위로 구성
- 헤더와 바디를 개행문자(\r, \n)로 구분
- HTTP/2부터는 둘을 layer로 구분하여 데이터 파싱 및 전송 속도가 증가하고 오류 발생 가능성을 감소시킴(Binary Framing Layer)
- HTTP/2에서는 Frame, Stream 단위를 추가
- Frame: Header 또는 Data가 들어 있는 통신의 최소단위
- Message: 요청/응답의 단위로서 다수의 Frame으로 구성됨
- Stream: 연결된 Connection 내에서 양방향으로 message를 주고받는 하나의 흐름. 하나의 커넥션에서 여러 개의 스트림이 병렬적으로 처리되기 때문에 속도가 빠름
- 효과: latency 감소, 효율적인 네트워크 사용으로 비용 감소

Server Push
- 클라이언트 요청에 대해 가까운 미래에 요청될 것 같은 리소스를 미리 보내는 것
- HTML 문서 요청이 들어왔을 때 해당 문서에 링크된 CSS, JS, 이미지 파일 등의 리소스를 파악하여 미리 브라우저의 캐시에 Push
- 브라우저에서 HTML 파싱 과정에서 필요한 리소스를 요청하여 발생하는 트래픽과 회전지연을 줄여줌 ⇒ Total Load Time 감소
스트림 우선순위 통신(Stream Prioritization)
- 멀티플렉싱으로 요청과 응답이 동시에 이루어질 때 패킷 순서가 흐트러지지 않도록 하기 위해 고안된 기법
- 각 스트림들의 우선순위를 지정하고 식별자를 설정한 우선순위 지정 트리 활용
- 클라이언트는 서버에게 스트림을 보낼때, 각 요청 자원에 가중치 우선순위(1~256)를 지정하고 전송
- 서버는 우선순위가 높은 응답이 클라이언트에 우선적으로 전달될 수 있도록 대역폭을 설정
- 응답 받은 각 프레임에는 이것이 어떤 스트림인지에 대한 고유한 식별자가 있어, 클라이언트는 여러개의 스트림을 interleaving을 통해 서로 끼워놓는 식으로 조립
Frame: [stream_id=2, type=DATA, payload...] Frame: [stream_id=1, type=DATA, payload...] Frame: [stream_id=2, type=DATA, payload...]- 우선순위의 기준은 리소스의 종류, 로드되는 위치, 이전 페이지 방문에서 학습한 결과 등에 따라 브라우저가 결정
HTTP 헤더 데이터 압축, 중복 필드 재전송 방지
- 메시지 헤더에 존재하는 중복값은 index값만 전송하고, 중복되지 않은 값은 허프만 인코딩 기법을 사용하는 HPACK 압축 방식으로 인코딩하여 전송
- 클라이언트-서버 양 말단에 헤더 정보를 관리하는 메모리 테이블이 존재하고 프레임을 주고받으면서 동기화된 상태를 유지함
- Static Header Table:
:method,:path,content-type,accept-encoding등 변경되지 않는 목록을 관리하는 테이블 - Dynamic Header Table: 커넥션 단위로 런타임에 생성되어, 이전 요청/응답에서 사용한 헤더들을 저장하고 중복된 헤더를 재사용할 수 있게 하는 테이블
- 서버가
indexed header field: index = 10같이 보낸다면 클라이언트도 자신의 동적 테이블 인덱스 10에서 헤더 정보를 찾아냄
- Static Header Table:
문제점
- 여전히 HTTP/2는 TCP를 사용하기 때문에 Handshake로 인한 레이턴시가 발생함
- TCP 자체의 HOLB
- 멀티플렉싱을 통해 파이프라이닝 시 발생하는 애플리케이션 계층(L4)에서의 HOLB 문제는 해결됨
- 그러나 기본적으로 TCP는 패킷이 유실되거나 오류가 있을때 재전송하기 때문에, 이 재전송 과정에서 패킷의 지연이 발생하면 결국 전송 계층(L3)에서의 HOLB 문제가 발생함

- 헤더 필드의 이름과 값을 바이너리로 인코딩하기 때문에, 헤더 필드로 어떤 문자열이든 사용할 수 있음 ⇒ 중간의 Proxy 서버가 HTTP 1.1 메시지로 변환할 때 메시지를 불법 위조 가능
- 성능을 위해 커넥션을 오래 유지함에 따라 개인정보 유출 우려
HTTP/3
배경
- HTTP/2는 단일 TCP 내 여러 개의 스트림에서 병렬적으로 요청-응답을 처리하고 스트림 식별자를 통해 각 스트림의 우선순위를 관리함
- HTTP/1.1 파이프라이닝의 HOLB 문제를 해결하고, 병렬 TCP 연결로 인한 리소스 낭비를 해결한 획기적인 변화였음
- 그러나, 단일 TCP 내에 스트림이 여러개라도 TCP 자체의 패킷 순서 보장 특징에 의해 특정 스트림의 패킷 손실/재전송이 발생하면 이후 다른 스트림 패킷들도 모두 지연됨 (TCP HOLB)
- 결국 TCP로 인터넷 통신을 하는 것이 여전히 성능 개선의 발목을 잡는 것
특징
- TCP를 버리고 UDP 기반의 QUIC(Quick UDP Internet Connections) 프로토콜을 고안
- QUIC 프로토콜은 구글에서 개발했으며, QUIC 프로토콜을 TCP/IP 4계층에서 동작시키기 위해 설계된 것이 HTTP/3
QUIC
- QUIC은 TCP + TSL + HTTP의 기능을 모두 구현한 프로토콜
- TCP의 프로토콜의 무결성 보장 알고리즘과 SSL이 이식됨으로써 높은 성능과 동시에 신뢰성을 충족시킴

- TCP의 프로토콜의 무결성 보장 알고리즘과 SSL이 이식됨으로써 높은 성능과 동시에 신뢰성을 충족시킴
- TCP Handshake 과정이 필요 없어 RTT 감소
- 네트워크가 변경되면 재연결이 필요한 TCP와 달리, QUIC은 초기 연결 시 Connection ID(클라이언트 IP와 무관)를 사용하여 네트워크가 변경되어도 재연결 필요 X

- 네트워크가 변경되면 재연결이 필요한 TCP와 달리, QUIC은 초기 연결 시 Connection ID(클라이언트 IP와 무관)를 사용하여 네트워크가 변경되어도 재연결 필요 X
- 독립 스트림 방식으로 기존 HTTP/2 멀티플렉싱에서의 TCP HOLB 문제 해결
- 패킷 손실/재전송이 발생한 스트림만 이후 패킷을 지연 ← UDP 기반이기 때문
- 특정 스트림에서 HOLB가 발생해도 다른 스트림에 영향을 미치지 않음

- 보안 강화
- 기본적으로 QUIC내에 TLS를 포함하기 때문에 TCP와 달리 헤더 영역도 같이 암호화됨
문제점
- 기존 체계와의 호환성 문제
- 기업이 HTTP/1.1 이나 HTTP/2 기반의 프론트엔드단 최적화를 이미 적용한 경우
- 헤더 필드까지 암호화하여 ISP/네트워크 중개사에서는 헤더 필드를 읽을 수 없음
- 네트워크 최적화 어려움
- 패킷이 ACK인지 재전송인지 알 수 없음 (RTT 추정 어려움)
- 패킷별로 암호화를 하기 때문에 기존의 TLS-TCP에서 패킷을 묶어서 암호화하는 것보다 CPU 리소스 소모가 큼
- UDP의 보안적인 취약점(53포트가 아닌 UDP 트래픽을 통한 DOS 공격 등) 내포
결론
웹에서 데이터를 주고받기 위한 통신 프로토콜인 HTTP는 초기 버전(0.9)에서 단순히 HTML 문서를 가져오는 수단이었지만, 웹의 발전과 함께 단순 하이퍼텍스트 문서 외에도 다양한 종류의 컨텐츠를 주고받기 위해 상태 코드와 Content-Type 등을 추가한 HTTP/1.0 버전이 등장하였다.
HTTP/1.1 버전에서는 이전 버전에서 비연결성 특징 때문에 매 요청-응답 시 발생하는 TCP Handshaking 문제를 해결하기 위한 지속 연결(Keep-alive) 기능과, 여러개의 요청을 한꺼번에 보내고 순서대로 응답을 받는 Pipelining, 병렬 TCP 연결을 활용하는 Domain Sharding, 압축 헤더 등의 기능을 선보였다.
HTTP/2 버전에서는 HTTP/1.1 Pipelining의 HOL Blocking 문제를 해결하기 위해 HTTP 메세지를 Framing Layer에서 Binary 형태의 프레임으로 나누고, 하나의 커넥션으로 동시에 여러 개의 메세지 스트림을 응답 순서에 상관없이 주고 받는 멀티플렉싱(Multiplexing) 기능을 도입하여 획기적인 성능 향상을 이뤄냈다.
그러나 여전히 HTTP/2는 TCP 기반으로 동작하였기 때문에, TCP Handshaking으로 인한 RTT 레이턴시와 TCP HOL Blocking 문제를 가지고 있었다. 이에 HTTP/3에서는 TCP+TLS의 장점을 그대로 계승한 UDP 기반의 QUIC 프로토콜을 사용하여, TCP의 Handshaking, 재연결 과정을 단순화하고, 하나의 커넥션 내 여러 개의 스트림을 독립적으로 핸들링하여 특정 스트림에서 HOLB가 발생해도 다른 스트림에 영향을 미치지 않게 함으로써 HTTP/1.1과 HTTP/2의 근본적인 한계(TCP)를 극복하고자 하였다.
참고자료
HTTP 0.9 ~ HTTP 1.1까지 알아보는 통신 기술
HTTP 2.0 소개 & 통신 기술 알아보기
HTTP 3.0 소개 & 통신 기술 알아보기
가상 호스팅 그리고 Host 헤더(Virtual Hosting and Host Header)
