본문으로 건너뛰기
  1. Posts/

[Network] HTTP, gRPC, 멀티플렉싱 정리 — 무상태성, HTTP/2, Java NIO

NineKoo9
작성자
NineKoo9
목차

이 문서는 의문을 가지고 있던 지점을 AI 에이전트와 대화하며 정리한 메모입니다. HTTP의 무상태성과 연결 재사용, gRPC의 HTTP/2 사용 이유, HTTP/2 멀티플렉싱과 Java NIO 멀티플렉싱의 차이를 함께 정리합니다.

HTTP / gRPC / 멀티플렉싱 정리
#


1. HTTP 무상태성과 TCP 연결 관리의 관계
#

HTTP의 무상태성이란?
#

RFC 기준으로 HTTP의 핵심 성질은 무상태성(statelessness) 이다.
즉, 각 요청은 이전 요청과 독립적으로 이해될 수 있어야 하며, 서버가 이전 요청의 문맥을 반드시 기억할 필요는 없다.

이 말은 TCP 연결을 매번 끊는다는 뜻이 아니다.
HTTP의 무상태성은 애플리케이션 의미론이고, TCP 연결 재사용 여부는 연결 관리(connection management) 문제다.


TCP 연결 생명주기 — HTTP 버전별
#

HTTP/1.0 (초기)
#

클라이언트                서버
    |-- TCP 3-way handshake -->|
    |-- HTTP 요청 ------------>|
    |<-- HTTP 응답 ------------|
    |-- TCP 종료 (4-way) ----->|   ← 매 요청마다 연결/해제

요청/응답 1쌍마다 TCP를 새로 맺고 끊었다. 비효율적.


HTTP/1.1 — Persistent Connection이 기본값
#

클라이언트                서버
    |-- TCP 3-way handshake -->|
    |-- 요청 1 --------------->|
    |<-- 응답 1 ---------------|
    |-- 요청 2 --------------->|  ← 같은 TCP 연결 재사용
    |<-- 응답 2 ---------------|
    |-- 요청 3 --------------->|
    |<-- 응답 3 ---------------|
    |-- TCP 종료 ----------->  |  ← 일정 시간 후 또는 명시적 종료

HTTP/1.1부터는 persistent connection 이 기본값이다.
별도로 Connection: close를 보내지 않으면 TCP 연결을 유지한다.


HTTP/2, HTTP/3
#

  • HTTP/2: 하나의 TCP 연결에서 멀티플렉싱 (여러 요청을 병렬로)
  • HTTP/3: TCP 대신 QUIC(UDP 기반)을 사용, 연결 오버헤드 더 줄임

버전별 비교
#

HTTP/1.0HTTP/1.1HTTP/2
기본 동작요청마다 TCP 재생성TCP 재사용 (keep-alive 기본)하나의 TCP로 다중 요청
Keep-Alive옵션 (헤더 명시 필요)기본값별도 Connection: keep-alive 개념 아님, 지속 연결이 기본

핵심 포인트
#

  • 무상태성 = 서버가 각 요청을 독립적으로 처리한다 (stateless)
  • Keep-Alive = TCP 물리 연결을 재사용한다 (persistent connection)
  • 둘은 다른 차원의 개념이다. 무상태성(HTTP 의미론)과 Keep-Alive(TCP 연결 관리)는 서로 독립적이다.

2. gRPC의 통신 프로토콜
#

레이어 구조
#

┌─────────────────────────────┐
│        gRPC (애플리케이션)    │  ← 서비스/메서드 정의 (proto)
├─────────────────────────────┤
│        HTTP/2               │  ← gRPC를 실어 나르는 애플리케이션 프로토콜
├─────────────────────────────┤
│        TLS (선택적, 보통 권장) │  ← 암호화
├─────────────────────────────┤
│        TCP                  │  ← 연결
└─────────────────────────────┘

표준 gRPC는 HTTP/2 위에 매핑되는 RPC(Remote Procedure Call) 프레임워크다.
TLS는 실무에서 매우 흔하지만, gRPC 자체가 TLS를 강제하는 것은 아니다.


gRPC가 HTTP/2를 쓰는 이유
#

① 멀티플렉싱

하나의 TCP 연결에서:
Stream 1: 요청A ─────────────► 응답A
Stream 3: 요청B ──────────────────────► 응답B
Stream 5: 요청C ───────────────────────────► 응답C

여러 RPC 호출이 동시에 하나의 연결을 공유한다.

참고로 클라이언트 라이브러리의 channel 은 구현에 따라 여러 HTTP/2 연결을 관리할 수 있다.
다만 개별 RPC 호출은 HTTP/2 stream 으로 전달된다고 이해하면 된다.

② 스트리밍 지원
HTTP/2의 스트림 개념 덕분에 gRPC만의 4가지 통신 패턴이 가능하다.


gRPC의 4가지 통신 패턴
#

1. Unary (일반 요청/응답)
#

클라이언트 ──요청 1개──► 서버
           ◄─응답 1개──

형태상 가장 익숙한 요청/응답 패턴이다.


2. Server Streaming
#

