0. 서론

이번 포스트에는 HTTP 메시지의 구조와 인코딩에 대해 알아보자.

1. TL;DR

  • HTTP Protocol을 통해 교환(Request/Response)하는 정보를 HTTP 메시지라 한다.
  • HTTP 메시지는 Header와 Body로 구분된다.
  • Header Field에 따라 메시지에 대한 응답이 달라지며, 이를 이용해 Content Negotiation등을 수행할 수도 있다.
  • Body 안에는 상대에게 전송할 Entity가 포함된다. 일반적인 경우 Message Body와 Entity Body는 동일하다.
  • Body 안에 들어가는 Entity의 크기가 클 경우 효율적인 전송을 위해 압축이나 분할 등을 수행할 수 있는데, 이를 인코딩(Encoding)이라 한다. 인코딩이 적용되는 경우 Entity Body는 변형된다.

2. HTTP 메시지

HTTP Protocol을 이용해 상대에게 정보를 보낼 때, 이 정보를 HTTP Message라 한다.

메시지는 클라이언트 측에서 보내는 경우 리퀘스트 메시지(Request Message), 서버 측에서 보내는 경우 리스폰스 메시지(Response Message)라 한다.


모든 메시지는 헤더와 바디로 이루어지며, 이 둘을 개행 문자(CR+LF)로 구분하는데, 도식으로 나타내면 아래와 같다.

2.1. Header

도식에서 알 수 있듯이 HTTP 헤더는 총 4가지 종류가 존재한다.

  • 일반 헤더(General Header)
  • 요청 헤더(Request Header)
  • 응답 헤더(Response Header)
  • 엔티티 헤더(Entity Header)


헤더 필드에는 상대가 알아야 할 여러 가지 정보와 플래그를 포함하는데, 전송 시간이나 HTTP Method, 응답에 대한 Status Code, 쿠키 설정 필드 등 매우 다양하며, 각 헤더 종류마다 서로 다른 구조와 정보를 포함하지만 자세한 구조는 추후 포스트에서 살펴보기로 하자.


참고로 위 도식에서 헤더 필드들 밑에 기타 필드(etc.)가 포함되어 있는데, 이 곳에는 일반적인 HTTP 공식 사양(RFC)에 명시되지 않은 추가 정보들을 담는다. 대표적인 예가 바로 쿠키(Cookie)이다.

2.2. Body

HTTP 바디는 상대에게 전송하는 리소스를 포함하는데, 정확히는 엔티티의 바디를 포함한다.

그 외에 추가적인 정보는 들어가지 않기 때문에, 일반적으로 HTTP Body == Entity Body라 생각해도 무방하다.


그러나 예외가 존재할 수 있는데, 뒤에서 살펴볼 인코딩(Encoding)이 적용된 경우라면 Entity Body가 변형되거나 쪼개질 수 있기 때문에 메시지 바디가 최초로 전송한 엔티티의 바디와 동일하다고 여길 수 없게 될 수도 있다.

3. Content Negotiation

앞서 헤더에는 여러 정보가 포함되며, 헤더의 내용에 따라 메시지 수신 측에서 해야 할 행동을 정할 수 있다고 했다.


이를 이용해 서버와 클라이언트가 서로 리소스의 양식에 대해 일종의 ‘교섭‘을 할 수 있는데, 이러한 구조를 Content Negotiation이라 한다.


이름만 들으면 매우 복잡해 보이지만, 실상은 매우 단순한데, 클라이언트가 리퀘스트 시 클라이언트의 사양 정보를 서버에 전달하면 이러한 사양 정보를 바탕으로 클라이언트에 가장 적합한 리소스를 서버가 전송해 주거나, 혹은 서버가 전송해 준 여러 리소스 중 클라이언트가 자신의 사양에 가장 적합한 리소스를 적용하는 것을 말한다.


대표적인 예로, 브라우저에서 Google 페이지에 들어가면 클라이언트의 국가와 언어 코드에 따라 해당 언어가 적용된 구글 메인 페이지를 보여주는데, 이것이 콘텐츠 네고시에이션의 대표적인 사례이다.


참고로 당연하겠지만, 이러한 ‘교섭’은 클라이언트의 리퀘스트 헤더 필드의 정보를 기준으로 한다.

4. Encoding

HTTP로 엔티티를 전송하는 경우 엔티티를 원본 그대로 전송할 수도 있지만, 여러가지 이유에 따라 전송 효율을 더욱 높이기 위해 추가적인 방안들을 고려할 수 있다. 예컨대 엔티티의 크기가 너무 클 경우 이를 압축해 보내거나, 잘게 쪼개어 보낼 수 있다.


이처럼 엔티티를 가공해 전송 효율을 높이는 것을 인코딩(Encoding)이라 한다.


인코딩의 대표적인 방식들은 다음이 있다.

  • 압축(Content Coding)
  • 분할(Chunked Transfer Coding)
  • 여러 데이터 동시 전송(Multi-Part)
  • 범위 지정 수신(Range-Request)

4.1. Content Coding

엔티티의 크기가 한 번에 전송하기에 매우 클 경우 가장 먼저 생각해 볼 수 있는 방법이 압축이다. 이는 마치 메일에 크기가 큰 파일을 첨부해 전송할 때 이를 압축 파일로 압축해 전송하는 것과 같은 원리이다.

