이번 포스팅에선 HTTP가 발전되어온 과정과 그 특징들을 살펴보려고 한다.
HTTP/0.9
HTTP/0.9 의 경우 매우 초기 버전의 프로토콜로, 단순히 GET 요청에 응답을 주고받는 기능에 불과하였다.
HTTP/1.0
HTTP/1.0 버전에서 헤더가 추가되었고, GET외에도 PUT, POST, DELETE 등의 메소드를 구분하여 요청할 수 있게 되었다.
기본적으로 HTTP는 TCP/IP를 기반으로 동작하는데, HTTP/1.0 버전에선 동일한 서버에 대한 모든 요청에는 별도의 TCP 커넥션이 수립되어야 했다. TCP 커넥션 수립 자체도 3-way handshake 같은 과정이 수행되어 네트워크를 통해 여러번의 요청을 주고받아야 했기 때문에 이는 성능 저하, 서버 부하 비용증가로 이어졌다.
HTTP/1.1
지속 커넥션
HTTP/1.1 버전은 한번 TCP 커넥션이 수립된 후 HTTP 요청을 여러번 주고 받는 방식으로 HTTP/1.0에서 발생하는 성능 저하 문제를 개선하였다. 클라이언트에선 지속 커넥션을 위한 keep-alive 헤더를 추가하여 서버에게 HTTP 요청을 보내고, 서버는 커넥션을 유지한 상태로 마찬가지로 keep-alive 헤더를 클라이언트에게 돌려준다.
파이프라이닝
HTTP/1.1버전에서 파이프라이닝이 추가되었다는 점도 주목할 만하다. 지속 커넥션이 가능해 졌어도 매 요청마다 응답을 기다려야 하는 것은 불필요하다. 따라서, 파이프라이닝 기법을 통해 응답을 받기 전에 연속으로 요청을 보내고, 응답 또한 연속으로 받는 방식으로 이를 개선하였다.
하지만, 응답은 요청과 동일한 순서로 수신되어야하고 이를 올바르게 구현하기가 까다로웠다. 클라이언트와 서버 사이의 많은 프락시 서버거 파이프라인을 제대로 처리하지 못했고, 그 지원은 결국 많은 웹 브라우저에서 제거되었다.
Head of line blocking
또한 파이프라이닝에선 HOL(head of line blocking) 이라는 문제가 발생한다. 단일 연결을 통해 여러 요청을 전송할 수 있음에도 불구하고 여전히 요청된 순서대로 응답을 보내야 했기 때문에 응답 중 하나가 느리거나 지연되면 이후의 모든 응답 또한 영향을 받게 되어 전체적인 성능 감소가 일어난다.
HTTP/1.1 에서는 HOL blocking 에서 발생하는 퍼포먼스 문제를 해소하기 위해서 여러개의 TCP 커넥션을 열어 리소스를 요청한다.
HTTP/2.0
바이너리 프레이밍
HTTP/2.0 에서는 단일 커넥션에서 여러 요청 스트림을 보낼 수 있는 HTTP 스트림을 도입하였다. HTTP/2.0 에서는 데이터를 plain text가 아닌 바이너리 프레이밍을 통해 주고 받는다.
HTTP/1.1 까지 사용되던 plain text 개발자가 프로토콜 메세지를 읽고 이해하긴 쉬웠으나, 효율성이 떨어지고 오류가 발생하기 쉬우며 추가 처리 오버헤드로 인해 속도가 느려지는 여러가지 단점이 존재하였다. HTTP/2.0 은 이러한 한계를 해결하기 위해 바이너리 프레이밍 계층을 도입하였다. 이는 일반 텍스트 형식에 비하여 다음과 같은 장점이 존재한다.
- 효율성: 바이너리 데이터는 더 압축되어 클라이언트와 서버 간의 전송해야하는 데이터 양을 줄여준다. 따라서 대역폭 사용량도 줄어들고 통신 속도가 빨라진다.
- 오류 발생 가능성 감소: 바이너리 프로토콜은 일반 텍스트 프르토콜에서 발생할 수 있는 공백 처리나 대소문자 불일치 등의 문제로 인한 오류에 덜 취약하다.
- 더 빠른 처리: 바이너리 데이터는 컴퓨터 입장에서 변환 단계가 필요하지 않기 때문에 더 쉽고 빠르게 처리될 수 있다.
또한, 일반 텍스트 형식이 아니기 때문에 개발자가 이를 이해할 수 없겠지만, 사실상 많은 디버깅 툴에서 바이너리 데이터를 해석해 주기 때문에 이는 크게 문제가 되지 않는다.
멀티플렉싱
HTTP/2.0에선 이 바이너리 프레이밍을 사용하여 멀티플렉싱을 구현함으로써 HTTP/1.1 에서 응용 계층에서 발생하던 HOL blocking 문제를 해결하였다. 위 이미지는 한번의 TCP 커넥션 연결에서 여러개의 HTTP 요청을 프레임 단위로 분리하여 이를 스트리밍 하는 것을 보여준다.
이 방식이 어떻게 HOL blocking 문제를 해결했다는 건지 명확하게 감이 잡히지 않는다. 결국 서버가 요청받는 데이터의 양은 HTTP/1.1의 파이프 라이닝 방식과 비슷할 것이고, 서버는 결국 순차적으로 이와 관련한 요청을 처리 하게 될텐데, 어떻게 이 방식은 HOL blocking 을 해결할 수 있는 것일까? 또한, 어떻게 HTTP 스트림은 순서를 보장할 필요 없이 데이터 전송이 이루어 질 수 있는걸까?
먼저 HTTP/2.0 의 스트림이 순서를 신경쓸 필요가 없는 이유는 스트림 헤더에 ID를 부여하여 이를 독립적으로 관리하기 때문이다. 응답이 어떤 순서로 오든 해당 스트림 ID 를 통해 요청과 응답을 연결시킬 수 있다. 이에 비해 기존 HTTP/1.1 에서는 스트림 아키텍쳐 기반의 고유 식별자가 없기 때문에, 파이프 라이닝 기법을 통해 연속적으로 요청을 보냈더라도 클라이언트는 응답이 온 순서대로 요청과 응답을 연결지어야 한다. 따라서 서버는 여전히 요청과 동일한 순서로 응답을 보내야 했고, 이에 따라 후속 요청이 먼저 처리가 되어도 이를 응답하지 못하는 HOL blocking 상황이 생긴다. 예를들어, 1번 요청과 2번 요청을 다른 스레드에서 병렬적으로 처리되는 상황에서 2번 요청이 먼저 처리가 완료되어도 1번이 끝날 때까지 기다려야 하는 것이다.
그에비해 HTTP/2.0 에서는 스트림 아키텍쳐를 사용하기 때문에 각각의 응답이 순서대로 전송될필요가 없어 응용 계층에서 HOL blocking 문제가 해소될 수 있는 것이다.
Stream Prioritization
더 나아가, HTTP/2.0 에선 클라이언트는 스트림의 우선순위를 지정할 수 있어 서버가 우선순위가 높은 요청을 먼저 처리할 수 있다. 즉, 중요한 리소스 (CSS, JavaScript 파일)을 더 일찍 로드하여 웹 페이지를 빠르게 렌더링함으로써, 만약 모든 요청을 처리하는 데 걸리는 총 시간이 동일하더라도 사용자 경험을 개선할 수 있다.
Header Compression
그 외에도 HTTP/2.0 에선 서버와 클라이언트간 전송되는 데이터 양을 줄이기 위한 방법으로 헤더 압축을 도입하였다. 사실상 한 커넥션에서 주고받는 응답과 요청의 경우 HTTP 헤더에 중복되는 정보가 많다. HTTP/2.0 에선 HPACK이라는 압축 알고리즘을 사용하여 중복된 헤더 데이터를 감소시켰다.
서버 푸시
서버 푸시는 클라이언트가 명시적으로 리소스를 요청하기 전에도 서버가 선제적으로 리소스를 클라이언트에게 전송할 수 있는 기능이다. 이를 통해 서버와 클라이언트간의 요청과 응답횟수를 줄일 수 있다.
예를들어, 클라이언트가 처음 서버에 접속하며 HTML 파일을 요청하는데, 기존 방식에선 이를 응답 받은 후 해당 HTML 파일에 필요한 CSS 및 JavaScript 파일을 다시 서버에게 요청하게 된다. 이 때 서버 푸시를 활용하면 서버는 클라이언트에게 HTML 파일 보낸 후 이와 관련한 CSS, JavaScript 를 요청할 것을 예상하여 별도의 요청 없이도 이어서 해당 리소스들을 클라이언트에게 보내는 것이다.
실제 동작방식으로, 서버에선 PUSH_PROMISE 라는 특수한 프레임을 보내며, 이 때 기존 어떤 ID의 스트림과 관련된 리소스인지를 포함한다. 클라이언트는 이를 수신하면 해당 리소스를 수락할지 거부할지 결정할 수 있다. 만약 클라이언트가 리소스를 거부한다면 푸시된 리소스를 원하지 않는다는 의미로 RST_STREM 프레임을 서버에 전달한다.
HTTP/1.1 과의 응답 시간 비교
위 사이트에서 HTTP/1.1과 HTTP/2의 처리 시간을 비교해 볼 수 있다. 200개의 작은 이미지를 요청한 후 모든 응답이 오는데 까지 걸린 시간을 비교한 것이다.
DevTools의 Network 기능을 통해 이에 대하여 자세히 살펴보면 다음과 같은 특징을 발견할 수 있다.
HTTP/1.1 의 경우 작은 이미지를 요청한 순서가 뒤죽박죽이다. 이는 여러개의 커넥션을 사용하여 병렬적으로 요청을 보냈기 때문이며, Connection ID 를 보면 약 6개의 커넥션이 반복적으로 나타나는 것을 확인할 수 있다. 커넥션을 맺는데 발생하는 지연과 각 커넥션의 HOL blocking 에 의한 지연이 발생할 것을 예상할 수 있다.
HTTP/2.0 의 경우 이미지 요청순서가 정렬되어있는 것을 확인할 수 있다. 이는 한개의 커넥션을 사용하여 요청을 스트리밍했기 때문이며, Connection ID 또한 하나만 존재하는 것을 확인할 수 있다.
결론적으로 HTTP/1.1 와 HTTP/2.0 의 응답시간 차이는 여러개의 커넥션을 생성하는데 걸리는 지연, 각 커넥션의 HOL blocking 에 의한 지연, 헤더 압축에 따른 데이터 크기 차이 때문에 발생한다고 볼 수 있다.
다만 위 예시에서 한가지 헷갈리는 점이 있다. HTTP/2.0 은 multiplexing 을 사용하기 때문에 데이터 요청과 응답의 순서가 정해져 있지 않다고 하였는데, tile 번호를 보면 순서가 너무 잘 정렬되어 있다. 사실 이는 이 데모의 특성 때문인데, 각 작은 이미지 요청의 처리가 복잡하지 않기 때문에 서버에서는 그저 이를 받은대로 처리하여 보내는 것이다. 여러 요청이 리소스에 따라 시간이 달라지는 복잡한 시나리오에서는 요청과 다른 순서로 응답이 도착하는 양상을 확인할 수 있을 것이다.
한계
HTTP/2.0 은 응용계층에서 HOL blocking 문제를 해결하였으나, 여전히 전송계층의 HOL blocking 문제가 존재한다. 즉, TCP 프로토콜에서 발생하는 HOL blocking 문제에 대해선 어떠한 개선도 이루지 못하였다.
이 문제에 대하여 조금 더 자세하게 살펴보자. TCP는 각 패킷에 시퀀스 번호를 할당하여 안정적인 데이터 전송을 보장한다. 수신측에서는 패킷을 승인하고 승인 번호를 발신자에게 다시 보내는데, 만약 패킷이 손실된 경우에 수신자는 이를 재전송 받을 때 까지 그 뒤에 패킷들을 처리할 수 없다. 이는 TCP가 어플리케이션 계층에 데이터를 올려줄 때 이에 대한 순서를 보장하기 때문이다.
패킷 손실을 감지하고 누락된 패킷을 재전송하는 데 걸리는 시간 동안 후속 패킷들은 성공적으로 도착했더라고 대기 상태에 놓이게 된다. 이러한 문제가 TCP 에서 발생하는 HOL Blocking 문제인 것이다.
즉, HTTP/2.0 에서는 열심히 한 커넥션 내에서 여러 요청을 스트리망 하지만, TCP에서는 이에 대한 어떠한 정보도 알지 못한다. 이에 따라 패킷 로스가 발생하면 2.0 에서는 커넥션 내의 여러 요청 중 하나가 실패한 것에 불과하지만, 전송 계층에선 그 외에 모든 요청도 영향을 받아 HOL blocking 문제가 발생하는 것이다.
패킷 로스가 많이 일어나는 상황에선 여러 커넥션을 사용하는 HTTP/1.1 이 HTTP/2.0 보다 더 좋은 퍼포먼스를 보일 수 있다고 한다.
HTTP/3.0
HTTP/3.0은 UDP를 기반으로 동작하는 QUIC 프로토콜을 채택하였다.
QUIC 에서 각 스트림은 독립적으로 동작하여, 이에 따라 한 스트림의 패킷 로스가 다른 스트림에게 영향을 미치지 못하도록 하였고, 이를 통해 TCP 에서 발생하던 HOL blocking 문제를 해결하였다.
HTTP/3.0 이 가진 여러가지 주요 특징 몇가지를 살펴보자.
Faster Connection Establishment & Built-in encryption
TCP에선 3-way handshake 를 통하여 커넥션을 맺는다. 더 나아가 커넥션이 맺어진 뒤에도 보안을 위한 HTTPS 연결을 위해 TLS 핸드셰이크도 진행하게 된다.
위와 같이 TCP + TLS 방식에서는 커넥션과 HTTPS 연결을 맺기 위해 여러번의 요청과 응답을 주고받아야 했다. 그에 비해 QUIC 에서는 TLS 를 내장하여 커넥션과 TLS 핸드셰이크를 한번의 왕복으로 끝내며 바로 데이터 전송을 시작할 수 있어 더욱 효율적이다.
Connection migration
집에서 스마트폰에 와이파이를 연결한 상태로 유튜브 영상을 보다가, 밖으로 나가면서 이를 끊김없이 계속 시청한 경험이 있을 것이다. 이는 유튜브 영상에 대한 요청과 응답이 QUIC 프로토콜을 사용하기 때문이 가능한 것이다.
반대로 TCP를 사용할 경우에는 네트워크 환경이 바뀔 경우 새로운 커넥션을 맺어야 하기 때문에 영상이 중간에 끊기는 현상이 발생힌디.
와이파이 환경에서 LTE 환경으로 변경된다면 IP주소가 변경될텐데 QUIC에선 어떻게 커넥션을 유지하는 것일까?
QUIC 에서는 Connection ID 라는 개념을 도입하여 여러 커넥션을 unique 하게 구분한다. 네트워크 환경이 변경되었을 때 클라이언트는 기존에 유지하던 Connection ID와 함께 IP 주소나 포트번호가 변경되었음을 서버에게 알리고, 서버는 이를 통해 클라이언트가 변경되었다는 것을 인지하며, 해당 IP주소와 포트로 도착지를 변경하여 같은 커넥션을 사용해 계속 데이터를 주고 받는 것이다.
마무리
2021~2022 년 기준, 크롬 브라우저에서 사용되는 HTTP 버전별 점유율은 위와 같다.
HTTP/3.0 이 빠르게 증가하고 있는 추세이다.
일반적인 브라우저 사용자 입장에선 브라우저가 알아서 요청을 처리해주기 때문에 이에 관해 전혀 신경쓸 필요가 없지만, 서버를 관리하는 입장에선 HTTP 버전을 직접 관리하고 변경하는 작업이 필요할 수 있을 것이다.
참고 문헌
'네트워크' 카테고리의 다른 글
[OSI 7 Layer] Application Layer (1) | 2023.06.02 |
---|---|
[OSI 7 Layer] Transport Layer (1) | 2023.05.08 |
[OSI 7 Layer] Network Layer (0) | 2023.04.19 |
[OSI 7 Layer] Link Layer (0) | 2023.04.19 |
[OSI 7 Layer] Overview & Physical Layer (0) | 2023.04.17 |