Http/2를 알아보자.

조정환조정환
5 min read

HTTP/2는 단순히 “더 빠른 HTTP”가 아닙니다.

HTTP/1.1의 한계를 극복하기 위해 근본적으로 재설계된 HTTP/2는 웹과 API 설계, 그리고 마이크로서비스 아키텍처(MSA) 환경에서 핵심 역할을 담당합니다.

이 글에서는 HTTP/2의 주요 기능—멀티플렉싱, 헤더 압축(HPACK), 서버 푸시, 요청 우선순위—와 이들 기능이 실제 프로토콜 동작에서 어떻게 구현되는지 구체적인 예시와 함께 살펴봅니다.

또한, gRPC가 왜 HTTP/2 위에서 동작하는지도 알아봅니다.


1. HTTP/1.1의 한계와 HTTP/2 도입 배경

HTTP/1.1의 문제점

  • 직렬 처리: 하나의 TCP 커넥션에서 한 번에 하나의 요청/응답만 처리할 수 있음

  • 헤드 오브 라인 블로킹(Head-of-Line Blocking): 한 커넥션 내에서 하나의 지연이 전체 순서를 막음

  • 커넥션 과다 사용: 브라우저는 병렬성을 위해 도메인당 최대 6개 정도의 커넥션을 동시에 사용

  • 중복 헤더 전송: 각 요청마다 비슷한 헤더가 반복되어 전송되어 대역폭 낭비 발생

Reflective Question:

왜 브라우저는 6개의 TCP 커넥션만 사용하려 할까요?
만약 수백 개의 요청을 처리해야 한다면 어떤 문제가 발생할지 생각해봅니다.

HTTP/2의 주요 목표

HTTP/2는 위의 문제를 해결하기 위해 설계되었습니다. 핵심 목표는 다음과 같습니다.

  • 멀티플렉싱: 하나의 TCP 커넥션 위에서 동시에 여러 요청과 응답을 주고받을 수 있도록 함

  • 헤더 압축: HPACK 알고리즘을 통해 중복 헤더 전송 오버헤드를 줄임

  • 서버 푸시: 클라이언트가 아직 요청하지 않은 리소스를 서버가 미리 전송할 수 있도록 함

  • 요청 우선순위: 각 요청에 우선순위 정보를 부여해 중요한 리소스가 먼저 전달되도록 함


2. HTTP/2 멀티플렉싱: 프로토콜 동작 예시

HTTP/2의 가장 큰 혁신은 멀티플렉싱입니다.
HTTP 메시지는 프레임(frame) 단위로 분할되고, 각 프레임에는 스트림 ID가 할당되어 여러 메시지가 같은 TCP 커넥션 내에서 동시에 interleave(교차 전송)됩니다.

실제 동작 예시

예를 들어, 브라우저가 /index.html, /style.css, /logo.png를 요청한다고 가정해봅시다.

  1. 클라이언트 요청

    • Stream 1: HEADERS 프레임 – GET /index.html

    • Stream 3: HEADERS 프레임 – GET /style.css

    • Stream 5: HEADERS 프레임 – GET /logo.png

  2. 서버 응답 (프레임 단위 전송)
    TCP 커넥션 위에서 프레임이 아래와 같이 전송될 수 있습니다.

     [Stream 1: HEADERS]  → GET /index.html 요청
     [Stream 3: HEADERS]  → GET /style.css 요청
     [Stream 5: HEADERS]  → GET /logo.png 요청
     [Stream 1: DATA]     → /index.html 응답의 일부
     [Stream 3: DATA]     → /style.css 응답의 일부
     [Stream 5: DATA]     → /logo.png 응답의 일부
     [Stream 1: DATA]     → /index.html 응답의 나머지
     ...
    

이와 같이 프레임 단위로 interleave되기 때문에, 한 스트림(예: /style.css)의 전송이 지연되더라도 다른 스트림(예: /logo.png)의 전송은 영향을 받지 않습니다.

