[2026] 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는 재시도 제외, 멱등 키 활용
아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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 호출 전후에 실행되는 미들웨어입니다. 인증, 로깅, 메트릭, 메타데이터 처리 등 크로스커팅 관심사를 비즈니스 로직과 분리합니다. 다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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를 오버라이드합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 소요 시간·에러 기록
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 추적
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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]
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 여러 인터셉터 등록
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 완전 구현
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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:///를 사용하면 자동으로 여러 엔드포인트에 연결됩니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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로 고급 설정
다음은 json를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
{
"loadBalancingConfig": [{ "round_robin": {} }],
"methodConfig": [
{
"name": [{"service": "myapp.MyService"}],
"timeout": "30s",
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
}
}
]
}
로드밸런싱 동작 다이어그램
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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()로 확인해 불필요한 작업을 중단할 수 있습니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 클라이언트: 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를 추가합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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;
}
멱등성 고려 재시도
아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 정의
다음은 protobuf를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 서비스를 활성화합니다. 빈 서비스명("")은 서버 전체 상태를 나타냅니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 배포 설정: 아래 코드는 yaml를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 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 서비스를 수동으로 구현하려면: 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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;
}
};
헬스 체크 시퀀스
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 인터셉터 + 로드밸런싱 + 재시도 클라이언트
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 그레이스풀 셧다운
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 환경별 설정
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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만 반환되는 주소 사용.
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 예
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가 너무 자주 발생
증상: 서버는 정상인데 클라이언트에서 타임아웃. 원인: 데드라인이 너무 짧거나, 서버 처리 시간이 김. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 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는 멱등하지 않음. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 멱등한 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 위치가 다름.
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 필요한 헤더
#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 시도. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 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·메모리 사용량 급증. 원인: 채널 생성 비용이 큼. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 매 요청마다 새 채널
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에 데드라인 설정
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 필수
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(30));
stub->MyRpc(&context, req, &res);
2. 채널·스텁 싱글톤 또는 의존성 주입
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 앱 전체에서 채널 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 설정 (장시간 유휴 연결)
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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. 에러 처리 일관성
다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 메타데이터 트레이싱
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 클라이언트: 요청 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: 그레이스풀 셧다운
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void GracefulShutdown(grpc::Server* server) {
server->Shutdown(); // 새 RPC 거부
server->Wait(); // 진행 중 RPC 완료 대기
}
패턴 3: Kubernetes 배포 체크리스트
아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
- [ ] 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: 다중 리전·폴백
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 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 변경 가능성이 있습니다. 버전 고정 및 테스트를 권장합니다.