Skip to content

필사 모드: gRPC 심화 완전 가이드 2025: Protocol Buffers, 4가지 통신 모델, 인터셉터, 로드 밸런싱

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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

작성 글자: 0원문 글자: 12,354작성 단락: 0/391