Reflective Question:

HTTP/1.1과 비교했을 때, HTTP/2의 멀티플렉싱은
왜 하나의 TCP 커넥션 내에서 여러 요청을 동시에 처리할 수 있게 해줄까요?


3. HPACK: HTTP/2 헤더 압축 메커니즘

HTTP/2는 매 요청마다 반복되는 헤더 정보를 효율적으로 압축하기 위해 HPACK 알고리즘을 도입했습니다.

HPACK 동작 원리

  • 정적 테이블: 자주 사용되는 헤더 필드(예: :method, :path, :scheme)를 미리 정의하여 인덱스 번호로 매핑

  • 동적 테이블: 클라이언트와 서버가 공유하는 캐시처럼 동작하며, 한 번 전송된 헤더를 저장하여 다음 요청부터는 인덱스 번호만 사용

예시

  1. 첫 번째 요청에서 클라이언트가 보내는 헤더:

     :method: GET
     :path: /index.html
     :scheme: https
     user-agent: Mozilla/5.0 (예시)
    
    • 이 때, 헤더 전체가 압축되어 전송됩니다.
  2. 이후 동일한 user-agent 헤더가 다시 사용될 경우:

    • 클라이언트는 동적 테이블에 이미 저장된 인덱스를 참조하여 해당 헤더를 인덱스 번호로만 전송합니다.

    • 결과적으로 전송되는 데이터 크기가 현저히 줄어듭니다.

Reflective Question:

HTTP 요청마다 반복되는 헤더 전송의 오버헤드는 얼마나 큰 문제일까요?
HPACK은 이 문제를 어떻게 해결할 수 있을지 생각해보세요.


4. 서버 푸시(Server Push): 미리 전송하는 리소스

HTTP/2에서는 서버가 클라이언트 요청 없이도 리소스를 미리 전송할 수 있습니다. 이를 서버 푸시라고 합니다.

동작 예시

  1. 클라이언트 요청:
    클라이언트가 /index.html을 요청합니다.

  2. 서버 응답 준비:
    서버는 /index.html에 대한 응답을 준비하는 동시에, HTML 내에 포함된 /style.css/main.js 같은 리소스를 미리 전송할 필요가 있다고 판단합니다.

  3. PUSH_PROMISE 프레임 전송:
    서버는 현재 Stream(예: Stream 1)에서 PUSH_PROMISE 프레임을 보내며, “style.css를 곧 보내겠다”는 약속을 합니다.

     [Stream 1: PUSH_PROMISE] → "style.css will be pushed on Stream 2"
    
  4. 실제 푸시 응답 전송:
    이후 서버는 새로운 Stream(예: Stream 2)에서 실제로 /style.css의 HEADERS와 DATA 프레임을 전송합니다.

     [Stream 2: HEADERS] → style.css 응답 헤더
     [Stream 2: DATA]    → style.css 파일 데이터
    
  5. 클라이언트 처리:
    클라이언트는 PUSH_PROMISE 프레임을 받고, 리소스가 이미 캐시에 존재하면 RST_STREAM으로 해당 푸시 스트림을 거부할 수 있습니다.

Reflective Question:

서버 푸시는 예측이 맞을 경우 큰 성능 이점을 주지만,
클라이언트가 이미 해당 리소스를 가지고 있을 경우 어떤 문제가 발생할 수 있을까요?


5. HTTP/2 요청 우선순위: 스트림 간 작업 순서 최적화

HTTP/2는 각 요청(Stream)에 대해 우선순위 정보를 추가할 수 있습니다.
이 정보는 가중치(Weight, 1~256)의존성(Dependency)으로 구성되어, 클라이언트는 중요 리소스를 먼저 받도록 서버에 신호를 보낼 수 있습니다.

동작 예시

