최근에 러닝 HTTP/2 라는 책을 다 읽게되어, 이를 리뷰하는 김에 해당 글을 작성하게 되었습니다.
해당 책을 하나의 글로 요약할것이지만, 상세한 내용은 책을 읽어보는것을 추천합니다. 제가 깨달은 바, 그리고 책의 내용이 뒤섞여있기 때문에 책이 더 정확할수도 있습니다! 🤗
HTTP/2에 관해서 서술하기 이전에 HTTP의 발전 역사에 관해서 서술을 해보고자합니다.
HTTP/0.9는 매우 단순한 프로토콜이었습니다. 메소드는 GET만이 존재했고 HTML만을 주고받을 수 있게 설계되었습니다.
그리고 HTTP 헤더또한 존재하지 않았습니다.
HTTP/1.0에 들어서 꽤 많은 기능이 추가되었는데, 다음과 같은 것들이 추가되었습니다.
하지만 HTTP/1.0은 REST하기 위한 요건들을 만족못한 부분이 있었기도하고, 1개의 요청 당 1개의 TCP 커넥션을 맺고 끊기 때문에 TCP Handshake 오버헤드가 상당히 많이 발생한다는 단점이 있었습니다.
REST를 만족하기 위한 조건은 아래와 같은 것들이 있습니다.
이 중에서 HTTP/1.1 들어서 Caching을 강하게 지원하기 시작하였고, Uniform Interface 요건 중 Self-Descriptive를 만족시키기 위해서 Request Header에서 HOST 헤더를 필수 항목으로 넣었습니다.
그리고 HTTP/1.1 부터는 1개의 요청마다 TCP Connection을 맺고 끊지않고, Application Layer에서도 TCP에서 적용되던 Timer 개념을 도입하여 여러개의 요청을 하나의 커넥션으로 해결할 수 있게되었습니다.
그 외에도 HTTP/1.1 버전부터 파이프라이닝 이라는 기능이 지원되기 시작하였지만, HOL(Head of Line) Blocking 현상 때문에 HTTP/1.1 버전에서는 파이프라이닝을 구현하는 곳은 거의 없습니다. (이거는 차차 설명드리겠습니다.)
지연 시간이란, 패킷 하나가 다른 지점까지 이동하는데 걸리는 시간을 의미합니다. 여기서 추가로, RTT(Round Trip Time)이라는 개념 또한 존재하는데, 지연시간 * 2 의 값을 의미합니다.
대역폭이란, 현실 세계로 따지만 도로의 폭이라고 생각하시면 되겠습니다. 하나의 연결에 대한 용량을 의미하는데, 대역폭이 작은 경우 포화 상태에서 병목이 발생할 수 있습니다.
연결을 수립하려면 TCP 계층에서 3-way handshake라는 과정을 거쳐야합니다. 3-way handshaking 과정은 아래의 단계를 거쳐 수립됩니다.
이 과정에서 병목점은 다음과 같습니다.
클라이언트가 만약 HTTPS 연결을 하고있다면 최초 연결 시 TLS Handshake를 해서 서로의 대칭키를 주고받아야합니다. 이 과정에서 약간의 병목이 발생합니다.
그 외에도 서버 측에서 렌더링하는데 걸리는 시간 또한 성능 지표에 포함이 됩니다.
우선 이전에 언급했던 HOL Blocking 문제부터 언급하고 가겠습니다.
HTTP/1.1까지의 패킷 교환은 선로가 1개뿐인 열차의 행렬 이라고 생각하면 매우 쉽습니다.
HTTP/1.1 까지는 HTTP 데이터를 순차 처리하기 때문에 먼저 들어온 요청의 처리가 오래 걸리면 뒤따르는 요청에 대한 처리는 계속 밀릴 수 밖에 없습니다. 이를 Head of Line Blocking 현상이라고 부릅니다.
말 그대로, HTTP/1.1 까지의 브라우저들은 HOL 블로킹 현상으로 인한 병목 현상을 막기 위해 TCP Connection을 6개 정도 맺어두고 시작했습니다.
그런데 이것도 그거대로 문제인데,
🥲 GET 요청에 걸리는 메시지 헤더의 총 크기를 63Kb 정도로 추산해봅시다. TCP의 한 개 Segment Size는 1460 byte이기 때문에, 리노 알고리즘에 따라서 혼잡제어 알고리즘을 수행하면 대략 63Kb를 보내는데 6번 정도의 Congestion 널뛰기를 거친다고 알려져있습니다. (Stadium Effect에 대한 고려는 배제하였습니다)
결론적으로, TCP 연결을 다중으로 맺는것은 일시적인 해결책이 될수는 있지만, TCP가 각각의 연결을 최적화해주지는 않기 때문에 여기서 병목이 발생할 수 있습니다.
한 브라우저를 로딩한다고 가정해봅시다. 이 때 GET요청이 서버를 향해 날아갈텐데, 하나의 Header가 460 byte, 그리고 개체의 수는 140개라고 가정하면 브라우저를 로딩하는데 헤더만 63Kb가 들어갑니다.
여기서 헤더의 내용이 겹치는 부분이 많을것이지만, HTTP/1.1까지는 이 헤더에 공통적인 내용이 있다고 하더라도 이를 떼낼 방법이 없었기 때문에 곧이곧대로 보내야합니다. 여기서 속도 저하의 요소가 하나 추가됩니다.
HTTP/1.1 까지는 한번에 하나의 요청/응답을 순차처리할 수 있다고 말한 바 있습니다.
여기서 만약에 특정 응답이 높은 우선순위를 가져서 먼저 처리가 되어야한다고 가정하면, 아래의 문제가 유발됩니다.
위의 문제들을 해결하기 위해 HTTP/2는 아래의 사항을 채택하였습니다.
HTTP/2부터는 데이터의 효율적인 전송을 위해서 데이터 부분과 그 외의 정보를 담는 프레임 계층으로 분리하여 각각을 따로 보내기 시작했습니다. 이게 아무래도 HTTP/2의 핵심이 아닐까 생각합니다.
HTTP/2는 이걸 이용해서 많은 부분을 개선하였습니다. 자세한거는 차차 설명드리겠습니다.
이전의 HTTP/1.1 까지는 모든 데이터를 응용계층에서 하위 계층으로 text 타입으로 보냈었습니다. 해당 특징은 아래의 문제를 야기한 바 있습니다.
Message를 데이터 레이어와 프레임으로 쪼개서 보내기 때문에, 전송 메세지의 다중화가 가능해졌습니다. 따라서 요청을 서로 뒤섞어서 보내는게 가능해지다보니 아래의 개선이 이루어졌습니다.
Flow Control 기능은 TCP에서 등장하는 개념이었습니다. 이게 HTTP/2에 들어서 Application layer에서도 추가되었습니다.
이또한 Frame 계층을 이용해 구현이 되었는데, 요청을 보낼 때 함께 WINDOW_UPDATE 프레임을 함께 보냄으로써, 자신의 수신 가능한 최대 바이트 수를 서버측으로 전달하여 Client또한 전송 데이터의 속도 조절에 참여할 수 있게되었습니다.
HTTP/2 부터는 약속된 개체를 Client에 미리 전달해두는 것이 가능해졌습니다. 다름 아닌, PUSH_PROMISE라는 프레임을 client 측에 전송함으로써 이를 구현했습니다.
이를 통해서 얻은 이점은 아래와 같습니다.
하지만 이를 위해선 약속되어야하는게 몇가지가 있습니다.
이전에도 말했다시피, 460 byte 정도의 헤더를 140번 정도 날리게되면 63Kb 만큼의 헤더가 발생합니다. 여기서는 공통된 내용이 많기 때문에 이를 뽑아낼 방법이 필요했는데, HTTP/2부터는 Frame layer의 형태로 헤더가 분리되어 날아가기 때문에 공통된 부분을 미리 보내고 별도의 추가 내용은 따로 보내는 방식으로 헤더의 크기를 줄여내는게 가능하기도합니다.
여기서 추가적으로, HTTP/2부터는 허프만 인코딩 알고리즘을 이용해서 헤더를 압축하는것 또한 가능하기 때문에 기존의 방식보다 대략 90% 정도의 바이트 수를 줄일수있다고 알려져있습니다.
HTTP/2의 단점을 소개해보겠습니다.
HTTP/2는 파이프라이닝 기능이 지원되기 때문에 TCP 다중 연결은 안티패턴으로 통합니다.
이로 인해서 발생하는 문제일수도 있는데, TCP 혼잡제어의 널뛰기 현상이 패킷 손실에 의해 발생해버리면 CWND가 1로 초기화되어서 다시 널뛰기를 시작해야하기 때문에 여기서 커다란 병목이 발생할 여지는 충분히 존재합니다.
1번의 연장일수도 있는데, single connection에서 패킷 로스가 터지면 TCP는 CWND가 1로 깎이거나, 그 외의 TCP 구현에 의해 손해를 볼 때가 있습니다.
구글에서 개발한 QUIC 이라는 Transport layer의 프로토콜이 있습니다.
해당 프로토콜은 UDP를 기반으로 만든 전송계층 프로토콜인데, 특징은 아래의 것들이 있습니다.
그리고 구글은 이를 이용해 HTTP/3를 구현하여 현재 사용중에 있다고 알려져있습니다.