HTTP에서는 이처럼 엔티티 바디를 압축해 보내는 콘텐츠 코딩(Content Coding)이라는 인코딩 방식을 제공한다.


콘텐츠 코딩이 적용된 엔티티는 수신 측에서 디코딩을 해 이용하게 된다.

주요 콘텐츠 코딩 양식은 다음이 존재한다.

  • gzip(GUN zip)
  • compress(UNIX 표준 압축)
  • deflate(zlib)
  • identity(인코딩 없음 - 원본)

4.2. Chunked Transfer Coding

앞서 살펴보았던 TCP/IP 4계층에서 전송 계층은 전송할 데이터의 크기가 크면 이를 잘게 쪼개 여러 번 전송한다 했었다.


이처럼 HTTP에서도 전송하려는 엔티티의 크기가 클 경우 이를 분할해서 여러 번 전송할 수 있는데, 이를 청크 전송 코딩(Chunked Transfer Coding)이라 한다.


전송 계층에서 TCP 프로토콜이 잘게 쪼갠 정보들을 패킷(Packet)이라 부르듯 전송 코딩을 이용해 잘게 쪼갠 엔티티 덩어리들을 청크(Chunk)라 한다.


이처럼 엔티티를 잘게 쪼개어 보내면 브라우저 입장에서도 장점이 존재하는데, 일반적으로 브라우저는 엔티티 전송이 완료되지 않으면 이를 표시할 수 없다. 그러나 전송 코딩을 이용하면 전달받은 일부 엔티티들을 하나씩 표시해 가면서 부분적으로 리소스 렌더링이 가능하기 때문에 사용자가 리소스를 확인하기 위해 기다리는 체감 시간을 단축시킬 수 있다.


청크 전송 코딩을 이용할 시 각 메시지 바디에는 청크들이 들어가는데, 이 때 각각의 청크가 다음 청크가 존재할 경우 다음 청크의 사이즈를 16진수를 사용해 명시함으로써 단락을 구분하고, 마지막 전송 청크에는 0(CR+LF)을 기록하여 청크들의 전송이 완료되었음을 명시한다.

4.3. Multi-Part

메일을 전송한다고 가정할 때, 메일 안에 보내고자 하는 텍스트만 포함시켜 전송하는 경우도 있지만 대부분 이미지나 영상, 기타 파일 등을 함께 첨부해 전송한다. 이처럼 복수의 엔티티들을 메시지에 담아 보내는 경우 사용하는 방식이 멀티 파트(Multi-Part)이다.


멀티 파트는 다음과 같은 형식들이 있다.

  • multipart/form-data Web 폼으로부터 파일 업로드에 사용

  • multipart/byteranges

    Status Code 206 응답 메시지가 복수 범위의 내용을 포함하는 경우 사용


HTTP 메시지로 멀티 파트를 사용하는 경우 Content-type 헤더 필드를 사용하며, 포함된 각각의 엔티티들(파트)을 구분하기 위해 ‘boundary’라는 문자열을 이용해 바디의 단락을 지정한다.

각 엔티티의 선두에는 boundary문자열 앞에 --를 삽입해 사용한다. 그리고 마지막으로 포함된 엔티티의 뒤에는 boundary문자열의 뒤에도 --를 삽입한다.

또, 멀티 파트에서는 각 파트별로 헤더 필드가 포함되며 멀티 파트에 포함된 파트가 멀티 파트로 구성될 수도 있다.


이를 종합하면 다음과 같은 예시가 완성된다.

Content-Type: multipart/form-data: boundary=HsE34p
--HsE34p
Content-Disposition: form-data; name="field1"
Im Yongsik
--HsE34p
Content-Disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain
...(file1.txt 데이터)...
--HsE34p--

4.4. Range Request

과거 웹 환경은 대용량 파일을 전송하기 쉽지 않았다. 전송 도중 연결이 불안정해 끊기는 일이 비일비재했는데, 만약 이러한 경우 처음부터 다시 전송을 요청하는 것은 시간적인 낭비이므로 전송받지 못한 부분만을 추가로 요청하는 기능이 필요했다.


이러한 기능을 Resume이라 하는데, 이 Resume기능을 이용해 엔티티의 일부를 요청할 때 사용하는 방식이 레인지 리퀘스트(Range Request)이다.


레인지 리퀘스트를 사용하면 전체 엔티티 바이트 중 특정 구간을 명시해 원하는 크기의 바이트만을 리퀘스트할 수 있다.


예를 들면 다음과 같다.


/* 5001 ~ 10000 Byte Request */
Range: bytes = 5001-10000
/* 0 ~ 3000, 5000 ~ 7000 Byte Request */
Range: bytes = -3000, 5000-7000


레인지 리퀘스트에 대한 응답에는 Status Code로 206(Partial Content)이 포함되어 되돌아오게 되며, 리스폰스는 multipart/byteranges로 구성된다.

만약 서버가 레인지 리퀘스트를 지원하지 않는 경우 명시한 바이트 구간에 상관 없이 전체 파일을 전송하며 Status Code로 200(OK)을 포함해 전송한다.