[네트워크] HTTP/2.0과 바이너리 프로토콜

HTTP/1.x와 HTTP/2.0

바이너리(이진) 프로토콜(binary protocol)이란 데이터를 바이트(byte) 형식으로 처리하는 통신 프로토콜이다.

텍스트 프로토콜과 바이너리 프로토콜의 차이는 비트 스트림(bit stream)이 처리되는 데이터의 타입이다. 텍스트 프로토콜의 경우 비트 스트림은 텍스트 데이터로 인코딩/디코딩 처리되는 반면 바이너리 프로토콜의 경우 텍스트 데이터가 아닌 특정 데이터 타입으로 처리된다. 예를 들어, 텍스트 프로토콜에서 연속적인 숫자는 한 자리씩 문자(character)로 처리되므로 한 자리 당 1바이트(8비트)를 차지하는 반면, 바이너리 프로토콜에서 숫자는 한 자리수가 아닌 전체 숫자에 해당하는 일련의 비트로 변환된다.

바이너리 프로토콜은 다음과 같은 이유로 텍스트 프로토콜 보다 효율적이고 빠른 네트워크 통신을 가능하게 한다.

  • 바이너리 프로토콜은 텍스트 보다 더 작은 크기의 바이너리 데이터를 처리한다. 따라서 네트워크 대역폭을 절약하고 데이터의 전송 시간 및 응답 지연 시간(latency)을 줄인다.
  • 데이터를 송수신할 때 사람이 읽을 수 있는 텍스트 형태로 변환할 필요가 없기 때문에 구문 분석(parse) 처리에 있어 효율적이다. 일반적으로 바이너리 데이터의 인코딩/디코딩 처리 속도는 텍스트 데이터의 처리 속도 보다 빠르다.


HTTP/1.x은 텍스트 프로토콜(text protocol)인 반면 HTTP/2.0은 바이너리 프로토콜이다. 따라서 HTTP/2.0은 데이터 처리가 보다 빠르고 효율적이다. HTTP/2.0은 또한 다음과 같은 특징을 갖고 있다.

  • 요청 및 응답의 다중화(multiplexing)
  • 응답 우선 순위(prioritization) 지정
  • 헤더 압축(header compression)


HTTP/2.0에서 HTTP 메시지는 더 작은 형태의 여러 프레임(frame)으로 분할되며 바이너리 형식으로 처리된다. 프레임은 HTTP/2.0에서 데이터 전송의 최소 단위이다. HTTP/2.0에 도입된 바이너리 프레이밍 레이어(binary framing layer)는 HTTP 메시지가 클라이언트와 서버 간에 메시지가 캡슐화되어 전송되는 방식을 지정하는 역할을 수행한다. 바이너리 프레이밍 레이어는 소켓 인터페이스와, 애플리케이션에 노출되는 고수준 HTTP API 사이에 최적화된 새로운 인코딩 메커니즘을 위한 것이다.

HTTP/2.0에서 기존의 HTTP 시맨틱스(메서드, 삭태 코드, 헤더 등)는 변경되지 않지만 HTTP 메시지 전송을 위해 데이터가 처리되는 방식이 변경된다. 개행(newline)으로 구분된 텍스트 형식의 데이터를 전송하는 HTTP/1.x과 달리 HTTP/2.0를 통해 전송되는 데이터는 바이너리 형식의 더 작은 메시지로 분할된다. 클라이언트가 HTTP 메시지를 바이너리 형식의 프레임으로 분할(인코딩)하고 이를 서버에게 전송하면 서버는 분할된 데이터를 다시 조립하여 재구성(디코딩)한다.

HTTP/2.0은 스트림(stream)이라는 추상화된 가상의 데이터 시퀀스(data sequence)를 사용한다. 스트림은 클라이언트와 서버 간에 수립된 단일 TCP 연결 내 양방향(bidirectinal)의 바이트 데이터 플로우이다. 여기서 양방향이란 의미는 하나의 스트림 상에서 요청과 응답 데이터가 전송된다는 의미이다. 스트림을 통해 전송되는 데이터는 프레임 시퀀스로 구성된다. 단일 TCP 연결에는 동시에 열려 있는 여러 개의 스트림이 포함될 수 있으며 클라이언트나 서버 모두 여러 스트림의 프레임을 인터리빙하거나 스트림을 닫을 수 있다. 즉, TCP 연결 하나로 여러 요청을 다중화 처리할 수 있다. 단일 TCP 연결에서 여러 스트림이 구성되고, 하나의 스트림에서는 하나 이상의 HTTP 메시지가 전송된다. 이때 HTTP 메시지가 하나 이상의 프레임 시퀀스로 구성된다. 데이터를 바이너리 코드 메시지로 분할하고 이러한 메시지에 번호를 매겨 클라이언트가 각 바이너리 메시지가 어느 스트림에 속하는지 알 수 있도록 한다.

