사전 HTTP-2
프로토콜

HTTP-2

gabury1고친 사람 github-actions[bot]

HTTP/2

HTTP/2 는 브라우저와 서버가 연결 하나 위에서 여러 요청을 한꺼번에 주고받게 하는 웹 전송 규칙입니다. 앞선 판에서는 요청을 보낸 쪽이 그 응답을 다 받아야 다음 요청을 내보낼 수 있었습니다. HTTP/2 는 오가는 것을 작은 조각으로 잘라 조각마다 번호를 붙이고, 그 조각들을 한 연결에 섞어 흘려보냅니다.

쉽고 빠른 이해

HTTP/2 는 웹페이지가 필요한 파일을 연결 하나로 한꺼번에 받아 오게 해 주는 전송 규칙입니다. 그림 백 장이 걸린 페이지를 열면 백 번의 요청이 그 연결 하나 위를 동시에 흘러갑니다.

이게 없으면 연결 하나가 한 번에 한 가지 일만 할 수 있습니다. 그래서 앞선 판을 쓰는 브라우저는 서버마다 연결을 예닐곱 개씩 따로 열어 두고, 그 수를 넘는 요청은 줄을 세워 기다리게 했습니다.

어떻게 도나:

  1. 연결을 하나 맺고 서로 인사말을 주고받아 이 규칙으로 말하기로 맞춥니다
  2. 요청과 응답을 작은 조각으로 자르고 조각마다 어느 요청 것인지 번호를 붙입니다
  3. 번호가 다른 조각들을 한 연결에 섞어 보냅니다
  4. 받는 쪽이 번호를 보고 원래 요청과 응답으로 도로 맞춥니다

대가도 있습니다. 조각을 섞어 보내도 그 조각들은 결국 연결 하나를 지나므로, 아래쪽에서 조각 하나가 사라지면 뒤따르던 것 전부가 같이 멈춥니다. 사람이 그대로 읽을 수 없는 이진 형식이라 오가는 내용을 눈으로 들여다보기도 어려워집니다.

상세

HTTP/2(HyperText Transfer Protocol version 2)는 웹에서 요청과 응답을 주고받는 규칙인 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)의 두 번째 판입니다. 요청 방식과 헤더와 본문이라는 구성은 손대지 않고, 그것을 선 위로 실어 나르는 방법만 바꿨습니다. 그래서 같은 웹 서비스가 주고받는 내용을 고치지 않고도 이 판으로 옮겨 갈 수 있습니다.

바꿀 까닭은 앞선 판인 HTTP/1.1 에 있었습니다. 거기서는 연결 하나가 요청 하나를 처리하는 동안 다른 요청을 받지 못했습니다. 그래서 이미지와 스크립트가 수십 개씩 걸린 페이지를 열려면 브라우저가 연결을 여러 개 열어 두거나, 나머지 요청을 줄 세워 기다리게 해야 했습니다.

아래에서는 연결 안에 놓이는 통로, 그 통로를 흐르는 조각, 헤더를 줄이는 압축, 한 번의 왕복, 몰림을 막는 장치, 그래도 남는 막힘, 그리고 이 판이 맞는 때를 차례로 봅니다.

연결 하나에 통로 여럿

HTTP/2 는 연결 하나 안에 스트림이라는 독립된 통로를 여러 개 둡니다. 스트림 하나가 요청 하나와 그에 대한 응답 하나를 맡습니다. 통로가 서로 독립이라 한 요청이 오래 걸려도 옆 통로의 요청은 그것을 기다리지 않습니다.

flowchart TD
    C["클라이언트"] --> CONN
    subgraph CONN["연결 하나"]
        S1["스트림 1 · 문서 요청"]
        S3["스트림 3 · 그림 요청"]
        S5["스트림 5 · 스크립트 요청"]
    end
    CONN --> SV["서버"]

스트림에는 번호가 붙습니다. 클라이언트가 연 스트림은 홀수 번호를, 서버가 연 스트림은 짝수 번호를 씁니다. 번호를 이렇게 갈라 두면 양쪽이 동시에 스트림을 열어도 같은 번호를 집는 일이 없습니다.

0번만은 통로가 아닙니다. 연결 전체에 걸리는 이야기, 이를테면 설정을 알리거나 연결을 닫자고 말할 때 이 번호를 씁니다.

