Network

[러닝 HTTP/2] 5. HTTP/2 프로토콜

noahkim_ 2025. 3. 30. 20:08

스티븐 루딘, 하비에르 가르사 님의 "러닝 HTTP/2" 책을 정리한 글입니다.

 

1. HTTP/2의 계층

프레이밍 계층

  • HTTP 프로토콜을 효율적으로 주고받기 위한 기능을 수행
특징 설명
바이너리 프로토콜 사용
HTTP/2는 텍스트 기반이 아닌 바이너리 프로토콜을 사용하여 효율적 전송
헤더 압축
반복적인 헤더 데이터를 압축하여 네트워크 효율성을 높임
암호화
데이터를 암호화하여 보안성을 강화 (기본적으로 HTTPS 사용)
다중화
하나의 연결을 통해 여러 요청과 응답을 동시에 처리하여 성능 향상

 

데이터 계층

  • HTTP1.1 과의 호환성 유지
  • 실제 데이터 전송

 

2. 연결

  • h2에서 모든 프레임과 스트림은 하나의 연결을 통해 전송됨

 

매커니즘

 

  1. 클라이언트가 연결을 시작
    • 새 연결 열기 or HTTP/1.1에서 Upgrader: h2c
    • HTTP1.1에서 HTTP/2.0로 업그레이드된 경우를 고려하여 설계됨
  2. 클라이언트가 초기 데이터 전송
    • 클라이언트는 연결을 시작할 때 특정한 16진수 옥텟 스트림을 첫번째 데이터로 전송
    • 서버가 HTTP/2.0 연결을 지원하는지 확인하기 위함
  3. 클라이언트가 SETTINGS 프레임 전송
    • 서버가 h2로 통신할수 있음을 확인하기 위한 목적
    • 서버가 이를 수신하면 클라이언트는 ACK 및 자신의 SETTINGS 프레임으로 응답
      • 클라이언트는 서버의 응답을 기다리지 않고 미리 프레임 전송을 할 수 있음
  4. SETTINGS 프레임 협상 완료 
    • 만약 클라이언트가 SETTINGS 프레임보다 다른 프레임을 먼저 받는다면 협상은 실패됨
    • 양측은 GOAWAY 프레임을 전송하며 연결을 종료함

 

3. 프레임

텍스트 구분 형식

  • h1에서 요청/응답 시 사용하는 방식

 

단점
단점 설명
동기 호출
한 번에 하나씩 처리 (요청 후 응답을 받아야 다음 요청 가능)
속도 저하 순차적 요청 처리로 인해 느림
오류 발생 가능
네트워크 지연 또는 장애 시 요청이 실패할 가능성 높음
메모리 사용 예측 어려움
헤더에 사이즈 정보가 없어 얼마나 메모리를 사용할지 예측하기 어려움

 

프레이밍

  • 프로토콜의 사용자가 쉽게 읽고 파싱하고 생성할 수 있는 방식
  • 프레임 단위로 전송

 

장점
장점 설명
파싱이 간단
구조화된 프레임 형식으로 인해 데이터 해석이 쉬움
길이를 알 수 있음 각 프레임의 길이가 명확하게 정의됨
메모리 사용량 예측 가능
프레임 크기를 기반으로 파싱에 필요한 메모리를 미리 계산 가능
다중화 가능
하나의 연결에서 여러 개의 스트림을 동시에 처리할 수 있음
요청과 응답을 뒤섞을 수 있음
여러 개의 요청과 응답을 병렬로 처리하여 효율성 증가
이전 응답과 관계없이 요청 가능
특정 요청의 응답을 기다릴 필요 없이 새로운 요청 전송 가능

 

 

프레임 구조
필드 크기 (바이트) 설명
Length 3 프레임의 페이로드 길이
Type 1 프레임의 타입
Flags 1 프레임의 플래그 (프레임 타입에 따라 다름)
Stream Identifier 4 스트림 식별자
Payload 가변적 프레임에 해당하는 실제 데이터 또는 제어 정보 (타입에 따라 달라짐)

 

프레임 유형
프레임 타입 ID 설명
DATA 0x0 데이터 전송을 위한 프레임 (HTTP 요청/응답의 본문 데이터)
HEADERS 0x1 HTTP 헤더 데이터를 전송하기 위한 프레임
PRIORITY 0x2 스트림의 우선순위를 설정하는 프레임
RST_STREAM 0x3 스트림을 중단시키는 프레임
SETTINGS 0x4 연결 설정 정보를 교환하는 프레임 (예: 윈도우 크기, 버퍼 크기 등)
PUSH_PROMISE 0x5
클라이언트의 요청에 따라 서버가 예비적으로 푸시할 리소스를 알리는 프레임
PING 0x6 연결 상태를 점검하기 위한 프레임
GOAWAY 0x7 연결 종료를 알리는 프레임
WINDOW_UPDATE 0x8
윈도우 크기 조정 (Flow Control) 관련 프레임
CONTINUATION 0x9
긴 헤더 데이터를 여러 프레임에 나누어 전송할 때 사용하는 프레임

 

