HTTP란? 세번째.

Journey to HTTP/2 == '출처'

번역기 사용 후 조금 수정했습니다. 오역이나 부족한 부분은 언제든 말씀해주세요.

저를 포함한 모든 백엔드 개발자 분들에게 도움이 됐으면 좋겠습니다.

HTTP는 모든 웹을 지원하고 더 나은 애플리케이션을 개발하는데 도움이 되기 때문에 모든 웹 개발자가 알아야할 프로토콜입니다.

HTTP란 무엇입니까?

HTTP는 클라이언트와 서버가 서로 통신하는 방법을 표준화한 TCP/IP 기반 애플리케이션 계층 통신 프로토콜입니다. 콘텐츠가 인터넷을 통해 요청되고 전송되는 방식을 정의합니다. 애플리케이션 계층 프로토콜이란 클라이언트와 서버가 통신하는 방식과 클라이언트와 서버 간의 요청 및 응답을 얻기 위해 TCP/IP에 의존하는 방식을 표준화한 추상적인 계층일 뿐입니다. 기본적으로 TCP 포트는 '80'을 사용하지만 다른 포트도 사용할 수 있습니다. 그러나 HTTPS는 '443' 포트를 사용합니다.

HTTP/0.9 (1991)

최초의 문서화된 HTTP 버전은 HTTP/0.9이고 1991년에 발표되었습니다. 이것은 가장 간단한 프로토콜이었습니다. 'GET'이라는 하나의 메서드밖에 없었습니다.

클라이언트가 서버의 일부 웹 페이지에 액세스해야 했다면 아래와 같이 간단한 요청을 했을 것입니다.

GET /index.html

그리고 서버의 응답은 다음과 같았을 것입니다.

(response body)
(connection closed)

즉, 서버는 요청을 받고 HTML을 응답하고 콘텐츠가 전송되는 즉시 연결이 끊깁니다.

초창기 HTTP는

  • 헤더 없음
  • 'GET'이 유일한 메소드
  • 응답은 무조건 HTML

보시다시피, HTTP는 시작에 불과했습니다.

HTTP/1.0 - 1996

1996년에 원래 버전보다 크게 개선된 'HTTP/1.0'이 나왔습니다.

HTTP/0.9은 단순히 HTML 응답용으로만 설계된 것과는 달리 HTTP/1.0은 이미지, 비디오 파일, 일반 텍스트 또는 기타 콘텐츠와 같은 다른 응답 형식도 처리할 수 있습니다.

개선된 점은

  • 더 많은 메소드(예: POST, HEAD) 추가.
  • 요청/응답 형식 변경.
  • HTTP 헤더가 요청/응답에 모두 추가.
  • 응답을 식별하기 위해 상태 코드 추가.
  • 'character set' 지원.
  • 'multi-part'(서버에 업로드할 때 사용됨.) 지원
  • 권한 부여.
  • 캐싱.
  • 콘텐츠 인코딩.

다음은 HTTP/1.0 요청 및 응답의 모습입니다.

GET / HTTP/1.0
Host: kamranahmed.info
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*

보시다시피, 요청과 함께 클라이언트는 브라우저 정보, 필요한 응답 타입, 기타 등등 헤드에 넣어서 보낼 수 있습니다. HTTP/0.9는 헤더가 없었기 때문에 이러한 정보를 보낼 수 없었습니다.

위의 요청에 대한 응답은 다음과 같습니다.

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의 버전, 상태 코드와 상태 코드 메세지가 있습니다.

이 버전에서 요청 및 응답 헤더는 여전히 'ASCII'로 인코딩된 상태로 유지되었지만 응답 본문은 이미지, 비디오, HTML, 일반 텍스트 또는 기타 콘텐츠 타입과 같은 모든 타입이 될 수 있습니다. 이제 서버는 모든 콘텐츠 타입을 클라이언트에 보낼 수 있습니다. 도입된 지 얼마 안됐지만 HTTP에 "하이퍼 텍스트" 라는 용어의 의미는 사라졌습니다. 'HMTP(Hypermedia transfer protocol)'가 더 합리적이지만 우리는 아직 HTTP라고 부르고 있습니다.

