본문으로 건너뛰기 C++ gRPC 고급 완벽 가이드 | 인터셉터·로드밸런싱·데드라인·재시도·헬스체크

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

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

들어가며: “프로덕션 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 (선택)

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, 인터셉터, 로드밸런싱, 헬스체크, 재시도, 마이크로서비스 등으로 검색하시면 이 글이 도움이 됩니다.