10.1 HTTP/2.0 의 등장 배경

1.1의 메시지 포맷은 구현의 단순성과 접근성에 주안점을 두고 최적화되었다. 그러다 보니 성능은 어느 정도 희생시키지 않을 수 없었다. 커넥션 하나 통해 요청하나 보내고 응답 하나만 받는건 단순해서 좋지만, 응답을 맏아야만 그 다음 요청을 부낼수 있기 때문에 심각한 latency 를 피할 수 없었다. 이 문제를 해결하기 위해 병렬 커넥션이나 파이프라인 커넥션이 도입되었지만 성능 개선에 근본적 해결책은 못됬다.

1.1 명세가 발표된지 수십년동안 이 성능 문제 해결하고자 많은 이들이 노력해왔다.

구글은 2009년 웹을 더 빠르게하겠다는 목표 아레 SPDY(스피디) 프로토콜을 내놓았다. SPDY는 헤더를 압축하여 대역폭을 절약했고, 하나의 TCP 커넥션에 여러 요청을 동시에 보내 회전 지연을 줄이는 것이 가능했고, 클라이언트가 요청을 보내지 않아도 서버가 능동적으로 리소스를 푸시하는 기능도 갖추고 있었다.

10.2 개요

HTTP/2.0은 서버와 클라이언트 사이의 tcp 커넥션 위에서 동작한다. 이때 tcp 커넥션을 초기화하는 것은 클라이언트다.

HTTP/2.0 요청과 응답은 길이가 정의된 하나의 프레임에 담긴다. 이때 HTTP 헤더는 압축되어 담긴다.

프레임들에 담긴 요청과 응답은 스트림을 통해 보내진다. 한 개의 스트림이 한 쌍의 요청과 응답을 처리한다. 하나의 커넥션 위에 여러 개의 스트림이 동시에 만들어질 수 있으므로, 여러 개의 요청과 응답을 동시에 처리하는것 역시 가능하다.

HTTP/2.0은 이들 스트림에 대한 흐름제어와 우선순위 부여 기능도 제공한다.

HTTP/2.0은 기존의 요청-응답과 약간 다른 새로운 상호작용 모델인 서버 푸시를 도입했다. 이를 통해 서버는 클라에게 필요하다고 생각하는 리소스라면 그에대한 요청을 명시적으로 받지 않아도 클라에게 보내줄 수 있다.

기존 웹 앱과 호환성 최대한 유지하기위해, HTTP/2.0은 요청과 응답 메시지의 의미를 1.1과 같도록 유지하고있다. 1.1에서와 마찬가지로 Content-Length 헤더는 본문의 길이를 의미하며 404는 리소스를 못찾는다는 의미다. 다만, 이를 표현하는 문법은 변경되었다. 예를 들어 Content-Length ‘:content-length’ 가 되었고, 상태줄을 통해 표현하던 404 Not Found는 ‘404’값을 갖고있는 ‘:status’ 헤더로 표현하게 되었다.

10.3 HTTP/1.1 과 차이점

10.3.1 프레임

HTTP/2.0에서 모든 메시지는 프레임에 담겨 전송된다. 모든 프레임은 8바이트 크기의 헤더로 시작하며 뒤이어 최대 16383 바이트 크기의 페이로드가 온다.

프레임 헤더의 각 필드는 다음과 같다.

  • R: 예약된 2비트 필드. 값의 의미 정의안되어있다. 반드시 0이어야 한다. 받는쪽에선 이 값을 무시해야한다.
  • 길이: 페이로드의 길이를 나타내는 14비트 무부호 정수. 여기에 프레임 헤더는 포함되지 않는다.
  • 종류: 프레임 종류
  • 플래그: 8비트 플래그. 플래그 값의 의미는 프레임 종류마다 다름
  • R: 예약된 1비트 필드. 앞서본 R과 마찬가지
  • 스트림 식별자: 31비트 스트림 식별자. 특별히 0은 커넥션 전체와 연관된 프레임임을 의미한다. HTTP/2.0은 DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION 이라는 총 10가지 프레임을 정의하고 있으며, 페이로드의 형식이나 내용은 프레임의 종류에 따라 다르다.