HTTP/1.0의 주요 단점 중 하나는 연결당 여러 요청을 가질 수 없다는 것입니다. 즉, 클라이언트가 서버로부터 무언가를 요청할 때마다 새 TCP 연결을 해야하고 하나의 요청이 완료된 후 연결이 끊깁니다. 그리고 또 다른 요청에 대해 새로 연결해야합니다. 이게 왜 나쁠까요? 10개의 이미지, 5개의 스타일시트, 5개의 자바스크립트 파일로 총 합 20개의 파일이 있는 웹페이지를 방문했다고 가정해 보겠습니다. 서버는 요청이 완료되자마자 연결을 닫기 때문에 20개의 요청을 별도로 연결해야 합니다. 많은 수의 TCP 연결은 'three-way handshake'로 인해 상당한 성능 저하가 발생합니다.

three-way handshake

모든 TCP 연결은 'three-way handshake'로 시작됩니다. 여기서 클라이언트와 서버는 애플리케이션 데이터 공유를 시작하기 전에 일련의 패킷을 공유합니다.

  1. SYN - 클라이언트가 임의의 숫자를 선택하여(여기서는 'x'라고 가정합니다.) 서버로 보냅니다.
  2. SYN ACK - 서버는 임의의 숫자를 고르고(여기서는 'y') 클라이언트가 보낸 숫자를 증가시키고('x' -> 'x+1') ACK 패킷을 클라이언트에 다시 전송하여 요청을 승인합니다.
  3. ACK - 클라이언트는 서버로부터 받은 숫자('y' -> 'y+1')를 증가시키고 ACK 패킷을 돌려 보냅니다.

'three-way handshake'가 완료되면 클라이언트와 서버 간의 데이터 공유가 시작될 수 있습니다. 클라이언트는 마지막 ACK 패킷을 발송하는 즉시 애플리케이션 데이터를 전송할 수 있지만 서버는 ACK 패킷이 수신될 때까지 기다려야 합니다.

(*) 그림에 사소한 문제가 있습니다. 클라이언트가 마지막에 보내는 ACK 패킷은 'x+1, y+1'이 아닌 'y+1'입니다.

HTTP/1.0에 새로운 헤더 'Connection: keep-alive'를 도입하여 이 문제(요청을 응답하면 클라이언트와 서버가 연결을 끊는 것)를 해결할려고 했습니다. 그러나 널리 지원되지 않았고 문제가 여전히 지속되었습니다.

연결을 끊는 것 외에도 HTTP는 'stateless(상태를 저장하지 않는)' 프로토콜입니다. 즉, 서버는 클라이언트에 대한 정보를 저장하지 않기 때문에 각 요청마다 클라이언트는 자신이 누구인지 서버에게 알려줘야합니다. 이러한 문제는 클라이언트가 중복 데이터를 보내야하기 때문에 대역폭이 증가합니다.

HTTP/1.1- 1999년

HTTP/1.0이 나온지 단 3년 만에 다음 버전 HTTP/1.1이 1999년에 출시되었습니다. 전작에 비해 많은 개선이 이루어졌습니다.

HTTP/1.0에 포함된 주요 개선 사항은

  • 새로운 HTTP 메서드: PUT, PATCH, OPTIONS, DELETE가 추가되었습니다.

  • 호스트 네임 식별(Hostname Identification): HTTP/1.0 Host 헤더는 필요하지 않았지만 HTTP/1.1 Host 헤더는 필요합니다.

  • 지속적인 연결(Persistent Connections): 위에서 설명했던 것처럼 HTTP/1.0은 연결당 요청은 하나뿐이였고 요청이 응답되자마자 연결이 종료되어 성능 저하 및 지연 시간이 증가합니다. HTTP/1.1은 영구적인 연결(Persistent Connections)(즉, 연결이 기본적으로 닫히지 않고 열린 상태로 유지됨.)을 도입하여 여러 요청을 허용했습니다. 연결을 닫으려면, HTTP 헤더에 'Connection: close'를 넣어서 요청하면 됩니다. 클라이언트는 일반적으로 안전하게 연결을 닫기 위해 마지막 요청에 이 헤더를 넣어 전송합니다.

  • 파이프라이닝(Pipelining): 파이프라이닝을 통해 클라이언트가 동일한 연결에서 서버로부터 응답을 기다리지 않고 서버에 여러 요청을 보낼 수 있고, 서버는 요청을 수신한 순서와 동일한 순서로 응답을 보냅니다. 하지만 어떻게 클라이언트가 응답의 순서를 알 수 있을까요? 클라이언트는 응답의 끝을 식별하기 위해 Content-Length 헤더를 사용하며 다음 응답을 대기합니다.