오가는 모든 것이 프레임이다

스트림 위를 흐르는 조각을 프레임이라고 부릅니다. 프레임은 9바이트짜리 머리와 그 뒤에 붙는 내용으로 이루어집니다. 머리만 읽으면 내용을 몇 바이트 읽어야 하는지, 이 조각이 무엇인지, 어느 스트림 것인지를 알 수 있습니다.

packet-beta
0-23: "길이 24비트"
24-31: "종류 8비트"
32-39: "플래그 8비트"
40: "예약"
41-71: "스트림 번호 31비트"

머리가 이렇게 고정된 크기라 받는 쪽은 언제나 9바이트를 먼저 떼어 읽고 나머지를 가늠합니다. 길이 칸이 24비트이므로 한 프레임에 담을 수 있는 내용은 최대 16,777,215바이트입니다. 다만 상대가 더 큰 값을 받겠다고 알리기 전까지는 16,384바이트를 넘겨 보내지 않습니다.

프레임은 종류마다 하는 일이 다릅니다. 헤더를 싣는 HEADERS, 본문을 싣는 DATA, 연결 설정을 알리는 SETTINGS, 스트림 하나만 취소하는 RST_STREAM 이 대표적입니다. 스트림 하나를 취소해도 연결은 살아 있고 다른 스트림도 그대로 흐릅니다.

헤더를 표 번호로 줄이는 압축

한 페이지를 여는 요청 수십 개는 헤더가 거의 같습니다. user-agent 나 accept 처럼 긴 값이 요청마다 글자 그대로 되풀이되면 그 양이 본문보다 커지기도 합니다. 그래서 HTTP/2 는 헤더를 압축해서 보냅니다.

압축 방식의 이름은 HPACK(Header Compression for HTTP/2, 헤더 압축)입니다. 보내는 쪽과 받는 쪽이 같은 표를 하나씩 들고 있다가, 이미 한 번 보낸 이름과 값은 다음부터 표의 번호로만 가리킵니다. 자주 쓰이는 이름과 값은 처음부터 정해진 표에 들어 있어 첫 요청부터 번호로 부를 수 있습니다.

요청 방식과 경로처럼 헤더가 아니었던 것도 여기서는 헤더처럼 실립니다. :method · :path · :scheme · :authority 넷이 그것이고, 이름이 콜론으로 시작해 보통 헤더와 구분됩니다. 이렇게 두면 압축의 표가 이 넷까지 함께 줄여 줍니다.

한 번의 요청과 응답

sequenceDiagram
    participant 클 as 클라이언트
    participant 서 as 서버
    클->>서: 인사말 24바이트 + SETTINGS
    서->>클: SETTINGS
    클->>서: HEADERS · 스트림 1 · 요청 헤더
    Note over 클,서: 보낼 본문이 없으면 요청은 여기서 끝난다
    서->>클: HEADERS · 스트림 1 · 응답 헤더
    서->>클: DATA · 스트림 1 · 응답 본문

클라이언트가 맨 앞에 보내는 인사말은 PRI * HTTP/2.0 으로 시작하는 24바이트짜리 글자열입니다. 앞선 판만 아는 서버가 이 글자열을 받으면 요청으로 읽어 내지 못하고 거기서 멈춥니다. 말이 안 통하는 상대와 계속 주고받는 것을 처음부터 막으려고 이렇게 생긴 인사말을 골랐습니다.

인사말 뒤에는 양쪽이 SETTINGS 프레임을 하나씩 보냅니다. 한 번에 열 수 있는 스트림 수나 받을 수 있는 프레임 크기 같은, 상대가 알아야 지킬 수 있는 값들이 여기 담깁니다. 받은 쪽은 그 값을 잘 받았다고 답해 줍니다.

몰림을 막는 흐름 제어

받는 쪽이 처리하는 속도보다 보내는 쪽이 빠르면 데이터가 쌓이기만 합니다. 그것을 막으려고 HTTP/2 는 받을 수 있는 남은 양을 양쪽이 세어 둡니다. 이 세기를 흐름 제어라고 부릅니다.

남은 양은 스트림마다 따로, 그리고 연결 전체로 또 한 번 셉니다. 처음 값은 둘 다 65,535바이트이고, 본문을 싣는 DATA 프레임만 이 양을 깎습니다. 받은 쪽이 쌓인 것을 처리하고 나면 WINDOW_UPDATE 프레임으로 여유가 늘었다고 알립니다.