프레임에는 HEADER, DATA, PRIORITY 등 여러 종류가 있으며 각각의 프레임은 프레임 종류, 스트림 식별자 등의 정보를 포함하는 공통 헤더를 갖는다.

HTTP/1.1에서는 하나의 요청이 TCP 소켓을 독점한다. 연결 당(TCP 소켓 당) 하나의 요청에 대한 응답이 처리될 수 있으며 요청에 대한 응답 처리가 동기적으로(순서대로) 수행되어야 하기 때문에 HOL 블로킹이 발생한다. 하나의 요청에 대한 응답을 전송 받은 후에 다음 요청에 대한 응답을 전송 받을 수 있다. HOL 블로킹(head-of-line blocking)이란 첫 번째 요청에 대한 응답이 늦어지는 경우 이로 인해 이후 요청들이 대기열에서 대기하게 되는 성능 제한 현상이다. 서버는 요청 받은 순서대로 응답해야만 하므로 응답 데이터 생성에 시간이 소요되거나 크기가 큰 데이터를 응답으로 반환하는 경우 이후 다른 요청에 대한 응답에 영향을 준다. 따라서 클라이언트와 서버의 요청/응답 상호작용 모델에서 성능 개선을 목적으로 처리 속도를 높이기 위해서는 한 서버에 대해 여러 개의 TCP 연결을 수립하여 물리적인 통신 병렬화가 필요하다. 통신 고속화를 위한 기능인 Keep-Alive는 TCP/IP 통신을 효율화하여 하나의 TCP 연결에서 연속된 요청이 전송될 수 있도록 하지만 TCP 연결 별로 하나의 요청 메시지에 대한 하나의 응답 메시지만 전달될 수 있으며 다중화를 통한 요청과 응답의 동시 전송은 불가능하다.

반면 HTTP/2.0에서는 다중화를 통해 하나의 TCP 연결 상에서 여러 개의 요청/응답 메시지를 동시에 전송할 수 있다. 이러한 다중화는 기존 HTTP/1.x에 존재하는, 애플리케이션 레이어의 HOL 블로킹을 해결한다. HTTP/2.0은 다중화를 통해 하나의 연결에서 여러 요청과 응답을 병렬적으로(parallelly) 전송할 수 있게 함으로써 지연 시간을 줄이고 성능을 향상시킨다. 그러나 HTTP/2.0가 TCP의 HOL 블로킹 문제를 해결하지는 않는다.

HTTP/2.0은 서버 푸시(server push) 기능을 통해 클라이언트의 요청 전에 서버가 클라이언트에 응답을 푸시할 수 있도록 한다. 서버 푸시는 웹 브라우저가 필요로 하는 리소스 응답을 캐시로 채우는데 중점을 둔다.

HTTP/2.0은 헤더 압축(header compression) 기능도 제공한다. 쿠키나 레퍼러(referrer), 인증(authorization)과 같은 정보로 인해 헤더의 크기가 커질 경우 헤더로 인한 오버헤드가 발생할 수 있다. 이 경우 헤더 압축을 통해 적은 수의 패킷으로 헤더 정보를 전송함으로써 리소스를 절약할 수 있다.

HTTP를 통해 전송되는 데이터에는 텍스트 뿐만 아니라 이미지, 음성, 영상 등의 데이터도 존재한다. 텍스트 뿐만 아니라 이러한 데이터는 결국 바이너리 형태로 전송된다. HTTP/2.0은 바이너리 데이터

각각의 데이터 타입에 대한 MIME 타입(파일의 종류를 구별하기 위한 문자열)이 존재하며 특정 서브타입이 없거나 알려진 서브타입이 없는 바이너리 데이터의 경우 application/octet-stream을 응답 헤더의 Content-Type으로 사용해야 한다.


통신 고속화

HTTP/1.1은 통신 고속화를 위해 파이프라이닝(pipelining)과 Keep-Alive 기능을 제공한다. 파이프라이닝이란 클라이언트의 첫 요청이 완료되기 전에 다음 요청을 보내는 기능이다. 파이프라이닝을 사용하면 클라이언트는 단일 TCP 연결을 통해 하나의 요청에 대한 응답을 기다리지 않고 여러 요청을 서버에게 동시에 전송할 수 있다. 그러나 서버는 요청 받은 순서대로 응답해야 하므로 하나의 응답이 지연될 경우 다른 응답에 영향을 주는 HOL 블로킹 문제가 여전히 발생 가능하다.