지속적인 연결이나 파이프라이닝을 사용하기 위해 Content-Length 헤더를 사용해서 클라이언트가 응답 순서를 알아야합니다.

하지만 이러한 방법을 사용해도 여전히 문제가 있습니다. 데이터가 동적이고 서버가 'Content-Length'를 찾을 수 없는 경우에는 어떻게 할까요? 이런 상황에선 '지속적인 연결'로 효과를 못보지 않을까요? 이 문제를 해결하기 위해 HTTP/1.1는 청크 인코딩을 도입합니다. 이러한 경우 서버는 청크 인코딩을 위해 콘텐츠 길이를 생략할 수 있습니다. 단, 둘 중 어느 것도 사용할 수 없는 경우에는 요청이 끝나면 연결을 종료해야 합니다.

  • 청크 전송(Chunked Transfers): 동적 컨텐츠인 경우 통신을 시작할 때 서버가 Content-Length를 찾지 못하면 청크로 나눠진 콘텐츠를 보내고 Content-Length를 청크에 추가합니다. 그리고 모든 청크를 전송하고 전송이 완료되었다고 클라이언트에게 알려주기 위해 Content-Length를 0으로 설정해서 빈 청크를 전송합니다. 클라이언트에게 청크 전송을 한다고 알려주기 위해 서버는 'Transfer-Encoding: chunked' 헤더를 포함합니다.

  • 기본적인 인증만 받은 HTTP/1.0와는 달리, HTTP/1.1은 다이제스트(digest) 및 프록시 인증을 포함합니다.

  • Caching

  • Byte Ranges

  • Character sets

  • Language 협상

  • Client cookies

  • 향상된 압축 지원

  • 새로운 상태 코드

  • 기타 등등

HTTP/1.1은 1999년에 도입되었고 오랫동안 표준이었습니다. 비록 이전 버전보다 많이 발전했지만, 웹이 계속 발전하면서 요구사항이 더 많아졌습니다. 요즘 웹 페이지를 로드하는 것은 그 어느 때보다도 자원 집약적입니다. 요즘 간단한 웹페이지는 30개 이상의 연결을 열어야 합니다. 글쎄요...HTTP/1.1의 지속적인 연결이 있는데, 왜 이렇게 많은 연결할까요? 그 이유는 HTTP/1.1 안에 있습니다. HTTP/1.1은 오직 하나의 처리되지 않은 연결만 가질 수 있기 때문입니다. 파이프라이닝을 도입하여 이 문제를 해결하려고 했지만, 느리거나 무거운 요청이 뒤에 있는 요청을 차단할 수 있는 'head-of-line blocking' 때문에 문제를 완전히 해결하지 못했으며 일단 요청이 파이프라인에 갇히면 다음 요청이 끝날 때까지 기다려야 합니다. HTTP/1.1의 이러한 단점을 해결하기 위해 개발자들은 스프라이트시트 사용, CSS에 인코딩된 이미지, 단일 CSS/Javascript 파일, 도메인 샤딩과 같은 해결 방법을 연구하기 시작했습니다.

SPDY - 2009

구글은 웹페이지의 지연 시간을 줄이면서 웹을 더 빠르게 만들고 웹 보안을 향상시키기 위해 대체 프로토콜을 만들기 시작했습니다. 2009년에 그들은 SPDY를 발표했습니다.

SPDY는 구글의 상표이며 약어가 아닙니다.

