본문으로 건너뛰기
yutils

gRPC 는 어떻게 동작할까?

gRPC 의 동작 원리 — HTTP/2 multiplexing, protobuf wire format, 4 가지 RPC 모드(unary·server·client·bidirectional streaming), 코드 생성, 내부 서비스에서 REST 를 이기는 이유, 그리고 브라우저는 왜 여전히 gRPC-Web 이 필요한가.

약 9분 읽기

REST + JSON 은 "사람이 읽기 편하다" 가 최대 강점. 그러나 내부 microservice 통신 — 사람이 읽을 일 없고 latency · throughput 이 중요한 환경 — 에서는 JSON parse · text format · HTTP/1.1 의 head-of-line blocking 이 비용. gRPC 는 Google 이 2015 년에 공개한 RPC 프레임워크로, protobuf 의 binary wire format + HTTP/2 의 multiplexed stream + 코드 생성으로 이 비용을 없앤다. 이 가이드는 gRPC 가 실제로 동작하는 방식, 4 가지 RPC 모드, REST 대비 강점·약점, 그리고 브라우저에서 직접 못 쓰는 이유를 정리한다.

전체 그림

.proto 파일 (스키마)
       │
       │  protoc + plugin
       ▼
   client stub          server skeleton
   (Java/Go/Py/…)       (Java/Go/Py/…)
       │                     ▲
       │ method call         │ method impl
       ▼                     │
   ┌─────────────────────────────┐
   │   gRPC runtime              │
   │ ┌─────────────────────────┐ │
   │ │ protobuf encode/decode  │ │
   │ ├─────────────────────────┤ │
   │ │ HTTP/2 frames           │ │  ← multiplexed streams
   │ ├─────────────────────────┤ │
   │ │ TCP + TLS               │ │
   │ └─────────────────────────┘ │
   └─────────────────────────────┘

핵심: 개발자는 .proto 하나 정의 → 양쪽 언어로 stub 생성 → 그 stub 의
메서드 호출은 "그냥 함수처럼" 보이는 RPC.

.proto 와 코드 생성

// user.proto
syntax = "proto3";

package myapp;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);     // server stream
  rpc UpdateProfile (stream ProfilePatch) returns (User);     // client stream
  rpc Chat (stream Message) returns (stream Message);         // bidi
}

message GetUserRequest { int64 id = 1; }
message User { int64 id = 1; string name = 2; string email = 3; }

// 컴파일
protoc --go_out=. --go-grpc_out=. user.proto
protoc --python_out=. --grpc_python_out=. user.proto

// Go 의 client 사용
client := pb.NewUserServiceClient(conn)
user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 42})
// → 일반 함수처럼 보이지만 내부에선 protobuf 인코딩 + HTTP/2 호출

Protobuf — 왜 JSON 보다 빠른가

JSON:
  {"id":42,"name":"jade","email":"x@y.com"}
  → 38 bytes, parse 비용 큼 (string → number, key matching)

Protobuf (wire format):
  08 2a 12 04 6a 61 64 65 1a 07 78 40 79 2e 63 6f 6d
   │  │  │  │       jade      │       x@y.com
   │  │  │  │                 field 3, length-delimited (7 bytes)
   │  │  field 2, length-delimited (4 bytes)
   │  varint(42) = id 값
   field 1, varint (tag = 1<<3 | 0)

총 17 bytes. 거의 절반.

장점:
- 작음 (네트워크 비용 ↓)
- 빠름 (field number 로 직접 매핑, key 매칭 X)
- schema 강제 (런타임 typo 0)

단점:
- 사람이 못 읽음 — 디버깅 시 grpcurl / proto reflection 필요
- schema 없이는 의미 없음 — 옛 binary log 파싱 hard

HTTP/2 — gRPC 의 두 번째 바닥

HTTP/1.1:
  한 TCP 연결 = 한 번에 한 요청 (head-of-line blocking)
  요청 100 개 = 연결 100 개 또는 직렬 100 회

HTTP/2:
  한 TCP 연결에 multiplexed stream — 100 요청 동시 가능
  binary framing — 텍스트 parse 없음
  header compression (HPACK) — 같은 헤더 반복 비용 ↓
  server push (사용 빈도 낮음)

gRPC 매핑:
  1 RPC = 1 HTTP/2 stream
  request / response = HEADERS frame + DATA frame들 + 끝맺음 trailer

  → 한 연결로 수천 RPC 동시 처리. 연결 setup 비용 (TLS handshake) 도
    한 번만.

cf. REST + HTTP/1.1: 매 요청마다 RTT, keep-alive 로 완화하지만
    multiplexing 은 안 됨. HTTP/2 도입한 REST 도 이점 일부 흡수.

4 가지 RPC 모드

# 1. Unary — 가장 흔함, REST 의 한 호출과 동등
  rpc GetUser (Req) returns (Resp);
  client → 1 request, server → 1 response.

# 2. Server Streaming
  rpc ListUsers (Req) returns (stream User);
  client → 1 request, server → N response (한 stream).
  사례: 큰 결과 셋, 진행률 갱신, server-side push.

# 3. Client Streaming
  rpc Upload (stream Chunk) returns (UploadResult);
  client → N request (한 stream), server → 1 response (끝에).
  사례: 파일 업로드, sensor data 수집.