10.3.2 스트림과 멀티플렉싱

스트림은 HTTP/2.0 커넥션을 통해 클라와 서버 사이에서 교환되는 프레임들의 돌깁된 양방향 시퀀스다.

한 쌍의 http 요청과 응답은 하나의 스트림을 통해 이루어진다. 클라는 새 스트림을 만들어 그를 통해 http 요청을 보낸다. 요청을 받은 서버는 그 요청과 같은 스트림으로 응답을 보낸다. 그러고 나면 스트림이 닫히게 된다.

1.1 에서는 한 tcp 커넥션을 통해 요청을 보냈을 때, 그에 대한 응답이 도착하고 나서야 tcp 커넥션으로 다시 요청을 보낼 수 있다. 따라서 웹브라우저들은 회전 지연 줄이려고 여러 개의 tcp 커넥션을 만들어 동시에 여러 개의 요청을 보내는 방법을 쓴다. 그러나 그렇다고 무한정 못 만드니 한 페이지에 보내야할 요청이 수백게까지 가는 오늘날에는 회전 지연을 피하기 어렵다. 파이프라인 커넥션을 통해 이것을 피할 수는 있으나 그다지 널리 구현되어 있지 않다.

그러나 HTTP/2.0 에서는 하나의 커넥션에 동시에 여러 스트림이 열릴 수 있다. 따라서 하나의 HTTP/2.0 커넥션 통해 여러 개의 요청이 동시에 보내질 수 있기에 이 문제가 쉽게 해결된다.

뿐만 아니라 스트림은 우선순위도 가질 수 있다. 예를 들어 웹브라우저가 어떤 페이지 보려할 때, 네트워크 대역폭이 충분치 않아 프레임의 전송이 느리다면, 웹브라우저는 보다 중요한 리소스를 요청하는 스트림에 더 높은 우선순위 부여가 가능할것이다. 의무사항은 아니다.

모든 스트림은 31비트의 무부호 정수로 된 고유한 식별자를 갖는다. 스트림이 클라에 의해 초기화되었다면 이 식별자는 반드시 홀수여야 하며 서버라면 짝수여야 한다. 또한 새로 만들어지는 스트림의 식별자는 이전에 만들어졌거나 예약된 스트림들의 식별자보다 커야 한다. 이 규칙을 어기는 식별자를 받았다면 에러 코드가 PROTOCOL_ERROR 인 커넥션 에러로 응답해야 한다.

서버와 클라는 스트림을 상대방과 협상 없이 일방적으로 만든다. 이는 스트림을 만들 때 협상을 위해 tcp 패킷을 주고받느라 시간을 낭비하지 않아도 됨을 의마한다.

HTTP/2.0 커넥션에서 한번 사용한 스트림 식별자는 다시 사용할 수 없다. 커넥션을 오래 사용하다보면 스트림에 할당할 수 있는 식별자가 고갈되기도 하는데 그런 경우엔 커넥션을 다시 맺으면 된다.

이처럼 동시에 여러 개의 스트림의 쓰면 스트림이 블록될 수 있다는 우려가 있다는 주장이 있다. HTTP/2.0은 WINDOW_UPDATE 프레임을 이용한 흐름 제어를 통해 스트림들이 서로 간섭해 망가지는 것을 막아준다.

10.3.3 헤더 압축

1.1 에서 헤더는 아무런 압축없이 그대로 전송되었다.

이를 개선하기 위해 HTTP/2.0 에서는 헤더를 압출하여 전송한다.

헤더는 HPACK 명세에 정의된 헤더 압축 방법으로 압축된 뒤 ‘헤더 블록 조각’들로 쪼개져서 전송된다. 받는 쪽에서는 이 조각들을 이은 뒤 압출을 풀어 원래의 헤더 집합으로 복원한다.