대역폭을 계속 늘리면 초기에는 네트워크 성능이 올라가지만 한계가 있습니다. 대기 시간을 줄이면 지속적인 성능 향상을 기대할 수 있습니다. 이것이 성능 향상을 위한 SPDY의 핵심 입니다. SPDY는 네트워크 성능을 높이기 위해 대기 시간을 줄입니다.

SPDY의 특징은 멀티플렉싱, 압축, 우선 순위 지정, 보안 등을 포함합니다. 뒤에 말할 HTTP/2는 대부분 SPDY에서 영감을 받았습니다.

SPDY는 HTTP를 대체하는게 아니라, 애플리케이션 계층에 존재하는 HTTP를 통한 변환 계층으로 요청을 수정해서 전송합니다. 이것은 사실상 표준이 되기 시작했고 대다수의 브라우저들이 구현하기 시작했습니다.

2015년 구글은 두 가지 표준이 경쟁하는 걸 원하지 않았고 HTTP/2.0이 탄생하면서 SPDY를 HTTP에 병합하기로 결정했습니다.

HTTP/2- 2015

이제 우리는 왜 또 다른 버전의 HTTP가 필요한지 알 수 있습니다. HTTP/2는 컨텐츠의 짧은 지연 시간 전송을 위해 설계되었습니다.

HTTP/1.1과의 차이점은 다음과 같습니다.

- 텍스트 대신 바이너리
- 멀티플렉싱 - 단일 연결을 통한 여러 비동기 HTTP 요청
- HPACK을 사용한 헤더 압축
- 서버 푸시 - 단일 요청에 대한 여러 응답
- 요청 우선 순위 지정
- 보안

1. 바이너리 프로토콜

HTTP/2는 HTTP/1.x에 존재하는 지연 시간 증가 문제를 바이너리 프로토콜로 만들어 해결했습니다. 바이너리 프로토콜은 분석이 쉬워졌지만 HTTP/1.x와 달리 사람이 읽을 수 없습니다. HTTP/2의 주요 구성 요소는 프레임과 스트림입니다.

프레임 및 스트림

HTTP 메시지는 현재 하나 이상의 프레임으로 구성되어 있습니다. 메타 데이터가 들어있는 헤더 프레임과 페이로드와 몇가지 다른 타입의 프레임(HEADERS, DATA, RST_STREAM, SETTINGS, PRIORITY)들이 있는 데이터 프레임이 있습니다.

매번 HTTP/2는 요청 및 응답에 고유한 스트림 ID가 부여되며 프레임으로 나눕니다. 프레임은 바이너리 데이터 조각에 불과합니다. 프레임 컬렉션은 스트림이라고 부릅니다. 각 프레임에는 자신이 속한 스트림을 식별하는 스트림 ID가 있으며 공통 헤더가 있습니다. 또한, 스트림 ID가 고유하다는 것 외에도 클라이언트의 요청은 홀수를 사용하고 서버의 응답은 짝수 스트림 ID를 가지고 있습니다.

언급할 필요가 있다고 생각하는 또 다른 프레임 타입은 RST_STREAM 입니다. RST_STREAM는 스트림을 중단하는데 사용하는 특수한 프레임입니다. 즉, 클라이언트가 더 이상 스트림이 필요하지 않다는 것을 서버에 알리기 위해 이 프레임을 사용합니다. HTTP/1.1에서 서버가 클라이언트에게 응답이 끝났다는걸 알리는 유일한 방법은 연결을 끊는 것이였습니다. 이로인해 요청에 대해 새로운 연결을 해야했기 때문에 지연 시간이 증가하는 것이였습니다. HTTP/2에서 클라이언트는 RST_STREAM를 사용함으로 특정 스트림 수신을 중단할 수 있지만 서버와 여전히 연결되어있고 다른 스트림을 수신 받을 수 있습니다.

2. 멀티플렉싱