클라이언트 ──요청 1개──► 서버
           ◄─응답 1──
           ◄─응답 2──
           ◄─응답 3──
           ◄─END──

예: 실시간 주가, 로그 스트리밍


3. Client Streaming
#

클라이언트 ──요청 1──► 서버
           ──요청 2──►
           ──요청 3──►
           ◄─응답 1개──

예: 파일 업로드, 센서 데이터 배치 전송


4. Bidirectional Streaming
#

클라이언트 ──요청 1──► 서버
           ◄─응답 1──
           ──요청 2──►
           ◄─응답 2──
           (양방향 자유롭게)

예: 채팅, 실시간 게임, 자율주행 텔레메트리


데이터 직렬화: Protocol Buffers
#

실무에서 REST가 JSON과 함께 자주 쓰이듯, gRPC는 기본적으로 Protobuf를 사용한다.
다만 REST가 JSON에만 묶이지 않듯, gRPC도 다른 데이터 형식으로 확장될 수 있다.

service TelemetryService {
  rpc SendData (stream TelemetryData) returns (Ack);
}

message TelemetryData {
  float speed = 1;
  float latitude = 2;
  float longitude = 3;
}

JSON 대비 일반적인 장점:

  • 크기 가 더 작을 가능성이 큼 (바이너리)
  • 파싱/직렬화 성능이 더 유리한 경우가 많음
  • 스키마 기반이라 타입 안정성 과 코드 생성에 유리

REST vs gRPC 비교
#

주의: REST는 프로토콜 이름이 아니라 아키텍처 스타일이다. 아래 표는 엄밀한 정의 비교라기보다, 실무에서 자주 만나는 조합을 비교한 것이다.

RESTgRPC
프로토콜HTTP 의미론 위 (HTTP/1.1, HTTP/2, HTTP/3 등)표준 gRPC는 HTTP/2
데이터 형식JSON이 흔함Protobuf가 기본값
통신 패턴요청/응답 중심4가지 패턴
인터페이스 정의OpenAPI 등 선택.proto 사용이 일반적
브라우저 지원넓음원본 gRPC는 제한적, 보통 grpc-web 사용
사용처공개 API, 범용 연동서비스 간 통신, 저지연/스트리밍

3. HTTP/2 멀티플렉싱 상세
#

HTTP/1.1의 문제: HOL Blocking
#

HTTP/1.1에서는 같은 연결 위에서 응답을 요청 순서에 맞춰 돌려줘야 하므로, 앞선 요청이 지연되면 뒤 요청도 지연되기 쉽다.
과거의 pipelining 도 이 문제를 근본적으로 해결하지 못했다.

TCP 연결 1: 요청A ──► [응답A 기다리는 중...] ──► 요청B ──► 요청C

요청A 응답이 늦으면 B, C가 전부 블로킹된다. 이게 HOL Blocking이다.

브라우저는 이를 우회하기 위해 보통 여러 개의 병렬 TCP 연결을 열어 완화해 왔다.


HTTP/2의 핵심 개념 3가지
#

1. Stream (스트림)
#

하나의 TCP 연결 안에 생기는 독립적인 가상 채널.

  • 각 스트림은 고유한 Stream ID를 가진다 (홀수: 클라이언트 시작, 짝수: 서버 시작)
  • 동시에 여러 스트림이 존재할 수 있다

2. Frame (프레임)
#

HTTP/2의 최소 전송 단위. 모든 데이터는 프레임으로 쪼개져서 전송된다.

┌──────────────────────────────────────┐
│  Length (3B) │ Type (1B) │ Flags (1B)│
├──────────────────────────────────────┤
│         Stream ID (4B)               │
├──────────────────────────────────────┤
│         Payload ...                  │
└──────────────────────────────────────┘

주요 프레임 타입:

  • HEADERS : HTTP 헤더 전송
  • DATA : 실제 body 데이터
  • SETTINGS : 연결 설정
  • WINDOW_UPDATE : 흐름 제어

3. Message (메시지)
#

하나의 HTTP 요청 또는 응답. 여러 프레임의 묶음으로 표현된다.

요청 한 건 = HEADERS 프레임 + DATA 프레임(들) + 필요 시 trailing HEADERS

멀티플렉싱 동작 원리
#

하나의 TCP 연결에서 프레임들이 뒤섞여서 전송되고, Stream ID로 어느 요청의 프레임인지 구분한다.

시간 →
──[S1:HEADERS]──[S3:HEADERS]──[S1:DATA]──[S5:HEADERS]──[S3:DATA]──[S1:DATA]──►
     ↑               ↑            ↑            ↑
  요청A 시작       요청B 시작    요청A 본문    요청C 시작

수신 측은 Stream ID를 보고 각 요청으로 재조립한다.


HOL Blocking 해결
#

HTTP/1.1:
요청A [==============느린 응답==============] 요청B는 대기...

HTTP/2:
Stream1(요청A): [==============느린 응답==============]
Stream3(요청B): [==빠른 응답==]  ← 요청A 안 기다림!
Stream5(요청C): [====응답====]   ← 독립적으로 처리

추가 기능
#