4. 스트림 

  • HTTP/2.0 연결 내에서 독립적이고 양방향으로 교환되는 프레임 모음
    • 클라이언트는 요청을 할 때 새 스트림을 개시하고, 서버는 동일한 스트림에서 응답함
  • 프레이밍 덕분에 다수의 요청과 응답이 서로를 차단하지 않고 병렬 처리 가능
  • HEADERS 프레임 전송을 통해 새 스트림이 시작됨

 

메시지

  • HTTP 요청이나 응답을 총칭하는 개념 (RFC 7230)
  • 메시지 전송을 위해 스트림을 생성함
    • 요청/응답 메시지의 쌍을 각각 하나의 스트림을 통해 교환
  • 하나의 메시지에는 반드시 먼저 HEADERS 프레임이 전송됨

 

구조
구분 HTTP/1.1 (h1) HTTP/2 (h2)
전송 방식 텍스트 기반 (줄바꿈 사용) 바이너리 프레임 기반
메시지 구조 요청줄/상태줄 + 헤더 + 본문 가상 헤더(HEADERS 프레임) + 본문 (DATA 프레임)
헤더 처리 방식 개별 헤더 필드 전송 HPACK 압축 사용 (중복 제거)
헤더 예시 GET /index.html HTTP/1.1
Host: example.com
:method GET
:scheme https
:path /index.html
전송 효율 헤더 중복 문제, 전송 속도 저하 헤더 압축 및 멀티플렉싱 지원으로 효율적

 

흐름 제어

  • 서버 뿐만 아니라 클라이언트도 전송 크기를 조절할 수 있음
  • WINDOW_UPDATE 프레임에 흐름 제어 정보를 지정함
    • 자신이 수신할 수 있는 바이트 수를 표시함

 

목적
  • 여러 스트림이 서로 방해하지 않게 하기 위해 사용
  • 중재를 위해 사용

 

우선 순위

  • 현대의 브라우저는 웹 페이지에서 가장 중요한 요소를 먼저 요청할 수 있음
  • 최적의 순서로 개체를 가져올 수 있으므로 성능이 개선됨

 

의존성 트리
  • 특정 개체에 의존하는 개체가 있음을 표시하는 자료구조
  • 상대적인 가중치를 정의하여 서버에게 우선순위를 알려줄 수 있음

 

5. 서버 푸시 

  • 특정 개체가 클라이언트에서 사용될 것 같은것을 서버측에서 미리 전달해주는 것

 

개체 푸시하기

  • 서버는 개체를 푸시하기로 결정하면 PUSH_PROMISE 프레임을 구성함

 

조건
조건 설명
스트림 ID 매칭
PUSH_PROMISE 프레임의 스트림 ID는 응답과 쌍을 이루는 요청의 스트림 ID여야 함.
HEADERS 블록 일치
클라이언트가 직접 개체를 요청할 때 사용하는 HEADERS 블록과 동일해야 함.
캐싱 가능성
푸시된 개체는 클라이언트 측에서 캐싱될 수 있어야 함.
멱등성 보장
:method 헤더 필드는 멱등성을 가져야 함 (일반적으로 GET 요청이 사용됨)
전송 순서
PUSH_PROMISE 프레임은 DATA 프레임보다 먼저 도착해야 함.
  • 조건이 충족되지 않으면, 클라이언트는 스트림을 재설정하거나 (RST_STREAM) 에러를 전송함 (GOAWAY)

 

푸시할 대상 선정하기

기준 설명
개체의 캐싱 여부
개체가 이미 브라우저 캐시에 존재할 가능성이 높다면 푸시가 불필요할 수 있음.
개체의 추정 우선순위
더 중요한 리소스를 먼저 푸시하는 것이 바람직함.
푸시로 인한 성능 이점
클라이언트가 직접 요청하는 것보다 푸시하는 것이 성능상 이점이 있는지 평가함.
  • 서버는 클라이언트의 성능 향상을 목표로 푸시할지 결정함

 

6. 헤더 압축(HPACK) 

  • 페이지당 요청/응답수가 많아지면서 네트워크 혼잡도가 높아짐
  • 요청들의 각 헤더들은 상당한 중복을 가짐

 

HPACK 알고리즘 (RFC 7541)

  • 헤더 크기를 줄이기 위해 사용하는 압축 알고리즘

 