그래도 남는 막힘

스트림을 아무리 나눠도 그 조각들은 결국 TCP(Transmission Control Protocol, 전송 제어 프로토콜) 연결 하나를 지납니다. 중간에서 조각 하나가 사라지면 TCP 는 그것이 다시 올 때까지 뒤에 도착한 것을 위로 올려 주지 않습니다. 사라진 조각과 아무 상관이 없는 스트림까지 같이 멈추는 이 현상을 헤드 오브 라인 블로킹이라고 부릅니다.

이 막힘은 HTTP/2 안에서는 풀 수 없습니다. 연결 하나가 순서를 지켜 나르는 성질은 TCP 의 것이고, HTTP/2 는 그 위에 얹혀 있기 때문입니다. 그래서 다음 판은 아래를 TCP 가 아닌 QUIC 으로 바꿨습니다. QUIC 은 Quick UDP Internet Connections 를 줄인 말로 알려졌지만, 지금은 그 풀이를 쓰지 않고 이름 그대로 부릅니다.

요청받지 않은 응답을 서버가 먼저 밀어 넣는 서버 푸시도 규격에 들어 있습니다. 다만 쓰기가 까다롭습니다. 클라이언트가 이어서 무엇을 요청할지 서버가 미리 맞혀야 하고, 클라이언트 캐시에 이미 있는 것을 또 보내면 오히려 손해이기 때문입니다. 클라이언트는 SETTINGS 로 이 기능을 꺼 달라고 말할 수 있습니다.

언제 쓰고 언제 안 쓰나

요청이 여럿이고 그 연결을 오래 붙잡고 있는 쪽일수록 얻는 것이 큽니다. 파일 수십 개를 한꺼번에 받아 가는 웹페이지, 그리고 서버끼리 계속 호출을 주고받는 통신이 그런 자리입니다. 서버 사이의 호출 규약인 gRPC 가 이 판 위에서 도는 것도 같은 까닭입니다.

반대로 요청 한두 개를 던지고 연결을 끊는 호출이라면 얻는 것이 적습니다. 인사말과 설정 교환이 앞에 붙는 만큼 처음 한 번은 오히려 손이 더 갑니다.

브라우저는 TLS 위에서만 이 판으로 말합니다. 어느 판으로 말할지는 TLS 인사를 나눌 때 ALPN(Application-Layer Protocol Negotiation, 응용 계층 프로토콜 협상)이라는 확장으로 미리 맞춥니다. 그래서 서버에 HTTP/2 를 켜는 일은 대개 인증서를 다루는 리버스 프록시 쪽 설정으로 끝납니다.

관련 항목

HTTP/2 를 정의하는 표준 문서

RFC 9113 · RFC 7541 · RFC 7540 · RFC 9110 · RFC 9112 · IETF

HTTP/2 연결을 이루는 구성 요소

스트림 · 프레임 · 스트림 식별자 · 프레임 헤더 · 의사 헤더 · 연결 프리페이스 · SETTINGS 프레임

HTTP/2 가 헤더를 줄이는 데 쓰는 수단

HPACK · 정적 표 · 동적 표 · 허프만 부호화 · 헤더 · 헤더 압축

HTTP/2 가 얹히는 아래쪽 프로토콜

TCP · TLS · ALPN · IP · 소켓

HTTP/2 와 같은 쓰임을 두고 겨루는 전송 규칙

HTTP · HTTP/1.1 · HTTP/3 · QUIC · 웹소켓

HTTP/2 가 줄이려 한 문제와 그 시절의 우회책

헤드 오브 라인 블로킹 · 멀티플렉싱 · 커넥션 풀 · 도메인 샤딩 · 스프라이트 시트 · 파이프라이닝

HTTP/2 연결의 흐름을 조절하는 장치

흐름 제어 · 윈도 크기 · WINDOW_UPDATE · RST_STREAM · 서버 푸시

HTTP/2 위에 얹혀 도는 규약

gRPC · 트레일러 · 서버 전송 이벤트 · REST

HTTP/2 를 구현해 내보내는 서버와 도구

nginx · 리버스 프록시 · Envoy · curl · Netty · 브라우저

다른 이름: HTTP/2 · h2 · HTTP version 2