C++ gRPC 고급 완벽 가이드 | 인터셉터·로드밸런싱·데드라인·재시도·헬스체크 [#52-3]
이 글의 핵심
C++ gRPC 프로덕션 배포 시 인터셉터 인증·로깅, round_robin 로드밸런싱, 데드라인 전파, 지수 백오프 재시도, gRPC Health Checking Protocol.
들어가며: “프로덕션 gRPC 배포가 막막해요”
실제 겪는 문제 시나리오
52-1, 52-2에서 gRPC 기초와 스트리밍을 다뤘다면, 이 글에서는 프로덕션급 고급 기능을 다룹니다. Kubernetes 배포, 인증·로깅, 장애 복구까지 실무에서 맞닥뜨리는 문제와 해결 방법을 제시합니다.
시나리오 1: 50개 RPC마다 수동으로 인증 토큰 추가
상황: 마이크로서비스 A가 B를 호출할 때 매번 ClientContext에 authorization 메타데이터 추가
문제: 비즈니스 로직과 인증이 섞여 유지보수 어렵고, 누락 시 UNAUTHENTICATED 에러
결과: 클라이언트 인터셉터로 모든 RPC에 자동 토큰 주입
시나리오 2: 3대 서버 중 1대에만 트래픽 몰림
상황: gRPC 서버 3대를 띄웠는데 pick_first(기본)로 첫 서버에만 연결
문제: 첫 서버 과부하, 나머지 서버 유휴
결과: round_robin 로드밸런싱 + DNS/다중 주소로 균등 분산
시나리오 3: 롤링 업데이트 중 UNAVAILABLE 폭주
상황: Kubernetes에서 Pod 재시작 시 5~10초간 연결 끊김
문제: 재시도 없이 한 번 실패하면 사용자 에러
결과: 지수 백오프 재시도 + 데드라인 관리로 일시 장애 극복
시나리오 4: Kubernetes liveness 프로브가 gRPC를 지원 안 함
상황: HTTP 헬스 체크는 되는데 gRPC 서버는 TCP만 체크 가능
문제: 서버가 살아있어도 RPC 처리 불가 상태 감지 못 함
결과: gRPC Health Checking Protocol + grpc_health_probe 연동
시나리오 5: 스트리밍 RPC에 짧은 데드라인 적용
상황: 60초 스트리밍 RPC에 5초 데드라인 설정
문제: 스트림 시작 직후 DEADLINE_EXCEEDED
결과: RPC 유형별 데드라인 분리 (단순 RPC 5초, 스트리밍 60초+)
시나리오 6: 재시도로 주문 2건 생성
상황: CreateOrder RPC가 UNAVAILABLE 후 재시도
문제: 서버는 1건 처리했는데 클라이언트가 재시도해 2건 생성
결과: 멱등하지 않은 RPC는 재시도 제외, 멱등 키 활용
flowchart TB
subgraph 문제[실무 문제]
P1[인증 중복] --> S1[인터셉터]
P2[단일 서버 과부하] --> S2[로드밸런싱]
P3[일시 장애] --> S3[재시도·데드라인]
P4[헬스 체크] --> S4[Health Protocol]
P5[멱등성 위반] --> S5[재시도 정책]
end
목표:
- 인터셉터: 인증·로깅·메트릭·트레이싱 완전 구현
- 로드밸런싱: round_robin, Kubernetes DNS 연동
- 데드라인: 전파, RPC 유형별 설정
- 재시도: 지수 백오프, jitter, 멱등성 고려
- 헬스 체크: gRPC 표준 Health Service 구현
요구 환경: C++17 이상, gRPC 1.50+, grpc-health-probe (선택)
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 인터셉터 완전 가이드
- 로드밸런싱 고급
- 데드라인·재시도 완전 구현
- gRPC 헬스 체크
- 완전한 고급 예제
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
- 정리
1. 인터셉터 완전 가이드
인터셉터란?
인터셉터는 RPC 호출 전후에 실행되는 미들웨어입니다. 인증, 로깅, 메트릭, 메타데이터 처리 등 크로스커팅 관심사를 비즈니스 로직과 분리합니다.
sequenceDiagram
participant App as 애플리케이션
participant Auth as 인증 인터셉터
participant Log as 로깅 인터셉터
participant Net as 네트워크
App->>Auth: RPC 호출
Auth->>Auth: 토큰 주입
Auth->>Log: 전달
Log->>Log: 시작 시간 기록
Log->>Net: 전달
Net->>Log: 응답
Log->>Log: 소요 시간 로깅
Log->>Auth: 전달
Auth->>App: 응답
클라이언트 인터셉터: 인증 토큰 자동 주입
gRPC C++ 인터셉터는 grpc::experimental 네임스페이스에 있습니다. Interceptor를 상속하고 Intercept를 오버라이드합니다.
#include <grpcpp/grpcpp.h>
#include <grpcpp/support/client_interceptor.h>
#include <grpcpp/support/interceptor.h>
using grpc::experimental::Interceptor;
using grpc::experimental::InterceptorBatchMethods;
using grpc::experimental::InterceptionHookPoints;
class AuthClientInterceptor : public Interceptor {
public:
explicit AuthClientInterceptor(const std::string& token) : token_(token) {}
void Intercept(InterceptorBatchMethods* methods) override {
if (methods->QueryInterceptionHookPoint(
InterceptionHookPoints::PRE_SEND_INITIAL_METADATA)) {
// 메타데이터에 authorization 추가
methods->AddSendInitialMetadata("authorization", "Bearer " + token_);
methods->AddSendInitialMetadata("x-client-version", "1.0");
}
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_;
};
// 사용: 채널 생성 시 인터셉터 등록
std::shared_ptr<grpc::Channel> CreateChannelWithAuth(
const std::string& target,
const std::string& token) {
std::vector<std::unique_ptr<
grpc::experimental::ClientInterceptorFactoryInterface>>
creators;
creators.push_back(std::make_unique<AuthClientInterceptorFactory>(token));
return grpc::experimental::CreateCustomChannelWithInterceptors(
target,
grpc::InsecureChannelCredentials(),
grpc::ChannelArguments(),
std::move(creators));
}
로깅 인터셉터: RPC 소요 시간·에러 기록
#include <chrono>
#include <iostream>
class LoggingClientInterceptor : public Interceptor {
public:
void Intercept(InterceptorBatchMethods* methods) override {
auto start = std::chrono::steady_clock::now();
if (methods->QueryInterceptionHookPoint(
InterceptionHookPoints::PRE_SEND_INITIAL_METADATA)) {
// RPC 메서드명 추출 (ClientRpcInfo에서)
std::cout << "[gRPC] RPC 시작" << std::endl;
}
methods->Proceed();
if (methods->QueryInterceptionHookPoint(
InterceptionHookPoints::POST_RECV_STATUS)) {
auto dur = std::chrono::steady_clock::now() - start;
auto status = methods->GetRecvStatus();
std::cout << "[gRPC] RPC 완료: "
<< (status.ok() ? "OK" : status.error_message())
<< ", 소요: " << dur.count() / 1000000 << "ms" << std::endl;
}
}
};
class LoggingInterceptorFactory
: public grpc::experimental::ClientInterceptorFactoryInterface {
public:
Interceptor* CreateClientInterceptor(
grpc::experimental::ClientRpcInfo* info) override {
return new LoggingClientInterceptor();
}
};
서버 인터셉터: 요청 ID 추적
#include <grpcpp/grpcpp.h>
#include <grpcpp/support/server_interceptor.h>
class RequestIdServerInterceptor : public grpc::experimental::Interceptor {
public:
void Intercept(grpc::experimental::InterceptorBatchMethods* methods) override {
if (methods->QueryInterceptionHookPoint(
grpc::experimental::InterceptionHookPoints::PRE_RECV_INITIAL_METADATA)) {
// 메타데이터에서 x-request-id 읽기 (실제로는 RecvInitialMetadata 훅에서)
}
methods->Proceed();
}
};
// 서버 빌더에 등록 (API는 gRPC 버전별로 확인)
// builder.experimental().SetInterceptorCreators(...);
인터셉터 체인: 순서와 조합
인터셉터는 등록 순서대로 실행됩니다. 애플리케이션에 가까운 것이 먼저, 네트워크에 가까운 것이 나중에 실행됩니다.
클라이언트: [Auth] → [Logging] → [Tracing] → [Network]
서버: [Network] → [Tracing] → [Logging] → [Auth] → [Service]
// 여러 인터셉터 등록
std::vector<std::unique_ptr<
grpc::experimental::ClientInterceptorFactoryInterface>>
creators;
creators.push_back(std::make_unique<AuthClientInterceptorFactory>(token));
creators.push_back(std::make_unique<LoggingInterceptorFactory>());
auto channel = grpc::experimental::CreateCustomChannelWithInterceptors(
target, creds, args, std::move(creators));
2. 로드밸런싱 고급
로드밸런싱 정책 비교
| 정책 | 동작 | 사용 사례 |
|---|---|---|
| pick_first (기본) | 주소 목록에서 첫 연결 성공 시 해당 서버만 사용 | 단일 서버, 개발 |
| round_robin | 연결된 서버들에 요청을 순환 분배 | 다중 서버, 수평 확장 |
| grpclb | 외부 로드밸런서 연동 (deprecated) | 레거시 |
| xDS | Envoy 등 xDS 호환 LB | 대규모 클러스터 |
round_robin 완전 구현
#include <grpcpp/grpcpp.h>
// DNS 기반: DNS가 여러 A 레코드 반환 시 round_robin 적용
grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
auto channel = grpc::CreateCustomChannel(
"dns:///my-grpc-service.namespace.svc.cluster.local:50051",
grpc::InsecureChannelCredentials(),
args);
// 명시적 다중 주소 (ipv4 스키마)
auto channel2 = grpc::CreateCustomChannel(
"ipv4:///127.0.0.1:50051,127.0.0.1:50052,127.0.0.1:50053",
grpc::InsecureChannelCredentials(),
args);
Kubernetes 서비스 연동
Kubernetes Service는 DNS로 여러 Pod IP를 반환합니다. dns:///를 사용하면 자동으로 여러 엔드포인트에 연결됩니다.
// Kubernetes 내부에서: Service 이름으로 연결
// my-grpc-service.namespace.svc.cluster.local → 여러 Pod IP
grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
std::string service_addr = "dns:///my-grpc-service:50051";
if (const char* ns = std::getenv("POD_NAMESPACE")) {
service_addr = "dns:///my-grpc-service." + std::string(ns) +
".svc.cluster.local:50051";
}
auto channel = grpc::CreateCustomChannel(
service_addr,
grpc::InsecureChannelCredentials(),
args);
Service Config로 고급 설정
{
"loadBalancingConfig": [{ "round_robin": {} }],
"methodConfig": [
{
"name": [{"service": "myapp.MyService"}],
"timeout": "30s",
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
}
}
]
}
로드밸런싱 동작 다이어그램
flowchart TB
subgraph 클라이언트
C[Stub]
end
subgraph 채널
LB[Load Balancer\nround_robin]
S1[Subchannel 1\nPod A]
S2[Subchannel 2\nPod B]
S3[Subchannel 3\nPod C]
LB --> S1
LB --> S2
LB --> S3
end
C -->|RPC 1| LB
LB -->|선택| S1
C -->|RPC 2| LB
LB -->|선택| S2
C -->|RPC 3| LB
LB -->|선택| S3
3. 데드라인·재시도 완전 구현
데드라인 전파
클라이언트가 설정한 데드라인은 서버에 전달됩니다. 서버는 context->IsCancelled()로 확인해 불필요한 작업을 중단할 수 있습니다.
// 클라이언트: RPC 유형별 데드라인
grpc::ClientContext context;
auto now = std::chrono::system_clock::now();
// 단순 조회: 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);
// 서버: 주기적 취소 확인
Status LongRpc(ServerContext* context, ...) override {
for (int i = 0; i < 10000; ++i) {
if (context->IsCancelled()) {
return Status(StatusCode::CANCELLED, "Deadline exceeded");
}
DoWork(i);
}
return Status::OK;
}
지수 백오프 + Jitter 재시도
동시 재시도 폭주(thundering herd) 방지를 위해 jitter를 추가합니다.
#include <grpcpp/grpcpp.h>
#include <chrono>
#include <random>
#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,
int deadline_seconds = 30) {
grpc::Status status;
std::random_device rd;
std::mt19937 gen(rd());
for (int attempt = 0; attempt < max_retries; ++attempt) {
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() +
std::chrono::seconds(deadline_seconds));
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 (attempt < max_retries - 1) {
// 지수 백오프: 100, 200, 400 ms
int delay_ms = base_delay_ms * (1 << attempt);
// Jitter: 0~50% 랜덤 추가
std::uniform_int_distribution<> dist(0, delay_ms / 2);
delay_ms += dist(gen);
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
}
}
return status;
}
멱등성 고려 재시도
bool IsRetryable(const std::string& method, grpc::StatusCode code) {
// 멱등하지 않은 메서드는 재시도 제외
static const std::unordered_set<std::string> non_idempotent = {
"CreateOrder", "SubmitPayment", "Transfer", "CreateUser"};
if (non_idempotent.count(method)) return false;
return code == grpc::StatusCode::UNAVAILABLE ||
code == grpc::StatusCode::DEADLINE_EXCEEDED ||
code == grpc::StatusCode::RESOURCE_EXHAUSTED ||
code == grpc::StatusCode::ABORTED;
}
4. gRPC 헬스 체크
gRPC Health Checking Protocol
gRPC 표준 헬스 체크 서비스입니다. grpc/grpc-health-probe와 Kubernetes liveness/readiness에서 사용합니다.
.proto 정의
// grpc/health/v1/health.proto (gRPC 표준)
syntax = "proto3";
package grpc.health.v1;
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1;
}
헬스 체크 서버 구현
gRPC C++는 EnableDefaultHealthCheckService로 기본 Health 서비스를 활성화합니다. 빈 서비스명("")은 서버 전체 상태를 나타냅니다.
#include <grpcpp/grpcpp.h>
#include <grpcpp/health_check_service_interface.h>
grpc::ServerBuilder builder;
builder.AddListeningPort("0.0.0.0:50051", grpc::InsecureServerCredentials());
builder.RegisterService(&my_service);
// 기본 Health 서비스 활성화 (gRPC 1.28+)
// Check(service="") 호출 시 서버 전체 상태 반환
grpc::EnableDefaultHealthCheckService(true);
auto server = builder.BuildAndStart();
grpc_health_probe 사용 (Kubernetes)
로컬에서 수동 테스트:
# grpc_health_probe 설치 (Kubernetes 이미지에 포함 또는 별도 설치)
grpc_health_probe -addr=localhost:50051
# 성공 시 exit 0, 실패 시 exit 1
Kubernetes 배포 설정:
# Kubernetes deployment
livenessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:50051"]
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
readinessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:50051"]
initialDelaySeconds: 2
periodSeconds: 5
수동 헬스 체크 서비스 구현
gRPC C++에서 표준 Health 서비스를 수동으로 구현하려면:
// health.grpc.pb.h 사용 (protoc로 생성)
#include "health.grpc.pb.h"
class HealthServiceImpl : public grpc::health::v1::Health::Service {
public:
grpc::Status Check(
grpc::ServerContext* context,
const grpc::health::v1::HealthCheckRequest* request,
grpc::health::v1::HealthCheckResponse* response) override {
// 서비스별 상태 확인 (선택)
if (!request->service().empty()) {
if (IsServiceHealthy(request->service())) {
response->set_status(
grpc::health::v1::HealthCheckResponse::SERVING);
} else {
response->set_status(
grpc::health::v1::HealthCheckResponse::NOT_SERVING);
}
} else {
// 빈 서비스명 = 전체 서버 상태
response->set_status(
grpc::health::v1::HealthCheckResponse::SERVING);
}
return grpc::Status::OK;
}
private:
bool IsServiceHealthy(const std::string& service) {
// DB 연결, 캐시 등 확인
return true;
}
};
헬스 체크 시퀀스
sequenceDiagram
participant K8s as Kubernetes
participant Probe as grpc_health_probe
participant Svc as gRPC 서버
K8s->>Probe: livenessProbe 실행
Probe->>Svc: Health.Check(service="")
Svc->>Svc: 상태 확인
Svc->>Probe: SERVING
Probe->>K8s: 성공 (exit 0)
5. 완전한 고급 예제
예제 1: 인터셉터 + 로드밸런싱 + 재시도 클라이언트
#include <grpcpp/grpcpp.h>
#include "my_service.grpc.pb.h"
#include <memory>
#include <string>
class ProductionGrpcClient {
public:
ProductionGrpcClient(const std::string& service_addr,
const std::string& auth_token) {
grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
args.SetInt("grpc.keepalive_time_ms", 10000);
args.SetInt("grpc.keepalive_timeout_ms", 5000);
std::vector<std::unique_ptr<
grpc::experimental::ClientInterceptorFactoryInterface>>
creators;
creators.push_back(
std::make_unique<AuthClientInterceptorFactory>(auth_token));
creators.push_back(std::make_unique<LoggingInterceptorFactory>());
channel_ = grpc::experimental::CreateCustomChannelWithInterceptors(
service_addr,
grpc::InsecureChannelCredentials(),
args,
std::move(creators));
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, 3, 100, 30);
}
private:
template <typename Req, typename Res>
grpc::Status CallWithRetry(
std::function<grpc::Status(grpc::ClientContext*, const Req&, Res*)> rpc,
const Req& req, Res* res, int max_retries, int base_delay_ms,
int deadline_sec);
std::shared_ptr<grpc::Channel> channel_;
std::unique_ptr<myapp::MyService::Stub> stub_;
};
예제 2: 그레이스풀 셧다운
#include <grpcpp/grpcpp.h>
#include <csignal>
grpc::Server* g_server = nullptr;
void SignalHandler(int signum) {
if (g_server) {
g_server->Shutdown(); // 새 RPC 거부, 기존 RPC 완료 대기
}
}
int main() {
grpc::ServerBuilder builder;
builder.AddListeningPort("0.0.0.0:50051", grpc::InsecureServerCredentials());
builder.RegisterService(&service);
g_server = builder.BuildAndStart().release();
std::signal(SIGINT, SignalHandler);
std::signal(SIGTERM, SignalHandler);
g_server->Wait(); // 모든 스레드 종료 대기
delete g_server;
return 0;
}
예제 3: 환경별 설정
struct GrpcClientConfig {
std::string address;
std::string lb_policy;
int deadline_seconds;
int max_retries;
bool use_tls;
};
GrpcClientConfig LoadConfig() {
if (std::getenv("KUBERNETES_SERVICE_HOST")) {
return {
"dns:///my-grpc-service:50051",
"round_robin",
30,
3,
false // 클러스터 내부는 종종 plaintext
};
}
if (std::getenv("STAGING")) {
return {"localhost:50051", "pick_first", 10, 2, false};
}
return {"my-service.example.com:443", "round_robin", 30, 3, true};
}
6. 자주 발생하는 에러와 해결법
문제 1: round_robin이 동작하지 않음
증상: 다중 서버를 두었는데 한 서버에만 요청이 감.
원인: pick_first가 기본값이거나, 단일 IP만 반환되는 주소 사용.
해결법:
// ❌ 잘못된 예
auto channel = grpc::CreateChannel("localhost:50051", ...);
// ✅ 올바른 예
grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
auto channel = grpc::CreateCustomChannel(
"dns:///my-service:50051", // DNS가 여러 IP 반환해야 함
grpc::InsecureChannelCredentials(),
args);
문제 2: 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);
문제 3: 재시도 시 중복 실행 (멱등성 위반)
증상: 주문 생성 RPC를 재시도했는데 주문이 2건 생성됨.
원인: 생성·수정 RPC는 멱등하지 않음.
해결법:
// ✅ 멱등한 RPC만 재시도
bool IsRetryable(const std::string& method, grpc::StatusCode code) {
if (method == "CreateOrder" || method == "SubmitPayment") {
return false;
}
return code == grpc::StatusCode::UNAVAILABLE ||
code == grpc::StatusCode::DEADLINE_EXCEEDED;
}
문제 4: 인터셉터 “experimental” API 컴파일 에러
증상: grpc::experimental::Interceptor를 찾을 수 없음.
원인: gRPC 버전에 따라 API 위치가 다름.
해결법:
// 필요한 헤더
#include <grpcpp/support/client_interceptor.h>
#include <grpcpp/support/interceptor.h>
// vcpkg로 버전 확인
// vcpkg list grpc
// grpc 1.50 이상 권장
문제 5: grpc_health_probe 연결 실패
증상: Kubernetes에서 grpc_health_probe가 실패함.
원인: 서버에 Health 서비스가 등록되지 않음.
해결법:
// gRPC 내장 Health 서비스 활성화
grpc::EnableDefaultHealthCheckService(true);
// 또는 수동으로 Health 서비스 구현 후 RegisterService
문제 6: 스트리밍 시 “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;
if (!stream->Write(chunk)) break;
}
문제 7: 채널/스텁 매 요청마다 생성
증상: 초당 100 RPC 시 CPU·메모리 사용량 급증.
원인: 채널 생성 비용이 큼.
해결법:
// ❌ 매 요청마다 새 채널
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(...);
}
7. 베스트 프랙티스
1. 모든 RPC에 데드라인 설정
// ✅ 필수
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(30));
stub->MyRpc(&context, req, &res);
2. 채널·스텁 싱글톤 또는 의존성 주입
// ✅ 앱 전체에서 채널 1개 재사용
class GrpcClientPool {
public:
static std::shared_ptr<grpc::Channel> GetChannel(
const std::string& service) {
std::lock_guard<std::mutex> lock(mutex_);
if (!channels_[service]) {
channels_[service] = CreateChannel(service);
}
return channels_[service];
}
private:
static std::unordered_map<std::string, std::shared_ptr<grpc::Channel>>
channels_;
static std::mutex mutex_;
};
3. Keepalive 설정 (장시간 유휴 연결)
grpc::ChannelArguments args;
args.SetInt("grpc.keepalive_time_ms", 10000); // 10초마다 ping
args.SetInt("grpc.keepalive_timeout_ms", 5000);
args.SetInt("grpc.keepalive_permit_without_calls", 1);
4. 메시지 크기 제한
args.SetInt("grpc.max_send_message_length", 64 * 1024 * 1024); // 64MB
args.SetInt("grpc.max_receive_message_length", 64 * 1024 * 1024);
5. 에러 처리 일관성
grpc::Status status = stub->MyRpc(&context, req, &res);
if (!status.ok()) {
switch (status.error_code()) {
case grpc::StatusCode::UNAVAILABLE:
// 재시도 또는 폴백
break;
case grpc::StatusCode::DEADLINE_EXCEEDED:
// 타임아웃 처리
break;
case grpc::StatusCode::UNAUTHENTICATED:
// 토큰 갱신
break;
default:
// 로깅 후 에러 반환
break;
}
}
6. 메타데이터 트레이싱
// 클라이언트: 요청 ID 주입
context.AddMetadata("x-request-id", GenerateRequestId());
context.AddMetadata("x-trace-id", GetCurrentTraceId());
// 서버: 로깅에 활용
auto it = context->client_metadata().find("x-request-id");
if (it != context->client_metadata().end()) {
std::string req_id(it->second.begin(), it->second.end());
LOG(INFO) << "[" << req_id << "] " << method_name;
}
8. 프로덕션 패턴
패턴 1: Circuit Breaker (개념)
연속 실패 시 일정 시간 호출 중단 후 재시도.
// 개념: 실패 횟수 카운트, 임계값 초과 시 OPEN 상태로 전환
// 일정 시간 후 HALF_OPEN으로 전환해 테스트 요청
// 성공 시 CLOSED로 복구
패턴 2: 그레이스풀 셧다운
void GracefulShutdown(grpc::Server* server) {
server->Shutdown(); // 새 RPC 거부
server->Wait(); // 진행 중 RPC 완료 대기
}
패턴 3: Kubernetes 배포 체크리스트
- [ ] round_robin 로드밸런싱 (다중 Pod)
- [ ] livenessProbe: grpc_health_probe
- [ ] readinessProbe: grpc_health_probe
- [ ] resource limits 설정
- [ ] 그레이스풀 셧다운 (preStop 훅)
- [ ] HPA (Horizontal Pod Autoscaler) 고려
패턴 4: 메트릭 수집
// 인터셉터에서 RPC 지연, 에러율 수집
// Prometheus exposition format으로 노출
// 또는 OpenTelemetry 연동
패턴 5: 다중 리전·폴백
// 1차: 로컬 리전
// 실패 시: 원격 리전으로 재시도
std::vector<std::string> endpoints = {
"dns:///my-service.local:50051",
"dns:///my-service.remote:50051"};
9. 구현 체크리스트
인터셉터
- 인증·로깅 등 크로스커팅 관심사 식별
-
ClientInterceptorFactoryInterface구현 -
CreateCustomChannelWithInterceptors사용 - 인터셉터 순서 검토
로드밸런싱
-
SetLoadBalancingPolicyName("round_robin")설정 - DNS 또는 다중 주소로 여러 백엔드 노출
- Kubernetes:
dns:///service-name:port사용
데드라인·재시도
- 모든 RPC에
set_deadline설정 - 재시도 가능 에러만 재시도
- 지수 백오프 + jitter 적용
- 멱등하지 않은 RPC 재시도 제외
헬스 체크
- gRPC Health Service 등록
- Kubernetes liveness/readiness에 grpc_health_probe 설정
- 서비스별 상태 확인 (선택)
프로덕션
- 그레이스풀 셧다운
- Keepalive 설정
- 메시지 크기 제한
- 메타데이터 트레이싱
- 채널·스텁 재사용
10. 정리
| 항목 | 요약 |
|---|---|
| 인터셉터 | 인증·로깅·메트릭 중앙 처리, experimental API, CreateCustomChannelWithInterceptors |
| 로드밸런싱 | round_robin, ChannelArguments, dns:/// 다중 주소 |
| 데드라인 | set_deadline, IsCancelled, RPC 유형별 분리 |
| 재시도 | 지수 백오프, jitter, 멱등성 고려 |
| 헬스 체크 | gRPC Health Protocol, grpc_health_probe, Kubernetes 프로브 |
| 베스트 프랙티스 | 데드라인 필수, 채널 재사용, Keepalive, 에러 처리 |
| 프로덕션 | 그레이스풀 셧다운, 메트릭, 환경별 설정 |
핵심 원칙:
- 인터셉터로 비즈니스 로직과 인증·로깅 분리
- 다중 서버 환경에서는 round_robin 필수
- 데드라인과 재시도로 일시 장애에 대비
- 멱등하지 않은 RPC는 재시도 제외
- Kubernetes 배포 시 grpc_health_probe 연동
자주 묻는 질문 (FAQ)
Q. gRPC Health Checking Protocol과 HTTP 헬스 체크 차이는?
A. gRPC Health는 gRPC 서비스로 구현되어, 서버가 실제로 RPC를 처리할 수 있는지 확인합니다. HTTP /health는 프로세스만 확인할 수 있습니다.
Q. 인터셉터 API가 experimental인데 프로덕션에서 써도 되나요?
A. gRPC C++ 인터셉터는 수년간 사용되어 왔으나, API 변경 가능성이 있습니다. 버전 고정 및 테스트를 권장합니다.
Q. round_robin과 pick_first 성능 차이는?
A. 단일 서버에서는 차이 없습니다. 다중 서버에서 round_robin이 연결을 분산해 처리량을 높입니다.
한 줄 요약: 인터셉터·로드밸런싱·데드라인·재시도·헬스체크로 프로덕션급 gRPC를 구축할 수 있습니다.
다음 글: C++ 시리즈 목차에서 다음 주제를 확인하세요.
이전 글: gRPC 마스터(#52-2)
관련 글
- C++ gRPC 기초 완벽 가이드 | Protocol Buffers·Unary·스트리밍·실전 문제 해결
- C++ gRPC 마스터 | 스트리밍·인터셉터·로드밸런싱 [#52-2]
- C++ Protocol Buffers 완벽 가이드 | 직렬화·스키마 진화·성능 최적화·프로덕션 패턴
- C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