HPACK은 헤더를 압축하고 해제할 때 ‘압축 콘텍스트’를 사용한다. 따라서 오동작 방지하려면 항상 올바른 압축 콘텍스트를 유지해야한다.

10.3.4 서버 푸시

HTTP/2.0은 서버가 하나의 요청에 대해 응답으로 여러개를 보낼수있게 해준다. 이 기능은 서버가 클라에서 어떤 요소를 요구할 것인지 미리 알 수 있는 상황에서 유용하다.

리소스를 푸시하려는 서버는 먼저 클라에게 자원을 푸시할 것임을 PUSH_PROMISE 프레임을 보내 알려야한다. 클라가 PUSH_PROMISE 프레임을 받으면 해당 프레임의 스트림은 클라 입장에서는 ‘예약됨’상태가 된다. 이 상태에서 클라는 RST_STREAM 프레임을 보내 푸시를 거절할 수도 있다. 이거 보내면 스트림이 즉각 닫힌다.

PUSH_PROMISE 프레임을 먼저 보내는 이유는 서버가 푸시하려는 자원을 클라가 별도로 또 요청하게 되는 상황을 피하기 위해서다.

서버 푸시 사용 시 다음을 주의.

  • 서버 푸시를 사용하기로 했어도, 중간의 프락시가 서버로 부터 받은 추가 리소스를 클라에 전달하지 않을 수도 있으며, 반대로 아무런 추가 리소스를 서버로부터 받지 않았음에도 클라에게 지가 추가 리소스를 전달할 수도 있다.
  • 서버는 오직 안전하고, 캐시 가능하고, 본문을 포함되지 않은 요처에 대해서만 푸시가 가능하다.
  • 푸시할 리소스는 클라가 명시적으로 보낸 요청과 연관된 것이여야한다. 서버가 보내는 PUSH_PROMISE 프레임은 원 요청을 위해 만들어진 스트림통해 보내진다.
  • 클라는 반드시 서버가 푸시한 리소스를 총일 출처 정책에 따라 검사해야한다. 예를들어 exam.org로의 http/2.0 커넥션은 www.exam.org로부터의 푸시 응답을 허용하지 않는다.
  • 마지막으로 서버 푸시를 끄고 싶다면 SETTINGS_ENABLE_PUSH 를 0으로 설정하면됨

10.4 알려진 보안 이슈

10.4.1 중개자 캡슐화 공격(Intermediary Encapsulation Attacks)

HTTP/2.0 메시지를 중간의 프락시가 1.1로 변환 시 메시지의 의미가 변질될 가능성 있다. 1.1과 달리 2.0은 헤더 필드의 이름과 값을 바이너리로 인코딩한다. 이는 HTTP/2.0이 헤더 필드로 어떤 문자열(줄바꿈 문자열 허용됨)이든 사용할 수 있게 해준다. 이는 정상적인 HTTP/2.0 요청이나 응답이, 불법적이거나 위조된 1.1 메시지로 번역되는 것을 유발할 수 있다.

다행히 1.1메시지를 HTTP/2.0 메시지로 번역하는 과정에서 이런문제가 발생하지 않는다.

10.4.2 긴 커넥션 유지로 인한 개인정보 누출 우려

HTTP/2.0은 사용자가 요청을 보낼 때 회전지연 줄이기 위해 클라와 서버사이의 커넥션을 오래 유지하는걸 염두에 두고 있다. 이것은 개인 정보의 유출에 악용될 가능성이 있다. 예를 들어 어떤 사용자가 브라우저를 사용할 때, 그 사용자는 이전에 그 브라우저를 사용햇던 사용자가 뭘 했는지 알아낼 가능성도잇다. 이건 http 가 현재 가진 문제기이기도 하지만 짧게 유지되는 커넥션에서는 위험이 적다.