클라이언트가 파이프라이닝된 요청을 보내는 경우 서버는 파이프라이닝을 지원하지 않더라도 유효한 응답을 해야 한다. 현재 대부분의 경우 서버가 파이프라이닝을 올바르게 지원하지 않아 클라이언트가 이를 사용하지 않는 추세이다.

파이프라이닝 기능은 HTTP/2.0에서 스트림을 통해 개선되었다. 스트림은 단일 TCP 연결에서 바이너리 데이터를 다중으로 전송한다. HTTP/2.0에서는 통신 순서를 유지해야 한다는 제약이 사라졌으며 하나의 세션 안에 여러 스트림이 존재할 수 있다. 각 스트림의 통신은 시분할 방식으로 이루어지며, 서버는 클라이언트의 요청 순서에 관계 없이 준비가 된 순서로 응답을 반환할 수 있다. 따라서 서버의 응답 처리 속도와 응답 지연 시간이 향상된다. 이때 우선 순위를 부여해 응답의 순서를 변경하는 것이 가능하다.

Keep-Alive는 요청/응답 마다 새로운 TCP 연결을 생성하는 대신 단일 TCP 연결을 사용 및 재사용하여 여러 요청/응답을 주고받는 기능이다. Keep-Alive는 TCP 연결 수, 클라이언트와 서버의 핸드셰이크(handshake) 횟수를 줄임으로써 RTT(round-trip time)와 응답 시간을 줄인다. 그 결과로 네트워크 혼잡도(network congestion)를 감소하여 TCP/IP 통신이 효율적이고 빠르게 만든다. 클라이언트는 이 기능을 사용하기 위해 현재의 전송이 완료된 후 네트워크 접속을 유지할지 말지를 제어하는 Connection 헤더를 사용할 수 있다. 요청 헤더에 Connection: Keep-Alive 값을 포함하면 연결은 지속되고 끊기지 않으며 동일한 서버에 대한 후속 요청을 수행할 수 있다. 이 요청을 받은 서버가 Keep-Alive 기능을 지원하면, 동일한 값의 헤더를 응답에 추가해서 반환한다. 이 경우 연결이 끊어지지 않고 열린 상태로 유지되고, 이후 클라이언트가 다른 요청 전송 시 동일한 연결을 재사용한다. 동일한 연결을 사용한 요청/응답 전송은 클라이언트나 서버 중 한 쪽이 연결을 종료하기 전까지 유지된다. Keep-Alive 연결이 설정되었다면 Keep-Alive 헤더를 통해 유휴 연결의 타임아웃과 연결이 닫히기 전에 전송될 수 있는 최대 요청 수를 파라미터로 지정할 수 있다.

Keep-Alive 연결은 클라이언트나 서버 중 한 쪽이 Connection: Close 헤더를 포함하여 요청 또는 응답을 전송하거나 유휴 연결의 타임아웃이 발생할 때까지 유지된다. Keep-Alive는 하나의 TCP 연결에서 연속된 요청이 전송될 수 있도록 하지만 다음과 같은 단점이 있다. Connection: Close을 통해 클라이언트나 서버가 연결을 종료할 수는 있지만 클라이언트가 필요한 모든 데이터를 서버로부터 응답 받았음에도 불구하고 연결을 닫지 않으면 서버에서 연결을 계속 열어 두게 되며 이로 인해 다른 클라이언트에서 서버와의 연결을 사용할 수 없게 된다. 하나의 서버가 여러 클라이언트로부터 요청을 받아 처리해야 하는 시스템 구성에서 Keep-Alive을 사용하여 한 서버에 대한 연결을 유지하는 것은 클라이언트의 요청에 대한 적절한 로드 밸런싱에 영향을 줄 수 있으므로 이를 고려해야 한다. Keep-Alive 지속 시간은 클라이언트와 서버 모두 갖고 있으며 한쪽이 연결을 끊는 순간에 통신은 완료되므로 지속 시간이 짧은 쪽에 의해 통신이 종료된다.

Keep-Alive 기능 사용 시 서버가 TCP 연결을 닫는 동시에 클라이언트가 서버에 요청을 보낼 경우 클라이언트와 서버 간 경합 조건(race condition)이 발생할 수 있다. 따라서 서버는 408 에러 코드를 응답하여 이미 연결이 닫혔다는 메시지를 보내는 것이 권장된다.

Keep-Alive 기능은 HTTP/1.1에서 기본으로 적용된다. HTTP/2.0는 Connection 헤더 같은 연결-지정(connection-specific) 헤더 필드를 사용하지 않으며 관련 메타데이터는 다른 방법으로 전달된다링크. HTTP/2.0은 단일 연결에서 여러 개의 동시 요청/응답을 스트림을 통해 다중화한다.