# 4. Bidirectional Streaming
  rpc Chat (stream Msg) returns (stream Msg);
  client ↔ server 양방향 독립 stream.
  사례: 채팅, 실시간 게임, collaborative 편집.

→ REST + HTTP 만으로 streaming 하려면 SSE / WebSocket / long-poll 등
  별도 메커니즘 필요. gRPC 는 4 모드 모두 같은 framework 안.

Deadline · Cancellation · Metadata

# Deadline (timeout)
  ctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
  client.GetUser(ctx, ...)
  → 200ms 안에 응답 못 받으면 자동 cancel.
  → 전파 (propagation): 그 서버가 또 다른 gRPC 호출하면 deadline
    상속 → cascade timeout 자연 처리.

# Cancellation
  client 가 cancel → 서버 stream 도 cancel 신호 받음.
  → 불필요 작업 즉시 중단 (장시간 query, big response).

# Metadata (헤더에 해당)
  md := metadata.Pairs("authorization", "Bearer …", "trace-id", "abc")
  ctx := metadata.NewOutgoingContext(ctx, md)
  → REST 의 헤더와 동일 역할, key/value 쌍.

# 상태 코드
  gRPC 자체 status code (12 + 1) — HTTP status 와 다름.
  OK / CANCELLED / DEADLINE_EXCEEDED / NOT_FOUND / PERMISSION_DENIED /
  RESOURCE_EXHAUSTED / UNAVAILABLE / INTERNAL …

REST 대비 — 언제 무엇

REST + JSONgRPC + protobuf
payload 크기크다 (text)작다 (binary)
parse 비용크다작다
schema 강제OpenAPI 별도.proto 자체가 schema
streamingSSE / WS 별도네이티브 4 모드
multiplexingHTTP/2 필요HTTP/2 기본
사람 가독성높다 (curl 가능)낮다 (grpcurl 등 필요)
browser 지원네이티브 fetchX (gRPC-Web 필요)
cache 친화HTTP cache 활용cache X (POST 전제)
외부 노출표준 (모두 익숙)드뭄 (보통 gateway 변환)

브라우저의 한계 — gRPC-Web

브라우저 fetch / XHR 는 HTTP/2 의 일부 기능 (trailer header, raw
frame 제어) 에 접근 불가. 그래서 순수 gRPC 를 직접 호출 불가.

해결: gRPC-Web
- 브라우저 ↔ proxy (Envoy / grpc-web-proxy) 사이는 HTTP/1.1 또는
  HTTP/2 의 제한 subset
- proxy ↔ backend 사이는 진짜 gRPC
- streaming 의 일부 (client / bidi) 는 미지원 또는 트릭

→ 그래서 public-facing API 는 보통 REST 또는 GraphQL,
  내부 service-to-service 만 gRPC 가 흔한 패턴.

→ Connect-RPC / Twirp 같은 변형은 브라우저에서 직접 호출 가능하게
  설계 (HTTP/1.1 + JSON 도 지원).

흔한 함정

  • schema breaking change — protobuf field number 는 영구. 절대 재사용 금지. 삭제는 reserved 로 보호. type 변경도 호환성 깨짐 (int32 → int64 OK, int32 → string X).
  • HTTP status 와 gRPC status 혼동 — gRPC 는 전송 성공이면 HTTP 200, 실제 응답의 OK / NOT_FOUND 는 gRPC trailer 의 status code. monitoring 시 둘 다 봐야.
  • load balancer 호환 — HTTP/2 multiplexed connection 은 L4 load balancer 와 안 맞음 (한 connection 이 한 서버 고정 → 부하 unbalance). L7 (Envoy / nginx 1.13+) 필요. 또는 client-side load balancing (xDS).
  • deadline 미설정 — 클라이언트가 deadline 안 박으면 hang 가능. 모든 RPC 에 deadline 강제하는 게 표준 패턴.
  • error 모델 단순함 — gRPC status code 13 개 만으로는 도메인 에러 표현 부족. google.rpc.Status 의 details (Any) 로 구조화 에러 첨부.
  • 외부 노출 시 인증 — gRPC 는 metadata 로 Bearer token 가능. 그러나 IAM / OAuth2 통합은 직접 코드 필요. oauth2-explained 가이드 참조.
  • generated code 의 build 부담 — .proto 변경 시 모든 언어 stub 재생성. monorepo + buf 같은 도구로 관리.

마무리

gRPC 의 강점은 한 줄 — 강한 schema + binary wire + HTTP/2 multiplex + streaming + 다언어 코드 생성. 이 4 가지가 같이 쌓이는 환경 (수많은 내부 서비스, 다언어, 높은 throughput) 에서 REST 대비 압도적.

반대로 외부 public API, 브라우저 직접 호출, curl 로 디버깅 많은 환경에서는 REST + JSON 이 여전히 유리. 실전 패턴 — 내부는 gRPC, edge gateway 에서 REST/GraphQL 로 변환해 외부 노출. Cache·rate-limit 등 HTTP 기반 부가 기능 (cors-explained, rate-limiting-strategies) 은 그 gateway 에서 처리.

가이드 목록으로