압축 방법
압축 방법 설명
인덱스 기반 압축
동일한 헤더가 반복될 경우, 전체 문자열 대신 헤더 테이블의 인덱스만 전송
동적 테이블 활용
새로 추가된 헤더를 동적 테이블에 저장하고, 이후 요청에서 인덱스로 참조
허프만 코딩
헤더 값을 가변 길이의 허프만 코드로 변환 (추가적인 압축 효과)

 

장점
장점 설명
헤더 크기 감소
반복되는 헤더를 인덱스로 전송하여 데이터 크기를 최소화.
전송 속도 향상
압축을 통해 네트워크 대역폭 절약 및 응답 속도 증가.
효율적인 리소스 사용
동적 테이블을 사용하여 중복 헤더 전송을 줄이고, LRU 방식으로 관리.
보안성 향상
헤더 크기를 줄여 특정 공격(예: BREACH 공격) 가능성을 낮춤.

 

전송 과정

 

1. 클라이언트가 첫 번째 요청을 보냄

:method: GET  
:scheme: https  
:authority: example.com  
:user-agent: Mozilla/5.0  
:accept-language: en-US

 

  • 서버는 받은 헤더를 동적 테이블에 저장
  • 이후 요청에서 재사용할 수 있도록 준비함

 

2. 두 번째 요청에서 중복된 헤더는 인덱스로 전송, 변경된 값만 새롭게 전송

:method: GET  
:scheme: https  
:authority: example.com  
:user-agent: Mozilla/5.0  
:accept-language: fr-FR  ← 값이 변경됨
  • 동일한 헤더는 테이블의 인덱스를 전송하여 크기를 최소화
  • 변경된 헤더는 헤더 이름의 인덱스 + 새로운 값만 전송

 

3. 서버가 변경된 값만 반영하고 응답을 생성함

 

7. 전송 절차

nghttp -v -n --no-dep -w 14 -a -H "Header1: Foo" https://nghttp2.org

 

0. 연결

[  0.063] Connected
The negotiated protocol: h2  # h2 협상 성공
[  0.106] send SETTINGS frame <length=12, flags=0x00, stream_id=0> # SETTINGS 프레임 전송
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):16383] # 윈도우 크기를 16KB로 지정

 

1. 요청

[  0.106] send HEADERS frame <length=42, flags=0x05, stream_id=1> 
          ; END_STREAM | END_HEADERS  # 서버에 더이상 전송할 헤더 및 데이터 없음을 알림
          (padlen=0)
          ; Open new stream
          :method: GET
          :path: /
          :scheme: https
          :authority: nghttp2.org
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.44.0
          header1: Foo   # 임의로 추가한 헤더

 

2. 응답

[  0.146] recv SETTINGS frame <length=30, flags=0x00, stream_id=0> # 서버의 SETTINGS 프레임 수신
          (niv=5)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):1048576]
          [UNKNOWN(0x09):1]
          [SETTINGS_ENABLE_CONNECT_PROTOCOL(0x08):1]
          [SETTINGS_HEADER_TABLE_SIZE(0x01):8192]
[  0.146] recv SETTINGS frame <length=0, flags=0x01, stream_id=0> # SETTINGS 프레임에 대한 ACK 송신
          ; ACK
          (niv=0)
[  0.181] recv (stream_id=1) :status: 200 # 응답 코드
[  0.181] recv (stream_id=1) date: Mon, 31 Mar 2025 10:32:31 GMT
[  0.181] recv (stream_id=1) content-type: text/html
[  0.181] recv (stream_id=1) last-modified: Sun, 02 Mar 2025 07:54:08 GMT
[  0.181] recv (stream_id=1) etag: "67c40ea0-18b4"
[  0.181] recv (stream_id=1) accept-ranges: bytes
[  0.181] recv (stream_id=1) content-length: 6324
[  0.181] recv (stream_id=1) x-backend-header-rtt: 0.002
[  0.181] recv (stream_id=1) strict-transport-security: max-age=31536000
[  0.181] recv (stream_id=1) server: nghttpx
[  0.181] recv (stream_id=1) alt-svc: h3=":443"; ma=3600, h3-29=":443"; ma=3600
[  0.181] recv (stream_id=1) via: 2 nghttpx
[  0.181] recv (stream_id=1) x-frame-options: SAMEORIGIN
[  0.181] recv (stream_id=1) x-xss-protection: 1; mode=block
[  0.181] recv (stream_id=1) x-content-type-options: nosniff
[  0.181] recv HEADERS frame <length=251, flags=0x04, stream_id=1> # 전송할 데이터가 있음을 의미 (END_STREAM X)
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  0.181] recv DATA frame <length=6324, flags=0x01, stream_id=1> # 데이터 전송
          ; END_STREAM

 

3. 종료

[  0.273] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=2, error_code=NO_ERROR(0x00), opaque_data(0)=[])