플로우 컨트롤

TCP 연결을 통한 네트워크 통신에서 송신자와 수신자는 버퍼(buffer)라는 임시 메모리 공간을 갖는다. 송신자가 수신자의 버퍼의 크기 보다 큰 데이터를 전송할 경우 버퍼 오버플로(buffer overflow)가 발생하여 패킷이 손실될 수 있다. 이러한 문제를 막기 위해 TCP 프로토콜은 플로우 컨트롤 기능을 통해 송신자의 데이터 전송량을 제어한다. 플로우 컨트롤(flow control)(또는 흐름 제어)이란 데이터가 효율적으로 전송되도록 하기 위한 통신량 제어 처리이다. 수신자가 처리할 수 있는 데이터의 양을 송신자에게 알리고 송신자는 이를 기반으로 데이터 전송량을 조절한다.

HTTP/2.0의 스트림은 단일 TCP 연결 내에서 다중화되기 때문에 TCP 흐름 제어만으로는 애플리케이션 계층 수준에서 개별 스트림에서의 데이터 전송량을 조절할 수 없으며 스트림 간 경합이 발생할 가능성이 있다. 이러한 문제를 해결하기 위해 HTTP/2.0의 플로우 컨트롤은 단일 연결에서 스트림마다 적용되어 애플리케이션 계층 수준에서 다중화된 통신의 전송량을 제어한다. 동일한 연결 상의 여러 스트림이 서로 간섭하지 않도록 만든다.

HTTP/2.0의 플로우 컨트롤은 TCP와 기능 및 구현 방식이 유사하다. 송신자는 수신자의 최대 버퍼 크기인 윈도우(window) 크기 만큼의 데이터를 전송함으로써 전송되는 데이터의 양을 제어한다. 수신자는 송신자에게 자신이 받을 수 있는 데이터의 양을 나타내는 윈도우 크기를 지속적으로 알리며, 송신자는 이 윈도우의 크기 만큼의 데이터를 수신자에게 전송한다. 수신자는 WINDOW_UPDATE 프레임을 통해 윈도우의 크기를 동적으로 조절할 수 있다. 수신자는 송신자로부터 전송된 데이터를 처리하고 버퍼에 여유가 생기면 윈도우 크기를 조절하면서 여유 버퍼 크기를 송신자에게 다시 전달한다. 수신자는 여유 버퍼 크기 만큼 윈도우 크기를 조절하여 WINDOW_UPDATE 프레임을 다시 송신자에게 전송함으로써 데이터 통신량이 조절된다. 송신자는 수신자가 설정한 윈도우의 크기를 초과하지 않는 범위에서 데이터를 전송해야 한다.


프로토콜 버퍼

프로토콜 버퍼(protocol buffer)는 구조화된 데이터를 직렬화하기 위한, 언어 및 플랫폼 중립적인 데이터 형식 또는 데이터 변환 메커니즘이다. XML이나 JSON과 같은 텍스트 기반 데이터 형식에 비해 데이터 크기가 더 작고 간단하며 전송 속도가 빠르다. 프로토 파일(.proto)에 직렬화하려는 데이터를 어떻게 구조화할지 데이터 구조를 정의한 후 프로토 파일을 사용하여 다양한 데이터 스트림 및 언어에서 구조화된 데이터를 쉽게 쓰고 읽을 수 있다.

프로토콜 버퍼는 HTTP 프로토콜을 통해 전송되기 전에 바이너리 형식으로 인코딩된다. 프로토콜 버퍼에 대한 표준화된 MIME 타입은 없으며 application/x-protobuf, application/octet-stream 등이 사용되고 있다.


gRPC

gRPC는 구글의 원격 프로시저 호출(remote procedure call, RPC) 프레임워크이다. gRPC는 직렬화 데이터 구조인 프로토콜 버퍼를 기본 인코더로 사용하며 HTTP/2.0 프로토콜을 통해 데이터를 전송한다. gRPC는 데이터 구조를 설명하는 인터페이스 정의 언어(interface definition language, IDL), 데이터의 인코딩 및 디코딩을 위한 기본 메시지 교환 형식으로 프로토콜 버퍼를 사용한다. 프로토콜 버퍼를 반드시 사용해야 하는 것은 아니며(JSON을 사용할 수도 있다링크) 다른 인코더를 사용함으로써 데이터 처리 방식을 선택할 수 있지만 이전 버전과의 호환성, 타입 검사, 성능을 위해 프로토콜 버퍼를 사용하는 것이 좋다.


참고

Comments