HTTP/2는 바이너리 프로토콜이며, 위에서 말했듯 요청과 응답에 프레임과 스트림을 사용하고 일단 TCP 연결을 하면 모든 스트림은 추가 연결을 하지 않고 하나의 연결을 통해 비동기적으로 전송됩니다. 그리고 서버는 비동기로 응답합니다. 즉, 응답은 순서가 없고 클라이언트는 할당된 스트림 ID를 사용하여 특정 패킷이 속하는 스트림을 구분합니다. 이것은 HTTP/1.x에 존재했던 연결 차단 문제를 해결합니다. 즉, 클라이언트는 시간이 걸리는 요청을 기다릴 필요가 없고 다른 요청도 함께 응답받을 수 있습니다.

3. HPACK 헤더 압축

우리가 동일한 클라이언트에서 서버에 지속적으로 요청할 때 헤더에 계속해서 보내는 중복 데이터가 많고, 때로는 대역폭과 지연 시간을 증가시키는 헤더가 있을 수 있는 문제가 있습니다. 이것을 극복하기 위해서 HTTP/2는 헤더 압축을 도입했습니다.

요청 및 응답과 달리 헤더는 gzip 또는 compress나 기타 등등 포맷으로 압축되지 않습니다. 헤더 압축는 다른 메커니즘이 있는데 리터럴 값을 Huffman 코드를 사용하여 인코딩하고 헤더 테이블은 클라이언트와 서버에 의해 유지되며 이후 요청에 헤더를 생략하고 헤더 테이블을 참조합니다.

헤더는 HTTP/1.1과 동일하지만 :method, :scheme, :host 와:path가 추가되었습니다.

4. 서버 푸시

서버 푸시는 HTTP/2의 또 다른 엄청난 기능입니다. 클라이언트가 특정 자원을 요구할거라고 알고 있는 서버가 클라이언트조차 요구하지 않은 리소스를 클라이언트에 푸시합니다. 예를 들어, 브라우저가 웹 페이지를 로드한다고 가정해봅시다. 페이지 전체를 분석하여 로드해야 하는 콘텐츠를 찾아낸 다음, 그 콘텐츠를 얻기 위해 서버에 요청을 보냅니다.

서버 푸시는 서버가 클라이언트가 요구할 것을 알고 있는 데이터를 밀어줌으로써 라운드 트립을 줄입니다. 어떻게 하냐면 서버가 PUSH_PROMISE라는 특수 프레임을 보내고 클라이언트에게 이렇게 말합니다.

"이봐, 나는 지금 이 자원을 너에게 보내려고 해! 나한테 더이상 부탁하지 마!"

PUSH_PROMISE 프레임은 푸시가 발생한 스트림과 관련되어 있으며 약속된 스트림 ID가 포함되어 있습니다. 여기서 스트림은 서버가 푸시할 리소스를 보내는 스트림입니다.

5. 우선순위 지정 요청

클라이언트는 우선 순위 정보를 헤더 프레임에 포함시켜 스트림에 우선 순위를 할당할 수 있습니다. 클라이언트는 언제든지 PRIORITY 프레임을 사용해서 우선 순위를 바꿀 수 있습니다.

우선 순위가 없으면 서버는 비동기적으로 요청을 처리합니다. 스트림에 우선 순위가 지정되어 있는 경우, 서버는 이 우선 순위에 기반하여 요청에 필요한 리소스 양을 결정합니다.

6. 보안

HTTP/2에 보안(TLS)을 의무로 할것인지 광범위한 토론이 있었습니다. 결국 의무화하지 않기로 했습니다. 다만 대부분 TLS를 사용하는 HTTP/2만 지원한다고 말했습니다. 비록 HTTP/2는 암호화가 필요하지는 않지만, 어쨌든 그것은 기본적으로 의무화 되었습니다. TLS가 사용된 HTTP/2에는 몇 가지 요구사항이 있습니다. TLS 버전은 1.2 또는 그 이상을 사용해야하며 특정 수준의 최소 키 크기와 유효기간이 짧은 키 사용 등이 있습니다.

끝으로...

HTTP/2는 이미 SPDY를 능가했습니다. HTTP/2는 성능 향상 측면에서 제공할 수 있는 것이 많으며 이제 우리가 그것을 사용해야 합니다.

좋은 웹페이지 즐겨찾기