C++ gRPC 고급 완벽 가이드 | 인터셉터·로드밸런싱·데드라인·재시도·헬스체크 [#52-3]

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++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 인터셉터 완전 가이드
  2. 로드밸런싱 고급
  3. 데드라인·재시도 완전 구현
  4. gRPC 헬스 체크
  5. 완전한 고급 예제
  6. 자주 발생하는 에러와 해결법
  7. 베스트 프랙티스
  8. 프로덕션 패턴
  9. 구현 체크리스트
  10. 정리

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)레거시
xDSEnvoy 등 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, 에러 처리
프로덕션그레이스풀 셧다운, 메트릭, 환경별 설정

핵심 원칙:

  1. 인터셉터로 비즈니스 로직과 인증·로깅 분리
  2. 다중 서버 환경에서는 round_robin 필수
  3. 데드라인과 재시도로 일시 장애에 대비
  4. 멱등하지 않은 RPC는 재시도 제외
  5. 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 호출·연결 풀·타임아웃·프로덕션 패턴
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3