우선순위 신호: 과거에는 스트림마다 가중치와 의존 관계를 보낼 수 있었지만, 이 priority signaling은 최신 HTTP/2 RFC에서 deprecated 되었다.

흐름 제어 (Flow Control): 스트림 단위뿐 아니라 연결 전체 단위로도 수신 윈도우를 조절한다. WINDOW_UPDATE 프레임으로 제어한다.


한계: TCP 레벨 HOL Blocking은 여전히 존재
#

TCP 패킷 손실 발생 시:
패킷 1 (Stream1 DATA) ✓
패킷 2 (Stream3 DATA) ✗ ← 손실!
패킷 3 (Stream5 DATA) ✓ ← 재전송 대기... 모든 스트림 블로킹
  • HTTP/2는 HTTP 레이어의 HOL Blocking은 해결
  • TCP 패킷 손실 시 전체 스트림이 멈추는 문제는 여전히 존재
  • 이걸 해결한 게 HTTP/3 + QUIC(UDP 기반)

4. HTTP/2 멀티플렉싱 vs Java NIO I/O 멀티플렉싱
#

공통된 철학
#

“하나의 주체가 여러 채널을 동시에 처리한다”

이름은 같지만 다른 레이어의 개념이다.


비교
#

HTTP/2 멀티플렉싱Java NIO I/O 멀티플렉싱
레이어애플리케이션 레이어 (HTTP/2가 TCP 위에서 동작)OS/런타임 레이어
다중화 대상하나의 TCP 연결 안의 스트림들여러 TCP 연결(소켓)들
핵심 문제여러 요청을 하나의 연결로 처리여러 연결을 하나의 스레드로 처리

Java NIO I/O 멀티플렉싱
#

기존 방식 (Blocking I/O)
#

스레드1 ──────► 소켓A (연결 기다리는 중... blocked)
스레드2 ──────► 소켓B (데이터 기다리는 중... blocked)
스레드3 ──────► 소켓C (데이터 기다리는 중... blocked)

연결 1개당 스레드 1개. 연결 1만 개면 스레드 1만 개 → 메모리/컨텍스트 스위칭 폭발

NIO 방식
#

                  ┌── 소켓A (이벤트 있을 때만 알림)
Selector(1개) ────┼── 소켓B (이벤트 있을 때만 알림)
스레드 1개        └── 소켓C (이벤트 있을 때만 알림)

SelectorSelectableChannel 을 다루는 Java의 멀티플렉서다. 내부적으로는 플랫폼별 I/O 멀티플렉싱 메커니즘(select, poll, epoll, kqueue 등)에 기대어 동작한다.
소켓들을 등록해두면, 이벤트가 생긴 소켓만 골라서 알려준다.

Selector selector = Selector.open();

socketA.register(selector, SelectionKey.OP_READ);
socketB.register(selector, SelectionKey.OP_READ);
socketC.register(selector, SelectionKey.OP_READ);

while (true) {
    selector.select(); // 이벤트 있는 소켓이 생길 때까지 대기

    for (SelectionKey key : selector.selectedKeys()) {
        if (key.isReadable()) {
            SocketChannel ch = (SocketChannel) key.channel();
            ch.read(buffer);
        }
    }
}

스레드 1개로 수천 개의 소켓을 처리할 수 있다.


두 개념의 위치 — 구체적인 시나리오
#

클라이언트 3명이 서버에 접속해서 각각 여러 요청을 보내는 상황:

① TCP 연결(소켓)이 3개 생긴다

클라이언트1 ──────── 소켓A ──┐
클라이언트2 ──────── 소켓B ──┤── 서버
클라이언트3 ──────── 소켓C ──┘

② NIO 멀티플렉싱 = 소켓 3개를 감시하는 문제

소켓A에 데이터 왔나?
소켓B에 데이터 왔나?  ← 이걸 스레드 1개로 감시
소켓C에 데이터 왔나?

NIO 멀티플렉싱의 관심사 = 소켓(연결) 여러 개를 어떻게 효율적으로 감시하나

③ HTTP/2 멀티플렉싱 = 각 소켓 안의 문제

소켓A (클라이언트1과의 TCP 연결) 안에:
  ├── Stream 1: GET /user
  ├── Stream 3: GET /posts
  └── Stream 5: GET /images

HTTP/2 멀티플렉싱의 관심사 = 하나의 소켓 안에서 요청 여러 개를 어떻게 동시에 처리하나

④ 전체 그림 (실제 프레임워크에서 두 개념이 함께 동작)

서버

Selector (스레드 1개로 소켓 3개 감시)  ← NIO 멀티플렉싱
  ├── 소켓A 안 → Stream1, Stream3, Stream5  ← HTTP/2 멀티플렉싱
  ├── 소켓B 안 → Stream1, Stream3
  └── 소켓C 안 → Stream1

한 줄 정리
#

대상역할
NIO 멀티플렉싱소켓(연결) 여러 개스레드 1개로 여러 소켓을 바깥에서 감시
HTTP/2 멀티플렉싱스트림(요청) 여러 개하나의 소켓 안을 여러 레인으로 나눔

Netty, Spring WebFlux 같은 프레임워크는 이 두 개념을 함께 활용한다.

참고 자료
#