예를 들어, 브라우저가 다음과 같이 요청했다고 가정해봅니다:

  • Stream 1: /index.html (기본 요청)

  • Stream 3: /style.css (렌더링에 필수 → 높은 가중치, 독립 요청)

  • Stream 5: /analytics.js (낮은 우선순위)

우선순위 정보가 포함된 HEADERS 프레임은 다음과 같이 구성될 수 있습니다:

Stream 3 HEADERS: priority -> { dependency: 0, weight: 256 }
Stream 5 HEADERS: priority -> { dependency: 1, weight: 16 }

서버는 이 정보를 참고하여 중요한 스트림(예: /style.css)의 프레임을 우선 전송하도록 대역폭을 분배할 수 있습니다.

Reflective Question:

만약 서버가 클라이언트가 보낸 우선순위 정보를 무시한다면, 이 기능은 어떤 의미를 가질 수 있을까요?


6. gRPC와 HTTP/2: 고성능 RPC를 위한 이상적 조합

gRPC는 HTTP/2의 멀티플렉싱, 스트리밍, 헤더 압축 등의 기능을 활용하여, 고성능 원격 프로시저 호출(RPC)을 가능하게 합니다.

gRPC 동작 예시

  1. 클라이언트 호출:
    클라이언트는 gRPC 클라이언트 라이브러리를 통해 “함수 호출” 형식으로 서버의 메서드를 호출합니다.
    이 호출은 내부적으로 HTTP/2의 새로운 Stream에서 Unary(단일 요청-응답) 또는 스트리밍 방식으로 전송됩니다.

  2. HTTP/2 프레임 교환:

    • 클라이언트는 호출에 필요한 메타데이터와 페이로드를 포함한 HEADERS 및 DATA 프레임을 보냅니다.

    • 서버는 해당 Stream에서 응답 HEADERS와 DATA 프레임을 통해 결과를 반환합니다.

    [Stream 1: HEADERS] → gRPC 호출: "GetUserData" (메서드 이름, 메타데이터)
    [Stream 1: DATA]    → 요청 페이로드 (Protocol Buffers로 인코딩)
    ...
    [Stream 1: HEADERS] → 응답 헤더 (상태, 메타데이터)
    [Stream 1: DATA]    → 응답 페이로드 (Protocol Buffers로 인코딩된 결과)
  1. 양방향 스트리밍 지원:
    gRPC는 HTTP/2의 양방향 스트리밍 기능을 활용하여, 클라이언트와 서버가 동시에 데이터를 주고받을 수 있습니다.
    이는 실시간 데이터 교환이나 채팅, 라이브 업데이트 등에 매우 유용합니다.

중요: 정식 gRPC는 반드시 HTTP/2 위에서 동작합니다. (브라우저 호환을 위한 gRPC-Web은 예외적입니다.)

Reflective Question:

gRPC가 HTTP/1.1이 아닌 HTTP/2를 선택한 이유는 무엇일까요?
어떤 HTTP/2 기능이 gRPC의 성능과 기능적 요구를 충족시킬 수 있었을까요?


결론

HTTP/2는 단순한 프로토콜 버전업 이상의 의미를 지닙니다.
멀티플렉싱, HPACK, 서버 푸시, 우선순위 기능 등을 통해 HTTP/1.1의 한계를 극복하고, 보다 효율적인 데이터 전송과 낮은 지연시간을 구현했습니다.
더 나아가, gRPC와 같은 최신 RPC 프레임워크는 이러한 HTTP/2의 강점을 활용하여 고성능 마이크로서비스 통신을 가능하게 합니다.

이 글을 통해 HTTP/2의 내부 동작과 실제 프로토콜 예시를 자세히 이해하셨다면, 이제 여러분은 단순한 사용자 수준을 넘어, 프로토콜 설계의 이면을 이해하는 개발자로 거듭날 수 있을 것입니다.

0
Subscribe to my newsletter

Read articles from 조정환 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

조정환
조정환