TL;DR
- **gRPC = HTTP/2 + Protobuf**: 5-10배 빠르고 작은 RPC
- **4가지 모델**: Unary, Server Streaming, Client Streaming, Bidirectional
- **Protobuf 최적화**: 필드 번호, 변환된 byte, varint 인코딩
- **인터셉터**: 횡단 관심사 (인증, 로깅, 메트릭) 처리
- **Deadline 전파**: 분산 환경에서 타임아웃 누적 방지
- **gRPC-Web**: 브라우저에서 gRPC 사용
1. gRPC가 등장한 배경
1.1 RPC의 역사
**RPC (Remote Procedure Call)** = 다른 머신의 함수를 로컬 함수처럼 호출.
**진화**:
- **1980s**: Sun RPC (NFS의 기반)
- **1990s**: CORBA (복잡, 실패)
- **2000s**: SOAP/WSDL (XML, 느림)
- **2010s**: REST (JSON over HTTP)
- **2015~**: gRPC (Google이 내부에서 사용하던 Stubby 공개)
1.2 REST의 한계
GET /api/users/123 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "name": "Alice", "email": "alice@example.com"}
**문제**:
1. **JSON 비효율**: 텍스트, 키 반복
2. **HTTP/1.1**: head-of-line blocking
3. **느슨한 계약**: 스키마 강제 X
4. **단방향**: 양방향 스트리밍 어려움
5. **클라이언트 코드 작성 부담**
1.3 gRPC의 약속
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc StreamUsers(Empty) returns (stream User);
}
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
→ **자동 코드 생성** (모든 언어), **스키마 강제**, **5-10배 작은 페이로드**, **HTTP/2 멀티플렉싱**, **양방향 스트리밍**.
2. Protocol Buffers 깊이
2.1 기본 구조
syntax = "proto3";
package myapp;
message User {
int64 id = 1;
string name = 2;
string email = 3;
repeated string tags = 4;
Address address = 5;
}
message Address {
string street = 1;
string city = 2;
}
2.2 필드 번호의 의미
message User {
int64 id = 1; // ← 1은 필드 번호
string name = 2;
}
**왜 필드 번호?**:
- **이진 형식**의 식별자
- **이름 변경에도 호환** (번호만 같으면)
- **삭제된 필드는 번호 재사용 X**
// v1
message User {
string user_name = 1;
}
// v2 (호환됨)
message User {
string display_name = 1; // 이름 바꿈, 번호 같음
}
2.3 Wire Format
Protobuf의 이진 형식:
┌─────────────┬──────────────┐
│ field_number│ field_value │
│ + type │ │
└─────────────┴──────────────┘
각 필드: `(field_number << 3) | wire_type`.
**Wire Types**:
- `0`: VARINT (int32, int64, bool)
- `1`: FIXED64 (double, fixed64)
- `2`: LENGTH_DELIMITED (string, bytes, message)
- `5`: FIXED32 (float, fixed32)
2.4 Varint 인코딩
작은 수는 작은 byte로 인코딩:
0 → 1 byte
127 → 1 byte
128 → 2 bytes
16383 → 2 bytes
16384 → 3 bytes
**효과**: 작은 ID, 카운터 등이 매우 효율적.
2.5 비교 예시
// JSON: 78 bytes
{"id":123,"name":"Alice","email":"alice@example.com"}
// Protobuf: 35 bytes (45% 작음)
0a 03 31 32 33 # field 1 (id), len 3, "123"
12 05 41 6c 69 63 65 # field 2 (name), len 5, "Alice"
1a 11 ...
**큰 메시지일수록 차이가 큼** (key 반복이 없으므로).
2.6 Schema Evolution
**규칙**:
- ✅ 새 필드 **추가** (다른 번호로)
- ✅ 필드 **이름 변경**
- ✅ optional → required X (proto3는 모두 optional)
- ❌ 필드 **번호 변경**
- ❌ 필드 **타입 변경** (호환 안 됨)
- ❌ 필드 **삭제 후 번호 재사용** (`reserved` 사용)
message User {
reserved 4, 5; // 4, 5는 사용 안 함
reserved "old_field"; // 이름도 reserved
int64 id = 1;
string name = 2;
string email = 3;
}
3. 4가지 통신 모델
3.1 Unary RPC (가장 일반적)
rpc GetUser(GetUserRequest) returns (User);
**동작**: 1 요청 → 1 응답. REST와 비슷.
Client
response = stub.GetUser(GetUserRequest(user_id=123))
print(response.name)
Server
def GetUser(self, request, context):
user = db.get(request.user_id)
return User(id=user.id, name=user.name, email=user.email)
**사용**: CRUD, 일반 API.
3.2 Server Streaming
rpc StreamUsers(Empty) returns (stream User);
**동작**: 1 요청 → N 응답.
Client
for user in stub.StreamUsers(Empty()):
print(user.name)
Server
def StreamUsers(self, request, context):
for user in db.iter_all():
yield User(id=user.id, name=user.name)
**사용**: 큰 결과 셋 (수천 행), 실시간 업데이트, 로그 스트리밍.
3.3 Client Streaming
rpc UploadEvents(stream Event) returns (UploadResponse);
**동작**: N 요청 → 1 응답.
Client
def event_generator():
for event in events:
yield event
response = stub.UploadEvents(event_generator())
print(response.processed)
Server
def UploadEvents(self, request_iterator, context):
count = 0
for event in request_iterator:
process(event)
count += 1
return UploadResponse(processed=count)
**사용**: 대용량 업로드, 배치 처리.
3.4 Bidirectional Streaming
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
**동작**: N 요청 ↔ N 응답. 양쪽이 독립적으로 메시지 보냄.
Client
def send_messages():
yield ChatMessage(text="Hello")
yield ChatMessage(text="How are you?")
for response in stub.Chat(send_messages()):
print(f"Received: {response.text}")
Server
def Chat(self, request_iterator, context):
for msg in request_iterator:
yield ChatMessage(text=f"Echo: {msg.text}")
**사용**: 채팅, 실시간 게임, IoT 양방향 통신.
3.5 비교
| 모델 | 요청 | 응답 | 사용 사례 |
|---|---|---|---|
| **Unary** | 1 | 1 | CRUD |
| **Server Streaming** | 1 | N | 큰 결과, 실시간 업데이트 |
| **Client Streaming** | N | 1 | 업로드, 배치 |
| **Bidirectional** | N | N | 채팅, 게임 |
4. HTTP/2의 역할
4.1 HTTP/1.1의 한계
[Client] ─────────→ [Server]
↓
[Connection 1: Request 1]
[Connection 2: Request 2]
[Connection 3: Request 3]
...
**문제**:
- 각 요청 = 새 연결 (또는 keep-alive but sequential)
- Head-of-line blocking
- 헤더 반복
4.2 HTTP/2 멀티플렉싱
[Client] ─────────→ [Server]
↓
[Single Connection]
├─ Stream 1: Request 1 ↔ Response 1
├─ Stream 2: Request 2 ↔ Response 2
└─ Stream 3: Request 3 ↔ Response 3
**장점**:
- **단일 TCP 연결로 여러 요청 동시**
- 헤더 압축 (HPACK)
- 서버 푸시
- 바이너리 프레이밍
4.3 gRPC의 HTTP/2 활용
- **각 RPC = 1 stream**
- 같은 연결로 수천 RPC 동시
- TCP 연결 오버헤드 최소화
- **양방향 스트리밍** 자연스럽게 가능
4.4 헤더 압축
HTTP/1.1:
GET /api/users/123 HTTP/1.1
Host: api.example.com
User-Agent: MyApp/1.0
Accept: application/json
Authorization: Bearer xyz...
매번 전송. HTTP/2 (HPACK)는:
- 정적 테이블 (자주 쓰는 헤더)
- 동적 테이블 (이전 요청의 헤더 캐시)
- Huffman 인코딩
→ **헤더 크기 80%+ 절감**.
5. 인터셉터 (Interceptors)
5.1 무엇인가?
**Interceptor** = RPC 호출 전후에 실행되는 미들웨어. 횡단 관심사 처리.
**용도**:
- 인증 (JWT 검증)
- 로깅
- 메트릭 (Prometheus)
- 분산 트레이싱
- 에러 처리
- 재시도
5.2 Server Interceptor (Python)
class AuthInterceptor(grpc.ServerInterceptor):
def intercept_service(self, continuation, handler_call_details):
metadata = dict(handler_call_details.invocation_metadata)
token = metadata.get('authorization')
if not verify_token(token):
return grpc.unary_unary_rpc_method_handler(
lambda req, ctx: ctx.abort(grpc.StatusCode.UNAUTHENTICATED, 'Invalid token')
)
return continuation(handler_call_details)
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
interceptors=[AuthInterceptor()]
)
5.3 Client Interceptor
class RetryInterceptor(grpc.UnaryUnaryClientInterceptor):
def intercept_unary_unary(self, continuation, client_call_details, request):
for attempt in range(3):
try:
return continuation(client_call_details, request)
except grpc.RpcError as e:
if e.code() != grpc.StatusCode.UNAVAILABLE:
raise
time.sleep(2 ** attempt)
raise
channel = grpc.intercept_channel(
grpc.insecure_channel('localhost:50051'),
RetryInterceptor()
)
5.4 Go Interceptor
func loggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("%s took %v", info.FullMethod, time.Since(start))
return resp, err
}
server := grpc.NewServer(
grpc.UnaryInterceptor(loggingInterceptor),
)
5.5 Chained Interceptors
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
interceptors=[
TracingInterceptor(),
AuthInterceptor(),
LoggingInterceptor(),
MetricsInterceptor(),
]
)
순서대로 실행. **인증 → 로깅** 같은 chain.
6. Deadline과 Cancellation
6.1 Deadline의 중요성
잘못 - 무한 대기
response = stub.GetUser(GetUserRequest(user_id=123))
올바름 - 5초 deadline
response = stub.GetUser(GetUserRequest(user_id=123), timeout=5.0)
**왜?**:
- 네트워크 문제 시 영원히 대기
- 스레드 고갈
- 사용자 경험
6.2 Deadline Propagation
[Client] (timeout=5s)
↓ deadline=now+5s
[Service A] (4s 소요)
↓ remaining=1s
[Service B] (이미 1s 남았으므로 짧게)
**효과**: 호출 체인 전체에 deadline 전파. **누적 timeout 방지**.
def call_chain(context):
context의 deadline 자동 사용
response = downstream_stub.SomeRPC(request, timeout=context.time_remaining())
6.3 Cancellation
클라이언트가 취소
future = stub.GetUser.future(request)
time.sleep(2)
future.cancel() # 서버에 취소 신호 전파
**서버 측**:
def GetUser(self, request, context):
while not context.is_active(): # 클라이언트가 취소했나?
return None
또는
if context.cancelled():
return None
6.4 Context의 역할
Go에서:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
response, err := client.GetUser(ctx, &GetUserRequest{UserId: 123})
Context는:
- Deadline 전달
- 취소 신호
- 메타데이터 (인증 토큰 등)
7. 로드 밸런싱
7.1 클라이언트 사이드 LB
gRPC 클라이언트가 여러 서버 IP를 알고 직접 선택.
DNS-based
channel = grpc.insecure_channel(
'dns:///my-service:50051',
options=[
('grpc.lb_policy_name', 'round_robin')
]
)
**정책**:
- `pick_first`: 첫 번째 사용 (기본)
- `round_robin`: 순환
- 사용자 정의
7.2 Look-aside LB
[Client] → [LB Service] → "사용 가능한 서버: A, B, C"
↓
[Client] → [Server A]
**예**: gRPC + xDS (Envoy)
7.3 Proxy LB
[Client] → [Envoy Proxy] → [Servers]
**예**: Envoy, Linkerd, Istio (Service Mesh)
**장점**: 클라이언트는 단일 endpoint만, 모든 LB 로직은 proxy.
7.4 헬스 체크
syntax = "proto3";
package grpc.health.v1;
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
}
ServingStatus status = 1;
}
표준 gRPC Health Check Protocol. 모든 gRPC 서버가 구현 가능.
8. 성능 최적화
8.1 메시지 크기 제한
기본 4MB. 큰 메시지는 거부됨.
options = [
('grpc.max_send_message_length', 100 * 1024 * 1024), # 100MB
('grpc.max_receive_message_length', 100 * 1024 * 1024),
]
channel = grpc.insecure_channel('localhost:50051', options=options)
**큰 데이터는 스트리밍 권장**, 단일 메시지 X.
8.2 Connection Pooling
잘못 - 매 요청마다 새 연결
def get_user(user_id):
channel = grpc.insecure_channel('localhost:50051')
stub = UserServiceStub(channel)
return stub.GetUser(GetUserRequest(user_id=user_id))
올바름 - 채널 재사용
channel = grpc.insecure_channel('localhost:50051')
stub = UserServiceStub(channel)
def get_user(user_id):
return stub.GetUser(GetUserRequest(user_id=user_id))
**왜?**: TCP 연결 오버헤드. HTTP/2의 멀티플렉싱 활용.
8.3 KeepAlive
options = [
('grpc.keepalive_time_ms', 10000), # 10초마다 ping
('grpc.keepalive_timeout_ms', 5000), # ping 응답 5초 대기
('grpc.keepalive_permit_without_calls', True),
('grpc.http2.max_pings_without_data', 0),
]
**효과**: 죽은 연결 감지, NAT timeout 방지.
8.4 압축
Server
server = grpc.server(..., compression=grpc.Compression.Gzip)
Client
channel = grpc.insecure_channel(
'localhost:50051',
compression=grpc.Compression.Gzip
)
**압축 알고리즘**: gzip, deflate, snappy.
**큰 메시지에 효과적**, 작은 메시지는 오버헤드.
9. gRPC-Web — 브라우저 지원
9.1 문제
브라우저는 HTTP/2 trailers, raw HTTP/2 등을 지원 안 함. gRPC를 직접 사용 X.
9.2 gRPC-Web
브라우저 친화적 변종:
- HTTP/1.1 또는 HTTP/2
- Trailers를 body에 인코딩
- CORS 지원
[Browser] ─ gRPC-Web ─→ [Envoy Proxy] ─ gRPC ─→ [Server]
9.3 사용
const client = new UserServiceClient('https://api.example.com')
client.getUser(new GetUserRequest().setUserId(123), (err, response) => {
if (err) console.error(err)
else console.log(response.getName())
})
9.4 한계
- **클라이언트 스트리밍** 지원 X (대부분 구현)
- **양방향 스트리밍** 지원 X
- 페이로드 약간 큼 (base64 인코딩)
9.5 Connect
**Connect** (Buf 회사): gRPC-Web의 후속작.
- HTTP/1.1, HTTP/2, gRPC, gRPC-Web 모두 지원
- 더 단순한 API
- TypeScript first
const client = createPromiseClient(UserService, transport)
const response = await client.getUser({ userId: 123 })
10. 디버깅과 도구
10.1 grpcurl
REST의 curl처럼 gRPC 호출:
서비스 목록
grpcurl -plaintext localhost:50051 list
메서드 호출
grpcurl -plaintext -d '{"user_id": 123}' \
localhost:50051 \
UserService/GetUser
**Reflection 필요**:
from grpc_reflection.v1alpha import reflection
SERVICE_NAMES = (UserService_pb2.DESCRIPTOR.services_by_name['UserService'].full_name, reflection.SERVICE_NAME)
reflection.enable_server_reflection(SERVICE_NAMES, server)
10.2 BloomRPC / Postman
GUI 클라이언트.
10.3 로깅
logging.basicConfig(level=logging.DEBUG)
os.environ['GRPC_VERBOSITY'] = 'DEBUG'
os.environ['GRPC_TRACE'] = 'all'
10.4 분산 트레이싱
OpenTelemetry 통합:
from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer
GrpcInstrumentorServer().instrument()
자동으로 모든 gRPC 호출이 trace에 포함.
퀴즈
**답**: 필드 번호는 **이진 형식의 식별자**입니다. JSON처럼 키 이름을 매번 전송하지 않고, 작은 정수만 사용 → 페이로드 크기 감소. 또한 **schema evolution의 핵심**: 필드 이름은 변경 가능하지만 번호는 변경 불가. 같은 번호면 이전 버전과 호환. **삭제된 필드의 번호는 절대 재사용하면 안 됨** (`reserved`로 표시). 1-15는 1 byte, 16-2047은 2 byte로 인코딩 → 자주 쓰는 필드는 1-15에 할당.
**답**: 4가지 요소: (1) **Protobuf** — JSON 대비 5-10배 작음, 파싱 빠름, (2) **HTTP/2** — 멀티플렉싱, 단일 연결로 많은 요청, (3) **HPACK 헤더 압축** — 반복 헤더 80%+ 절감, (4) **이진 프로토콜** — 텍스트 파싱 오버헤드 X. 결과: 처리량 5-10배, latency 절반. 단점: 디버깅 어려움 (이진), 브라우저 직접 지원 X (gRPC-Web 필요).
**답**: 양쪽이 독립적으로 메시지를 보내야 할 때. **사용 사례**: (1) **채팅** — 사용자가 메시지 보내고, 서버가 다른 사용자 메시지 푸시, (2) **실시간 게임** — 클라이언트의 입력과 서버의 게임 상태 양방향, (3) **IoT** — 디바이스의 센서 데이터와 서버의 명령, (4) **음성 인식** — 오디오 스트림과 transcript 양방향. WebSocket과 비슷하지만 gRPC의 강한 타입 + Protobuf 효율.
**답**: 분산 시스템에서 호출 체인의 **누적 timeout을 방지**합니다. 예: Client (5s) → A (4s 소요) → B를 5s timeout으로 호출하면 → 총 9s 가능. **올바른 동작**: A가 B를 호출할 때 남은 시간(1s)으로 호출. gRPC는 Context를 통해 deadline을 자동 전파. **모든 다운스트림 호출이 부모의 deadline을 상속**. Go의 `context.Context`, Python의 `context.time_remaining()`. 분산 시스템 안정성의 핵심.
**답**: 브라우저는 HTTP/2 trailers, raw streams 등 gRPC 기능을 직접 지원 못 합니다. **gRPC-Web**은 브라우저 친화적 변종으로 (1) HTTP/1.1 또는 HTTP/2 사용, (2) trailers를 body에 인코딩, (3) CORS 지원. **한계**: 클라이언트 스트리밍과 양방향 스트리밍 미지원 (서버 스트리밍은 OK). 서버는 보통 **Envoy proxy**를 통해 gRPC-Web ↔ gRPC 변환. **Connect** (Buf)가 더 단순한 후속 표준.
참고 자료
- [gRPC](https://grpc.io/) — 공식
- [Protocol Buffers](https://protobuf.dev/) — 공식
- [gRPC Internals](https://grpc.io/blog/grpc-on-http2/)
- [Connect](https://connectrpc.com/) — gRPC-Web 후속
- [Buf](https://buf.build/) — Protobuf 도구
- [grpcurl](https://github.com/fullstorydev/grpcurl) — gRPC용 curl
- [BloomRPC](https://github.com/bloomrpc/bloomrpc) — GUI 클라이언트
- [gRPC Best Practices](https://grpc.io/docs/guides/performance/)
- [Envoy gRPC-Web](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_web_filter)
- [gRPC Health Checking](https://github.com/grpc/grpc/blob/master/doc/health-checking.md)
- [Awesome gRPC](https://github.com/grpc-ecosystem/awesome-grpc)
현재 단락 (1/391)
- **gRPC = HTTP/2 + Protobuf**: 5-10배 빠르고 작은 RPC