모든 웹 개발자가 알아야 할 프로토콜이 바로 HTTP다. HTTP는 전체 웹을 움직이는 핵심 엔진이다. HTTP를 제대로 알면 더 나은 애플리케이션을 개발하는 데 큰 도움이 된다.
이번 글에서는 HTTP가 무엇이고, 어떻게 발전해왔으며 현재 어떤 위치에 있는지 알아보자.
HTTP란 무엇일까?
일단, HTTP란 무엇인가?
HTTP는 TCP/IP 기반의 애플리케이션 계층 통신 프로토콜로, 클라이언트와 서버가 통신하는 방식을 표준화한다. 인터넷에서 콘텐츠가 어떻게 요청되고 전송되는지를 정의하는 규칙이라고 생각하면 된다.
- *‘애플리케이션 계층 프로토콜’**은 클라이언트와 서버 같은 호스트들이 소통하는 방식을 추상화한 것이다. HTTP 자체는 클라이언트와 서버 사이에서 요청과 응답을 전달하기 위해 TCP/IP에 의존한다. 기본적으로 TCP 포트 80을 사용하지만, 다른 포트도 쓸 수 있다. HTTPS는 **포트 443**을 사용한다.
HTTP/0.9 - 한 줄의 단순함 (1991)
최초의 HTTP 버전인 HTTP/0.9는 1991년에 제안되었다. GET이라는 단 하나의 메서드만 가진 가장 단순한 프로토콜이었다.
클라이언트가 서버의 웹페이지에 접속하려면 아래처럼 간단한 요청을 보냈다.
HTTP는 웹 개발자라면 알고 있어야하는 프로토콜이다. HTTP 안다는 것은 더 나은 애플리케이션을 개발할 수 있다는 것이다.
이 글에서는, HTTP가 무엇인지, 어떻게 시작된 것인지, 오늘날 어디서 영향을 끼치는지, 어떻게 가져오는지를 배울 수 있다.
GET /index.html그리고, 서버로부터 받은 응답은 아마 이런 식일 것이다.
(response body)
(connection closed)서버는 요청을 받고, HTML로 응답한다. 그리고, content를 전송하자마자 자 연결은 닫힌다. 따라서,
- 헤더가 없다. (No headers)
GET만 허용된 메서드다. (GETwas the only allowed method)- 응답은 HTML이어야한다. (Response had to be HTML)
보면 알 수 있듯이, 프로토콜은 앞으로 일어날 일을 위한 디딤돌에 불과하다.
HTTP/1.0 (1996)
1996년, 다음 버전인 HTTP/1.0이 등장하며 획기적인 개선이 이뤄졌다.
HTTP/0.9가 HTML 응답만 가능했던 것과 달리, HTTP/1.0은 이미지, 동영상, 텍스트 파일 등 다양한 콘텐츠 형식을 다룰 수 있게 됐다. POST, HEAD 같은 메서드가 추가되었고, 요청/응답 형식과 HTTP 헤더, 상태 코드도 생겼다. 문자 세트, 인증, 캐싱, 콘텐츠 인코딩 등 많은 기능도 포함됐다.
HTTP/1.0 요청 및 응답 예시는 아래와 같다.
response:
GET / HTTP/1.0
Host: cs.fyi
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*보다시피, 클라이언트는 요청과 함께 개인 정보나 원하는 응답 유형 등을 헤더에 담아 보낼 수 있었다. 이는 헤더가 없던 HTTP/0.9에서는 불가능한 일이었다.
request:
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
(response body)
(connection closed)응답의 맨 앞에는 HTTP/1.0이 있고, 그 뒤에 **상태 코드(200)**와 상태 코드 설명이 따라온다.
이 버전에서도 요청/응답 헤더는 ASCII로 인코딩되지만, 응답 본문은 모든 종류의 콘텐츠가 가능했다. 그래서 HTTP의 ‘Hyper Text’라는 이름이 시대에 맞지 않게 되었다. ‘Hypermedia Transfer Protocol(HMTP)‘이 더 어울렸겠지만, 우리는 평생 이 이름에 갇혀 지내야 할 것 같다.
HTTP/1.0의 가장 큰 단점은 하나의 연결에 하나의 요청만 보낼 수 있다는 점이다. 클라이언트가 서버에서 무언가를 가져오려면 새로운 TCP 연결을 열어야 하고, 요청이 완료되면 연결은 바로 끊긴다. 다음 요청을 하려면 또다시 새로운 연결을 만들어야 한다.
이것이 왜 문제일까?
한 웹페이지에 이미지 10개, 스타일시트 5개, 자바스크립트 파일 5개가 있다고 가정해 보자. 총 20개의 항목을 가져와야 하는데, 서버가 요청 하나가 끝날 때마다 연결을 끊으니 20개의 개별 연결을 만들어야 한다. 이런 수많은 연결은 **‘3-way handshake’**와 ‘slow-start’ 때문에 심각한 성능 저하를 초래한다.
3-way handshake
아주 간단하게 말해, 모든 TCP 연결은 클라이언트와 서버가 애플리케이션 데이터를 주고받기 전에 일련의 패킷을 주고받는 3단계 과정을 거친다.
- SYN: 클라이언트가 임의의 숫자(x)를 선택해 서버에 보낸다.
- SYN ACK: 서버가 요청을 받았다는 ACK 패킷을 클라이언트에 보낸다. 이 패킷에는 서버가 선택한 임의의 숫자(y)와 클라이언트가 보낸 숫자(x+1)가 포함된다.
- ACK: 클라이언트가 서버로부터 받은 숫자(y)에 1을 더해 ACK 패킷을 다시 보낸다.
이 핸드셰이크가 완료되면 클라이언트와 서버 간 데이터 공유가 시작된다.
하지만 일부 HTTP/1.0 구현에서는 Connection: keep-alive라는 헤더를 도입해 이 문제를 해결하려 했다. 이 헤더는 서버에게 ‘연결을 끊지 마, 다시 쓸 거야’라고 알려주는 역할을 했다. 하지만 널리 지원되지 않아 문제는 여전했다.
또한 HTTP는 ‘비연결성(Connectionless)’ 프로토콜 외에 ‘무상태성(Stateless)’ 프로토콜이기도 하다. 서버는 클라이언트에 대한 정보를 유지하지 않으므로, 모든 요청은 이전 요청과 관련 없이 독자적으로 필요한 정보를 모두 담고 있어야 한다. 이 때문에 많은 수의 연결을 열어야 할 뿐만 아니라 중복된 데이터를 계속 보내야 해서 대역폭 낭비가 심했다.
HTTP/1.1 (1997)
HTTP/1.0이 나온 지 3년 만인 1999년에 HTTP/1.1이 출시되며 이전 버전보다 훨씬 많은 개선이 이뤄졌다. 주요 개선 사항은 다음과 같다.
- 새로운 HTTP 메서드 추가: PUT, PATCH, OPTIONS, DELETE 등이 도입됐다.
- 호스트명 식별: HTTP/1.0에서는 필수가 아니었던
Host헤더가 HTTP/1.1에서는 필수가 됐다. - 지속적 연결(Persistent Connections): HTTP/1.0의 가장 큰 문제점이었던 성능 저하를 해결하기 위해, 연결이 기본적으로 닫히지 않고 계속 열려 있도록 했다. 이로 인해 하나의 연결로 여러 개의 요청을 순차적으로 보낼 수 있게 됐다. 연결을 닫으려면 요청에
Connection: close헤더를 포함해야 한다. - 파이프라이닝(Pipelining): 클라이언트가 서버의 응답을 기다리지 않고도 같은 연결에 여러 요청을 연달아 보낼 수 있게 됐다. 서버는 요청을 받은 순서대로 응답을 보냈다. 이를 위해선
Content-Length헤더가 있어야 클라이언트가 응답의 끝을 파악할 수 있다.
하지만 데이터가 동적이어서 서버가 미리 콘텐츠 길이를 알 수 없는 경우에는 문제가 있었다. 이를 해결하기 위해 HTTP/1.1은 청크 인코딩(Chunked Encoding)을 도입했다.
청크 전송 (Chunked Transfers)
서버가 콘텐츠 길이를 미리 알 수 없을 때, 데이터를 여러 ‘청크(chunk)‘로 나눠서 보낼 수 있다. 각 청크를 보낼 때마다 그 청크의 길이를 함께 포함시킨다. 모든 청크 전송이 끝나면 Content-Length가 0인 빈 청크를 보내서 전송이 완료되었음을 클라이언트에게 알린다. 서버는 Transfer-Encoding: chunked 헤더를 포함해 클라이언트에게 청크 전송임을 알린다.
이 외에도 HTTP/1.1은…
- 캐싱
- 바이트 범위
- 쿠키
- 압축 지원
등 많은 기능을 추가했다.
HTTP/1.1은 1999년에 도입되어 오랫동안 표준으로 사용됐다. 하지만 웹이 계속 변화하면서 그 한계가 드러났다. 요즘 웹페이지는 30개가 넘는 연결을 열어야 할 정도로 리소스 집약적이다. HTTP/1.1에는 지속적 연결이 있지만, 한 번에 하나의 요청만 처리할 수 있다는 한계가 있었다.
- **‘파이프라이닝’**으로 이를 해결하려 했지만, ‘헤드 오브 라인 블로킹(Head-of-Line Blocking)’ 문제 때문에 완벽하게 해결하지 못했다. 느리거나 무거운 요청 하나가 파이프라인에서 막히면 뒤에 있는 모든 요청이 멈추는 문제다. 이 때문에 개발자들은 CSS 스프라이트, 인코딩 이미지, 거대한 CSS/자바스크립트 파일 병합, 도메인 샤딩 등 다양한 편법을 써야 했다.
SPDY (2009)
구글은 웹을 더 빠르게 만들고, 보안을 강화하며, 지연 시간을 줄이기 위해 새로운 프로토콜을 실험하기 시작했다. 그리고 2009년 SPDY를 발표했다.
SPDY는 구글의 상표이며, 약어는 아니다.
연구 결과, 대역폭을 계속 늘려도 어느 시점부터는 성능 개선 효과가 미미한 반면, 지연 시간(Latency)을 줄이면 성능이 꾸준히 향상된다는 것을 발견했다. 지연 시간을 줄여 네트워크 성능을 높이는 것이 SPDY의 핵심 아이디어였다.
SPDY는 HTTP를 대체하려 하지 않고, HTTP 위에 존재하는 일종의 ‘번역 계층’ 역할을 했다. SPDY는 브라우저 대다수가 도입하면서 사실상의 표준이 되어갔다.
결국 2015년, 구글은 두 개의 경쟁 표준을 두지 않기로 결정하고, SPDY를 HTTP에 통합하여 HTTP/2를 탄생시키고 SPDY는 중단했다.
HTTP/2 (2015)
왜 새로운 HTTP 프로토콜이 필요했는지 이제 충분히 이해했을 것이다. HTTP/2는 콘텐츠 전송 시 지연 시간을 줄이기 위해 설계됐다. HTTP/1.1과의 주요 차이점은 다음과 같다.
- 텍스트 대신 바이너리
- 멀티플렉싱: 하나의 연결로 여러 개의 비동기 요청을 처리한다.
- 헤더 압축: HPACK을 사용한다.
- 서버 푸시: 하나의 요청에 여러 응답을 보낸다.
- 요청 우선순위
- 보안
1. 바이너리 프로토콜
HTTP/2는 바이너리 프로토콜이 되어 HTTP/1.x의 지연 시간 문제를 해결한다. 바이너리 방식은 파싱하기 쉽지만, 인간의 눈으로는 읽을 수 없다. HTTP/2의 주요 구성 요소는 프레임(Frames)과 스트림(Streams)이다.
프레임과 스트림
HTTP 메시지는 이제 하나 이상의 프레임으로 구성된다. 메타데이터용 HEADERS 프레임, 페이로드용 DATA 프레임 등 여러 프레임 유형이 있다.
모든 HTTP/2 요청과 응답에는 고유한 스트림 ID가 부여되고, 프레임으로 나뉜다. 스트림은 프레임들의 집합이다. 모든 프레임에는 소속 스트림을 식별하는 스트림 ID가 있으며 공통 헤더를 가진다. 클라이언트가 시작하는 요청은 홀수 스트림 ID를 사용하고, 서버의 응답은 짝수 스트림 ID를 사용한다는 점도 흥미롭다.
HEADERS와 DATA 외에 RST_STREAM 프레임도 주목할 만하다. 이 프레임은 특정 스트림을 중단할 때 사용된다. HTTP/1.1에서는 응답을 중단하려면 연결을 끊어야 해서 지연 시간이 늘어났지만, HTTP/2에서는 RST_STREAM을 사용해 특정 스트림만 중단하고 다른 스트림은 계속 유지할 수 있다.
2. 멀티플렉싱
HTTP/2는 바이너리 프로토콜이며 프레임과 스트림을 사용하므로, TCP 연결이 한 번 열리면 모든 스트림이 추가 연결 없이 비동기적으로 전송된다. 서버도 응답을 비동기적으로 처리하여 응답 순서가 중요하지 않다. 클라이언트는 할당된 스트림 ID로 특정 패킷이 어떤 스트림에 속하는지 식별한다. 이 기능은 HTTP/1.x의 ‘헤드 오브 라인 블로킹’ 문제도 해결한다.
3. 헤더 압축
HTTP/2는 헤더를 최적화하기 위해 헤더 압축을 도입했다. 같은 클라이언트가 서버에 계속 접속하면 중복된 헤더 데이터를 계속 보내게 되고, 쿠키 같은 정보가 헤더 크기를 키워 대역폭 낭비와 지연 시간을 늘린다.
HTTP/2의 헤더 압축은 HPACK 방식을 사용한다. 텍스트를 허프만 코드로 인코딩하고, 클라이언트와 서버가 ‘헤더 테이블’을 유지하며 반복되는 헤더(예: User-Agent)는 생략하고 참조하는 방식이다.
4. 서버 푸시
HTTP/2의 또 다른 기능은 ‘서버 푸시’다. 서버가 클라이언트가 특정 리소스를 요청할 것을 미리 알고, 요청하기도 전에 리소스를 보내주는 기능이다. 예를 들어, 브라우저가 웹페이지를 로드할 때 원격 콘텐츠를 파싱하고 요청을 보내는데, 서버는 클라이언트가 필요로 할 것을 예측해 먼저 보내 지연 시간을 줄인다.
서버는 PUSH_PROMISE라는 특별한 프레임을 보내 “이 리소스를 곧 보낼 테니 요청하지 마”라고 클라이언트에게 알린다.
5. 요청 우선순위
클라이언트는 HEADERS 프레임에 우선순위 정보를 포함시켜 스트림에 우선순위를 할당할 수 있다. 서버는 이 우선순위 정보를 기반으로 어떤 요청에 리소스를 얼마나 할당할지 결정한다.
6. 보안
HTTP/2에 보안(TLS)을 의무화할지에 대해 논의가 많았다. 결국 의무화하지는 않았지만, 대부분의 공급업체가 HTTP/2를 TLS 위에서만 지원하겠다고 선언했다. 따라서 HTTP/2는 사실상 기본적으로 암호화가 필수가 됐다.