C++ gRPC 마스터 | 스트리밍·인터셉터·로드밸런싱 [#52-2]
이 글의 핵심
C++ gRPC 고급: 양방향 스트리밍, 인터셉터, 클라이언트 로드밸런싱, 데드라인, 재시도. 실무 문제 시나리오, 완전한 예제, 자주 발생하는 에러, 성능 최적화, 프로덕션 패턴까지.
들어가며: “양방향 스트리밍·인증·로드밸런싱이 막막해요”
실제 겪는 문제 시나리오
43-1과 52-1에서 gRPC 기초를 다뤘다면, 이 글에서는 고급 기능을 다룹니다. 실무에서 자주 맞닥뜨리는 문제와 해결 방법을 제시합니다.
시나리오 1: 채팅 서버에서 클라이언트·서버가 동시에 메시지 송수신
상황: 실시간 채팅 앱에서 클라이언트가 메시지를 보내는 동시에 서버가 다른 사용자 메시지를 푸시해야 함
문제: 단순 요청-응답이나 서버 스트리밍만으로는 양방향 동시 통신 불가
결과: gRPC 양방향 스트리밍 → 단일 연결로 읽기/쓰기 동시 수행
시나리오 2: 모든 RPC에 인증 토큰을 수동으로 넣는 번거로움
상황: 50개 이상의 RPC 메서드가 있는 서비스에서 매번 ClientContext에 authorization 메타데이터 추가
문제: 비즈니스 로직과 인증 로직이 섞여 유지보수 어려움, 누락 시 런타임 에러
결과: 클라이언트 인터셉터로 모든 RPC에 자동으로 토큰 주입
시나리오 3: 단일 서버에 트래픽이 몰려 장애
상황: 3대의 gRPC 서버를 띄웠는데 클라이언트가 첫 번째 서버에만 연결됨
문제: pick_first(기본) 정책은 첫 연결 성공 시 나머지 무시
결과: round_robin 로드밸런싱으로 요청을 서버들에 균등 분산
시나리오 4: 서버 재시작 시 일시적 UNAVAILABLE로 전체 실패
상황: Kubernetes에서 서버를 롤링 업데이트할 때 5~10초간 연결 끊김
문제: 재시도 없이 한 번 실패하면 사용자에게 에러 반환
결과: 지수 백오프 재시도 + 데드라인 관리로 일시 장애 극복
시나리오 5: 스트리밍 중 클라이언트 연결 끊김 감지 지연
상황: 양방향 스트리밍에서 클라이언트가 종료했는데 서버가 계속 Write 시도
문제: Write 실패를 확인하지 않으면 리소스 낭비, 데드락 가능성
결과: Write 반환값 확인, IsCancelled() 주기적 체크
flowchart TB
subgraph 문제[실무 문제]
P1[양방향 통신] --> S1[양방향 스트리밍]
P2[인증·로깅 중복] --> S2[인터셉터]
P3[단일 서버 과부하] --> S3[로드밸런싱]
P4[일시적 장애] --> S4[재시도·데드라인]
end
목표:
- 양방향 스트리밍: 채팅·실시간 협업 패턴
- 인터셉터: 인증·로깅·메트릭 중앙 처리
- 로드밸런싱: round_robin, 다중 주소
- 데드라인·재시도: 프로덕션급 에러 처리
요구 환경: C++17 이상, gRPC 1.50+
이 글을 읽으면:
- 양방향 스트리밍을 실전에서 활용할 수 있습니다.
- 인터셉터로 크로스커팅 관심사를 분리할 수 있습니다.
- 로드밸런싱으로 다중 서버를 활용할 수 있습니다.
- 데드라인·재시도로 안정적인 클라이언트를 구현할 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 양방향 스트리밍 완벽 가이드
- 인터셉터: 인증·로깅·메트릭
- 클라이언트 로드밸런싱
- 데드라인·재시도 고급
- 완전한 gRPC 마스터 예제
- 자주 발생하는 에러와 해결법
- 성능 최적화
- 프로덕션 패턴
- 구현 체크리스트
- 정리
1. 양방향 스트리밍 완벽 가이드
양방향 스트리밍이란?
클라이언트와 서버가 동시에 읽기와 쓰기를 수행하는 RPC 패턴입니다. 단일 HTTP/2 스트림에서 양쪽이 독립적으로 메시지를 송수신합니다.
sequenceDiagram
participant C as 클라이언트
participant S as 서버
par 동시 읽기/쓰기
C->>S: msg1
S->>C: response1
C->>S: msg2
S->>C: response2
end
Note over C,S: Read/Write 순서는 보장되지 않음
.proto 정의
syntax = "proto3";
package chat;
// 양방향 스트리밍: stream 입력, stream 출력
service ChatService {
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user_id = 1;
string text = 2;
int64 timestamp = 3;
}
서버 측: 양방향 스트리밍 구현
핵심: ServerReaderWriter로 읽기와 쓰기를 동시에 수행합니다. Read()는 블로킹되며, 별도 스레드에서 Write()를 수행할 수 있습니다.
#include <grpcpp/grpcpp.h>
#include "chat.grpc.pb.h"
#include <thread>
#include <queue>
#include <mutex>
using grpc::ServerContext;
using grpc::Status;
using grpc::StatusCode;
class ChatServiceImpl final : public chat::ChatService::Service {
public:
Status Chat(ServerContext* context,
grpc::ServerReaderWriter<chat::ChatMessage, chat::ChatMessage>* stream) override {
// 수신 스레드: 클라이언트 메시지 읽기
std::thread reader([context, stream]() {
chat::ChatMessage msg;
while (stream->Read(&msg)) {
// 비즈니스 로직: 메시지 브로드캐스트 등
// 여기서는 에코
chat::ChatMessage response;
response.set_user_id("server");
response.set_text("Echo: " + msg.text());
response.set_timestamp(std::time(nullptr));
if (!stream->Write(response)) {
break; // 클라이언트 연결 끊김
}
}
});
reader.join();
return Status::OK;
}
};
멀티클라이언트 채팅: 메시지 브로드캐스트
여러 클라이언트가 같은 방에 있을 때, 한 클라이언트 메시지를 다른 클라이언트들에게 전달하는 패턴입니다.
#include <grpcpp/grpcpp.h>
#include "chat.grpc.pb.h"
#include <unordered_map>
#include <mutex>
#include <memory>
class ChatRoom {
public:
void Join(const std::string& user_id,
grpc::ServerReaderWriter<chat::ChatMessage, chat::ChatMessage>* stream) {
std::lock_guard<std::mutex> lock(mutex_);
clients_[user_id] = stream;
}
void Leave(const std::string& user_id) {
std::lock_guard<std::mutex> lock(mutex_);
clients_.erase(user_id);
}
void Broadcast(const std::string& sender_id, const chat::ChatMessage& msg) {
std::lock_guard<std::mutex> lock(mutex_);
for (auto& [user_id, stream] : clients_) {
if (user_id != sender_id) {
stream->Write(msg); // Write 실패 시 해당 클라이언트 제거 고려
}
}
}
private:
std::unordered_map<std::string,
grpc::ServerReaderWriter<chat::ChatMessage, chat::ChatMessage>*> clients_;
std::mutex mutex_;
};
// 서비스 구현에서
Status Chat(ServerContext* context,
grpc::ServerReaderWriter<chat::ChatMessage, chat::ChatMessage>* stream) override {
std::string user_id = GetUserIdFromMetadata(context);
room_.Join(user_id, stream);
// scope 종료 시 Leave 호출하려면 RAII 활용
chat::ChatMessage msg;
while (stream->Read(&msg)) {
if (context->IsCancelled()) break;
room_.Broadcast(user_id, msg);
}
room_.Leave(user_id);
return Status::OK;
}
클라이언트 측: 양방향 스트리밍
클라이언트는 ClientReaderWriter로 읽기와 쓰기를 동시에 수행합니다. 읽기와 쓰기를 별도 스레드에서 실행하는 것이 일반적입니다.
#include <grpcpp/grpcpp.h>
#include "chat.grpc.pb.h"
#include <thread>
#include <iostream>
void RunChatClient(const std::string& user_id) {
auto channel = grpc::CreateChannel("localhost:50051",
grpc::InsecureChannelCredentials());
auto stub = chat::ChatService::NewStub(channel);
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(60));
auto stream = stub->Chat(&context);
// 쓰기 스레드: 사용자 입력을 서버로 전송
std::thread writer([&stream, &user_id]() {
std::string line;
while (std::getline(std::cin, line)) {
chat::ChatMessage msg;
msg.set_user_id(user_id);
msg.set_text(line);
msg.set_timestamp(std::time(nullptr));
if (!stream->Write(msg)) break;
}
stream->WritesDone();
});
// 읽기 스레드: 서버 메시지 수신
std::thread reader([&stream]() {
chat::ChatMessage msg;
while (stream->Read(&msg)) {
std::cout << "[" << msg.user_id() << "] " << msg.text() << std::endl;
}
});
writer.join();
reader.join();
grpc::Status status = stream->Finish();
if (!status.ok()) {
std::cerr << "Chat failed: " << status.error_message() << std::endl;
}
}
양방향 스트리밍 주의사항
| 항목 | 설명 |
|---|---|
| Read/Write 순서 | 메시지 순서는 보장되지만, 읽기와 쓰기는 독립적 |
| IsCancelled() | 장시간 루프에서 주기적으로 확인해 조기 종료 |
| Write 실패 | false 반환 시 클라이언트 연결 끊김, 즉시 종료 |
| WritesDone() | 클라이언트가 더 이상 보낼 메시지 없을 때 호출 |
2. 인터셉터: 인증·로깅·메트릭
인터셉터란?
인터셉터는 RPC 호출 전후에 실행되는 미들웨어입니다. 인증, 로깅, 메트릭, 메타데이터 처리 등 크로스커팅 관심사를 비즈니스 로직과 분리합니다.
flowchart LR
subgraph 클라이언트
A[애플리케이션] --> I1[인증 인터셉터]
I1 --> I2[로깅 인터셉터]
I2 --> N[네트워크]
end
subgraph 서버
N --> J1[로깅 인터셉터]
J1 --> J2[인증 인터셉터]
J2 --> B[비즈니스 로직]
end
클라이언트 인터셉터: 인증 토큰 자동 주입
gRPC C++ 인터셉터는 grpc::experimental 네임스페이스에 있습니다. Interceptor를 상속하고 Intercept를 오버라이드합니다.
#include <grpcpp/grpcpp.h>
#include <grpcpp/support/client_interceptor.h>
#include <iostream>
using grpc::experimental::Interceptor;
using grpc::experimental::InterceptorBatchMethods;
class AuthClientInterceptor : public Interceptor {
public:
explicit AuthClientInterceptor(const std::string& token) : token_(token) {}
void Intercept(InterceptorBatchMethods* methods) override {
// send_initial_metadata 훅에서 메타데이터 추가
if (methods->QueryInterceptionHookPoint(
grpc::experimental::InterceptionHookPoints::PRE_SEND_INITIAL_METADATA)) {
// 메타데이터에 authorization 추가
// (실제 API는 버전에 따라 다를 수 있음)
}
methods->Proceed();
}
private:
std::string token_;
};
class AuthClientInterceptorFactory : public grpc::experimental::ClientInterceptorFactoryInterface {
public:
explicit AuthClientInterceptorFactory(const std::string& token) : token_(token) {}
Interceptor* CreateClientInterceptor(grpc::experimental::ClientRpcInfo* info) override {
return new AuthClientInterceptor(token_);
}
private:
std::string token_;
};
// 사용: 채널 생성 시 인터셉터 등록
void CreateChannelWithAuth(const std::string& token) {
std::vector<std::unique_ptr<grpc::experimental::ClientInterceptorFactoryInterface>>
creators;
creators.push_back(std::make_unique<AuthClientInterceptorFactory>(token));
auto channel = grpc::experimental::CreateCustomChannelWithInterceptors(
"localhost:50051",
grpc::InsecureChannelCredentials(),
grpc::ChannelArguments(),
std::move(creators));
}
참고: gRPC C++ 인터셉터 API는 experimental이며 버전별로 차이가 있을 수 있습니다. grpc/examples/cpp/interceptors를 참고하세요.
로깅 인터셉터 (개념)
모든 RPC 호출의 메서드명, 소요 시간, 성공/실패를 로깅합니다.
// 개념적 구조
class LoggingClientInterceptor : public Interceptor {
public:
void Intercept(InterceptorBatchMethods* methods) override {
auto start = std::chrono::steady_clock::now();
methods->Proceed();
// Proceed()는 비동기일 수 있으므로, 실제로는
// recv_status 등 훅에서 로깅
auto dur = std::chrono::steady_clock::now() - start;
LOG(INFO) << "RPC took " << dur.count() << " ns";
}
};
서버 인터셉터: 요청 ID 추적
서버 측에서는 들어오는 메타데이터에서 x-request-id를 읽어 로깅에 활용합니다.
#include <grpcpp/grpcpp.h>
#include <grpcpp/support/server_interceptor.h>
// 서버 빌더에 인터셉터 등록
grpc::ServerBuilder builder;
builder.AddListeningPort("0.0.0.0:50051", grpc::InsecureServerCredentials());
builder.RegisterService(&service);
// builder.experimental().SetInterceptorCreators(...); // API 확인 필요
인터셉터 순서
인터셉터는 등록 순서대로 실행됩니다. 애플리케이션에 가까운 것이 먼저, 네트워크에 가까운 것이 나중에 실행됩니다.
클라이언트: [Auth] → [Logging] → [Network]
서버: [Network] → [Logging] → [Auth] → [Service]
3. 클라이언트 로드밸런싱
로드밸런싱 정책 비교
| 정책 | 동작 | 사용 사례 |
|---|---|---|
| pick_first (기본) | 주소 목록에서 첫 연결 성공 시 해당 서버만 사용 | 단일 서버, 개발 |
| round_robin | 연결된 서버들에 요청을 순환 분배 | 다중 서버, 수평 확장 |
| grpclb | 외부 로드밸런서 연동 (deprecated, xDS 권장) | 대규모 클러스터 |
round_robin 활성화
ChannelArguments에 로드밸런싱 정책을 설정합니다.
#include <grpcpp/grpcpp.h>
grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
// 단일 주소: DNS로 여러 IP가 반환되면 round_robin 적용
auto channel = grpc::CreateCustomChannel(
"dns:///my-service:50051", // DNS 기반
grpc::InsecureChannelCredentials(),
args);
// 명시적 다중 주소 (일부 환경)
// ipv4:///host1:50051,host2:50051,host3:50051
auto channel2 = grpc::CreateCustomChannel(
"ipv4:///localhost:50051,localhost:50052,localhost:50053",
grpc::InsecureChannelCredentials(),
args);
Service Config로 로드밸런싱 (JSON)
일부 환경에서는 채널 인자 대신 Service Config로 로드밸런싱을 지정합니다. DNS 응답에 포함되거나, 이름 해석 시 반환됩니다.
{
"loadBalancingConfig": [{ "round_robin": {} }],
"methodConfig": [{
"name": [{"service": "myapp.MyService"}],
"timeout": "30s"
}]
}
Kubernetes 서비스와 연동
Kubernetes Service는 DNS로 여러 Pod IP를 반환합니다. dns:///를 사용하면 자동으로 round_robin이 적용됩니다.
// Kubernetes 내부: my-grpc-service.namespace.svc.cluster.local:50051
grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
auto channel = grpc::CreateCustomChannel(
"dns:///my-grpc-service:50051",
grpc::InsecureChannelCredentials(),
args);
로드밸런싱 동작 원리
flowchart TB
subgraph 클라이언트
C[Stub]
end
subgraph 채널
LB[Load Balancer]
S1[Subchannel 1]
S2[Subchannel 2]
S3[Subchannel 3]
LB --> S1
LB --> S2
LB --> S3
end
C -->|RPC 1| LB
LB -->|round_robin| S1
C -->|RPC 2| LB
LB --> S2
C -->|RPC 3| LB
LB --> S3
4. 데드라인·재시도 고급
데드라인 전파
클라이언트가 설정한 데드라인은 서버에 전달됩니다. 서버는 context->IsCancelled()로 확인해 불필요한 작업을 중단할 수 있습니다.
// 클라이언트
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(10));
stub->SomeRpc(&context, request, &response);
// 서버
Status SomeRpc(ServerContext* context, ...) override {
for (int i = 0; i < 10000; ++i) {
if (context->IsCancelled()) {
return Status(StatusCode::CANCELLED, "Deadline exceeded");
}
DoWork(i);
}
return Status::OK;
}
지수 백오프 재시도 (완전한 구현)
#include <grpcpp/grpcpp.h>
#include <chrono>
#include <thread>
#include <functional>
template<typename Request, typename Response>
grpc::Status CallWithRetry(
std::function<grpc::Status(grpc::ClientContext*, const Request&, Response*)> rpc,
const Request& request,
Response* response,
int max_retries = 3,
int base_delay_ms = 100) {
grpc::Status status;
for (int i = 0; i < max_retries; ++i) {
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(30));
status = rpc(&context, request, response);
if (status.ok()) return status;
// 재시도 가능한 에러만
switch (status.error_code()) {
case grpc::StatusCode::UNAVAILABLE:
case grpc::StatusCode::DEADLINE_EXCEEDED:
case grpc::StatusCode::RESOURCE_EXHAUSTED:
case grpc::StatusCode::ABORTED:
break;
default:
return status; // 재시도 불가
}
if (i < max_retries - 1) {
int delay_ms = base_delay_ms * (1 << i); // 100, 200, 400
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
}
}
return status;
}
// 사용
grpc::Status status = CallWithRetry<MyRequest, MyResponse>(
[&](grpc::ClientContext* ctx, const MyRequest& req, MyResponse* res) {
return stub_->MyRpc(ctx, req, res);
},
request, &response);
재시도 시 주의사항
| 항목 | 설명 |
|---|---|
| 멱등성 | 재시도 시 중복 실행되어도 되는 RPC인지 확인 (조회는 OK, 주문 생성은 주의) |
| 최대 재시도 | 무한 재시도 방지, 3~5회 권장 |
| jitter | 동시 재시도 폭주 방지를 위해 delay에 랜덤 추가 고려 |
5. 완전한 gRPC 마스터 예제
예제 1: 양방향 스트리밍 + 데드라인 + IsCancelled
실시간 협업 에디터의 동기화 스트림을 가정한 예제입니다.
// .proto
// service SyncService {
// rpc Sync(stream Delta) returns (stream Delta);
// }
Status Sync(ServerContext* context,
grpc::ServerReaderWriter<sync::Delta, sync::Delta>* stream) override {
sync::Delta delta;
while (stream->Read(&delta)) {
if (context->IsCancelled()) {
return Status(StatusCode::CANCELLED, "Client disconnected");
}
// 델타 적용 후 다른 클라이언트들에게 브로드캐스트
sync::Delta response = ApplyDelta(delta);
if (!stream->Write(response)) {
break; // 쓰기 실패
}
}
return Status::OK;
}
예제 2: 로드밸런싱 + 재시도 클라이언트
#include <grpcpp/grpcpp.h>
#include "my_service.grpc.pb.h"
class ResilientClient {
public:
ResilientClient(const std::string& service_addr) {
grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
channel_ = grpc::CreateCustomChannel(
service_addr,
grpc::InsecureChannelCredentials(),
args);
stub_ = myapp::MyService::NewStub(channel_);
}
grpc::Status CallWithRetry(const myapp::Request& req, myapp::Response* res) {
return CallWithRetry<myapp::Request, myapp::Response>(
[this](grpc::ClientContext* ctx, const myapp::Request& r, myapp::Response* s) {
return stub_->MyRpc(ctx, r, s);
},
req, res);
}
private:
std::shared_ptr<grpc::Channel> channel_;
std::unique_ptr<myapp::MyService::Stub> stub_;
};
예제 3: 메타데이터 기반 요청 ID + 트레이싱
// 클라이언트: 요청 ID 주입
void AddRequestId(grpc::ClientContext* context) {
std::string request_id = "req-" + std::to_string(std::rand());
context->AddMetadata("x-request-id", request_id);
context->AddMetadata("x-trace-id", GetCurrentTraceId());
}
// 서버: 메타데이터 읽기
std::string GetRequestId(grpc::ServerContext* context) {
auto it = context->client_metadata().find("x-request-id");
if (it != context->client_metadata().end()) {
return std::string(it->second.begin(), it->second.end());
}
return "";
}
// RPC 핸들러에서
Status GetUser(ServerContext* context, ...) override {
std::string req_id = GetRequestId(context);
LOG(INFO) << "[" << req_id << "] GetUser called";
// ...
}
6. 자주 발생하는 에러와 해결법
문제 1: 양방향 스트리밍에서 “Stream removed”
증상: 스트리밍 중 Write 또는 Read에서 스트림이 끊김.
원인: 클라이언트가 연결을 끊었는데 서버가 계속 Write 시도.
해결법:
// ✅ Write 반환값 확인
while (stream->Read(&msg)) {
if (!stream->Write(response)) {
break; // 즉시 종료
}
}
// ✅ 주기적 IsCancelled() 확인
for (int i = 0; i < 1000; ++i) {
if (context->IsCancelled()) return Status::OK;
stream->Write(chunk);
}
문제 2: round_robin이 동작하지 않음
증상: 다중 서버를 두었는데 한 서버에만 요청이 감.
원인: pick_first가 기본값이거나, 단일 IP만 반환되는 주소 사용.
해결법:
// ❌ 잘못된 예: 로드밸런싱 미설정
auto channel = grpc::CreateChannel("localhost:50051", ...);
// ✅ 올바른 예: round_robin 명시
grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
auto channel = grpc::CreateCustomChannel(
"dns:///my-service:50051", // DNS가 여러 IP 반환해야 함
grpc::InsecureChannelCredentials(),
args);
문제 3: “DEADLINE_EXCEEDED”가 너무 자주 발생
증상: 서버는 정상인데 클라이언트에서 타임아웃.
원인: 데드라인이 너무 짧거나, 서버 처리 시간이 김.
해결법:
// ✅ RPC 유형별 데드라인 분리
// 단순 조회: 5초
context.set_deadline(now + std::chrono::seconds(5));
stub->GetUser(&context, req, &res);
// 스트리밍: 60초
context.set_deadline(now + std::chrono::seconds(60));
auto stream = stub->StreamLogs(&context, req);
문제 4: 재시도 시 중복 실행 (멱등성 위반)
증상: 주문 생성 RPC를 재시도했는데 주문이 2건 생성됨.
원인: 생성·수정 RPC는 멱등하지 않음.
해결법:
// ✅ 멱등한 RPC만 재시도
bool IsRetryable(grpc::StatusCode code, const std::string& method) {
if (method == "CreateOrder" || method == "SubmitPayment") {
return false; // 멱등하지 않음
}
return code == grpc::StatusCode::UNAVAILABLE ||
code == grpc::StatusCode::DEADLINE_EXCEEDED;
}
문제 5: 인터셉터에서 “experimental” API를 찾을 수 없음
증상: grpc::experimental::Interceptor 컴파일 에러.
원인: gRPC 버전에 따라 API 위치가 다름.
해결법:
// 헤더 확인
#include <grpcpp/support/client_interceptor.h>
#include <grpcpp/support/interceptor.h>
// vcpkg/CMake에서 gRPC 버전 확인
// vcpkg list grpc
문제 6: 스트리밍 시 메모리 증가
증상: 장시간 스트리밍 시 메모리 사용량이 계속 증가.
원인: 수신 버퍼에 메시지가 쌓이거나, 발송 큐가 비우지 않음.
해결법:
// ✅ 읽은 메시지는 즉시 처리 후 버림
while (reader->Read(&chunk)) {
ProcessChunk(chunk); // chunk 사용 후 스코프 종료로 해제
}
// ✅ 대량 발송 시 흐름 제어 고려 (백프레셔)
7. 성능 최적화
채널·스텁 재사용
// ❌ 매 요청마다 새 채널 (매우 비효율)
for (int i = 0; i < 10000; ++i) {
auto channel = grpc::CreateChannel(...);
auto stub = Service::NewStub(channel);
stub->Call(...);
}
// ✅ 채널·스텁 한 번 생성 후 재사용
auto channel = grpc::CreateChannel(...);
auto stub = Service::NewStub(channel);
for (int i = 0; i < 10000; ++i) {
stub->Call(...);
}
스트리밍 버퍼 크기 조정
grpc::ChannelArguments args;
args.SetInt("grpc.max_receive_message_length", 64 * 1024 * 1024); // 64MB
args.SetInt("grpc.keepalive_time_ms", 10000); // 10초
args.SetInt("grpc.keepalive_timeout_ms", 5000);
양방향 스트리밍에서 읽기/쓰기 스레드 분리
읽기와 쓰기를 같은 스레드에서 블로킹하면 데드락이 발생할 수 있습니다.
// ✅ 읽기·쓰기 스레드 분리
std::thread reader([&]() { while (stream->Read(&msg)) Process(msg); });
std::thread writer([&]() { while (Send()) stream->Write(msg); });
HTTP/2 멀티플렉싱 활용
gRPC는 HTTP/2 기반이므로 단일 연결에서 여러 RPC를 동시에 처리합니다. 채널을 스레드 간에 공유해도 됩니다.
// ✅ 스레드 안전: 같은 채널을 여러 스레드에서 사용
auto channel = grpc::CreateChannel(...);
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i) {
workers.emplace_back([channel]() {
auto stub = Service::NewStub(channel);
while (running) stub->Call(...);
});
}
8. 프로덕션 패턴
그레이스풀 셧다운
void ShutdownServer(grpc::Server* server, grpc::CompletionQueue* cq) {
server->Shutdown(); // 새 RPC 거부, 기존 RPC 완료 대기
cq->Shutdown(); // CompletionQueue 종료
server->Wait(); // 모든 스레드 종료 대기
}
헬스 체크 연동
service HealthService {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
// Kubernetes liveness/readiness 프로브
// grpc_health_probe -addr=:50051
로컬 테스트: 다중 서버 시뮬레이션
# 터미널 1: 서버 1
./grpc_server --port 50051
# 터미널 2: 서버 2
./grpc_server --port 50052
# 터미널 3: 서버 3
./grpc_server --port 50053
# 터미널 4: round_robin 클라이언트 (3개 주소)
./grpc_client --addresses 127.0.0.1:50051,127.0.0.1:50052,127.0.0.1:50053
메트릭 수집 (인터셉터 활용)
인터셉터에서 RPC 호출 수, 지연 시간, 에러율을 수집해 Prometheus 등에 노출합니다.
// 개념: 인터셉터에서
// - PRE_SEND_INITIAL_METADATA: 시작 시간 기록
// - RECV_STATUS: 소요 시간, status 기록 → 메트릭 업데이트
환경별 설정
struct GrpcConfig {
std::string address;
std::string lb_policy;
int deadline_seconds;
int max_retries;
};
GrpcConfig LoadConfig() {
if (std::getenv("KUBERNETES_SERVICE_HOST")) {
return {"dns:///my-service:50051", "round_robin", 30, 3};
}
return {"localhost:50051", "pick_first", 10, 1};
}
9. 구현 체크리스트
양방향 스트리밍
-
stream입력,stream출력으로 .proto 정의 -
ServerReaderWriter/ClientReaderWriter사용 -
Write반환값 확인 (false 시 종료) -
IsCancelled()주기적 확인 (장시간 루프) - 읽기/쓰기 스레드 분리 (데드락 방지)
인터셉터
- 인증·로깅 등 크로스커팅 관심사 식별
-
ClientInterceptorFactoryInterface/ServerInterceptorFactoryInterface구현 - 채널/서버 빌드 시 인터셉터 등록
- 인터셉터 순서 검토
로드밸런싱
-
ChannelArguments::SetLoadBalancingPolicyName("round_robin")설정 - DNS 또는 다중 주소로 여러 백엔드 노출
- Kubernetes 서비스와 연동 시
dns:///사용
데드라인·재시도
- 모든 RPC에
set_deadline설정 - 재시도 가능 에러만 재시도 (UNAVAILABLE, DEADLINE_EXCEEDED 등)
- 지수 백오프 적용
- 멱등하지 않은 RPC는 재시도 제외
프로덕션
- 그레이스풀 셧다운
- 헬스 체크 RPC
- 메타데이터 트레이싱 (x-request-id)
- 메트릭·로깅 연동
10. 정리
| 항목 | 요약 |
|---|---|
| 양방향 스트리밍 | ServerReaderWriter/ClientReaderWriter, Read/Write 동시 수행, Write 실패·IsCancelled 확인 |
| 인터셉터 | 인증·로깅·메트릭 중앙 처리, experimental API |
| 로드밸런싱 | round_robin, ChannelArguments, DNS·다중 주소 |
| 데드라인 | set_deadline, IsCancelled, RPC 유형별 분리 |
| 재시도 | 지수 백오프, 멱등성 고려, UNAVAILABLE 등만 재시도 |
| 성능 | 채널·스텁 재사용, 스레드 분리, HTTP/2 멀티플렉싱 |
| 프로덕션 | 그레이스풀 셧다운, 헬스 체크, 메트릭, 환경별 설정 |
핵심 원칙:
- 양방향 스트리밍에서는 Write 실패와 IsCancelled를 반드시 확인
- 인터셉터로 비즈니스 로직과 인증·로깅 분리
- 다중 서버 환경에서는 round_robin 로드밸런싱 적용
- 데드라인과 재시도로 일시 장애에 대비
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 마이크로서비스 통신, 실시간 스트리밍, 분산 시스템, 고성능 RPC 등에 활용합니다. 양방향 스트리밍은 채팅·게임 서버, 인터셉터는 인증·로깅, 로드밸런싱은 다중 서버 분산에 필수입니다.
Q. 선행으로 읽으면 좋은 글은?
A. gRPC·Protobuf(#43-1)와 gRPC 완벽 가이드(#52-1)를 먼저 읽으세요.
Q. 더 깊이 공부하려면?
A. cppreference와 gRPC 공식 문서를 참고하세요. grpc/examples/cpp 예제도 활용하면 좋습니다.
한 줄 요약: 양방향 스트리밍·인터셉터·로드밸런싱·데드라인·재시도를 마스터할 수 있습니다.
다음 글: C++ 시리즈 목차에서 다음 주제를 확인하세요.
이전 글: gRPC 완벽 가이드(#52-1)
관련 글
- C++ gRPC 기초 완벽 가이드 | Protocol Buffers·Unary·스트리밍·실전 문제 해결
- C++ gRPC 고급 완벽 가이드 | 인터셉터·로드밸런싱·데드라인·재시도·헬스체크 [#52-3]
- C++ Protocol Buffers 완벽 가이드 | 직렬화·스키마 진화·성능 최적화·프로덕션 패턴
- C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