C++ gRPC 기초 완벽 가이드 | Protocol Buffers·Unary·스트리밍·실전 문제 해결

C++ gRPC 기초 완벽 가이드 | Protocol Buffers·Unary·스트리밍·실전 문제 해결

이 글의 핵심

C++ 마이크로서비스 통신에서 JSON 직렬화 병목·스키마 불일치·대용량 스트리밍 문제를 겪는다면? Protocol Buffers 정의부터 Unary RPC, 서버/클라이언트/양방향 스트리밍까지 완전한 예제, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴을 실전 코드로 구현합니다.

들어가며: “마이크로서비스 통신이 병목이에요”

HTTP/JSON만으로는 부족한 이유

마이크로서비스 아키텍처에서 서비스 간 통신에 REST + JSON을 쓰다 보면, 직렬화 비용·스키마 관리·스트리밍 구현에서 한계에 부딪힙니다. gRPC(Google이 만든 고성능 RPC 프레임워크)와 Protocol Buffers(바이너리 직렬화 포맷)는 이런 문제를 해결합니다.

이 글에서 다루는 것:

  • Protocol Buffers: .proto 문법·메시지·서비스 정의·C++ 코드 생성
  • Unary RPC: 단일 요청-응답 (가장 기본)
  • 서버 스트리밍: 클라이언트 1회 요청 → 서버가 여러 응답
  • 클라이언트 스트리밍: 클라이언트가 여러 요청 → 서버 1회 응답
  • 양방향 스트리밍: 클라이언트·서버가 동시에 송수신
  • 자주 발생하는 에러와 해결법
  • 베스트 프랙티스·프로덕션 패턴

실제 문제 시나리오

시나리오 1: JSON 직렬화로 CPU가 100%

상황: C++ 결제 서비스가 초당 10,000건 요청 처리 시 CPU 80% 사용
문제: JSON 파싱·생성이 CPU를 많이 소모, 대용량 객체일수록 심함
결과: Protobuf 바이너리 직렬화로 전환 → CPU 40%로 감소, 처리량 2배

시나리오 2: 스키마 없이 필드명 오타로 장애

상황: "user_id" vs "userId" 필드명 불일치로 클라이언트·서버 간 데이터 누락
문제: JSON은 런타임에만 검증, 타입 안전성 없음
결과: .proto로 스키마 정의 → 컴파일 타임 검증, 필드 번호로 호환성 유지

시나리오 3: 대용량 로그 스트리밍이 끊김

상황: 수백 MB 로그를 HTTP로 전송 시 타임아웃·메모리 부족
문제: 단일 요청-응답 모델로는 스트리밍 불가
결과: gRPC 서버 스트리밍으로 청크 단위 전송 → 안정적 전달

시나리오 4: REST로 양방향 실시간 통신 구현이 복잡

상황: 채팅·게임 서버처럼 클라이언트·서버가 동시에 메시지 송수신
문제: REST는 요청-응답만 지원, WebSocket은 별도 구현 필요
결과: gRPC 양방향 스트리밍 → 단일 연결로 양방향 통신

시나리오 5: 연결 타임아웃·재시도 로직 직접 구현

상황: 서버 재시작 시 일시적 UNAVAILABLE로 전체 실패
문제: HTTP 클라이언트에서 재시도·백오프를 직접 구현해야 함
결과: gRPC 데드라인·StatusCode 기반 재시도 패턴 표준화
flowchart TB
    subgraph 문제[실무 문제]
        P1[JSON 직렬화 병목] --> S1[Protobuf]
        P2[스키마 불일치] --> S2[.proto 스키마]
        P3[대용량 전송] --> S3[스트리밍]
        P4[양방향 통신] --> S4[양방향 스트리밍]
    end

이 글을 읽으면:

  • Protocol Buffers로 스키마를 정의하고 C++ 코드를 생성할 수 있습니다.
  • Unary·서버·클라이언트·양방향 스트리밍을 완전한 예제로 구현할 수 있습니다.
  • 자주 발생하는 에러를 진단하고 해결할 수 있습니다.
  • 프로덕션 환경에 맞는 패턴을 적용할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 환경 설정
  2. Protocol Buffers 기초
  3. Unary RPC 완전한 예제
  4. 서버 스트리밍
  5. 클라이언트 스트리밍
  6. 양방향 스트리밍
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 구현 체크리스트
  11. 정리

1. 환경 설정

필수 의존성

항목버전비고
C++C++14 이상C++17 권장
gRPC1.50+vcpkg 또는 소스 빌드
Protocol Buffers3.21+gRPC와 버전 호환 확인
CMake3.16+FindPackage 지원

vcpkg로 설치

# vcpkg로 gRPC 설치 (Protobuf 포함)
vcpkg install grpc

# protoc 설치 (macOS)
brew install protobuf

protoc로 코드 생성

# 코드 생성
protoc --cpp_out=./generated --grpc_out=./generated \
  --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` \
  -I./proto \
  proto/echo_service.proto

생성되는 파일:

  • echo_service.pb.h, echo_service.pb.cc: 메시지 클래스
  • echo_service.grpc.pb.h, echo_service.grpc.pb.cc: Stub·Service 클래스

2. Protocol Buffers 기초

.proto와 코드 생성

Protocol BuffersIDL(Interface Definition Language)로 메시지와 서비스를 정의하고, protoc로 C++·Java·Go 등 여러 언어의 코드를 생성합니다. 필드 번호는 스키마 호환성에 중요합니다. 기존 번호를 바꾸지 않고 새 필드만 추가하면 하위 호환이 유지됩니다.

gRPC + Protobuf 아키텍처

flowchart TB
    subgraph Client[클라이언트]
        C1[.proto 정의]
        C2[Stub 생성]
        C3[비즈니스 로직]
        C1 --> C2 --> C3
    end
    subgraph Server[서버]
        S1[.proto 정의]
        S2[Service 베이스]
        S3[구현체 Override]
        S1 --> S2 --> S3
    end
    C3 -->|HTTP/2 + Protobuf| S3

완전한 .proto 예시 (4가지 RPC 유형 포함)

syntax = “proto3”는 Protocol Buffers 3 문법을 사용합니다. message는 직렬화될 필드와 번호(1, 2, …)를 정의합니다. 번호는 스키마 호환에 중요해, 기존 번호를 바꾸지 않고 필드만 추가하면 하위 호환이 유지됩니다.

syntax = "proto3";
package echo;

// Unary RPC: 요청 1개 → 응답 1개
// 서버 스트리밍: 요청 1개 → 응답 스트림
// 클라이언트 스트리밍: 요청 스트림 → 응답 1개
// 양방향 스트리밍: 요청 스트림 ↔ 응답 스트림

service EchoService {
  // Unary: 가장 기본적인 요청-응답
  rpc Echo(EchoRequest) returns (EchoResponse);

  // 서버 스트리밍: 클라이언트 1회 요청 → 서버가 여러 응답
  rpc ServerStream(EchoRequest) returns (stream EchoResponse);

  // 클라이언트 스트리밍: 클라이언트가 여러 요청 → 서버 1회 응답
  rpc ClientStream(stream EchoRequest) returns (EchoResponse);

  // 양방향 스트리밍: 클라이언트·서버가 동시에 송수신
  rpc BidirectionalStream(stream EchoRequest) returns (stream EchoResponse);
}

message EchoRequest {
  string message = 1;
  int32 sequence = 2;
}

message EchoResponse {
  string message = 1;
  int32 sequence = 2;
}

oneof·enum·import (고급)

syntax = "proto3";
package myapp;

// enum: 숫자로 직렬화되어 효율적
enum UserStatus {
  UNKNOWN = 0;
  ACTIVE = 1;
  INACTIVE = 2;
  BANNED = 3;
}

// oneof: 여러 필드 중 하나만 설정
message Event {
  oneof payload {
    string message = 1;
    int32 code = 2;
    bytes binary_data = 3;
  }
}

// 다른 .proto 파일 import
// import "google/protobuf/timestamp.proto";

CMake 통합

cmake_minimum_required(VERSION 3.16)
project(grpc_example LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

find_package(Protobuf REQUIRED)
find_package(gRPC CONFIG REQUIRED)

set(PROTO_PATH "${CMAKE_CURRENT_SOURCE_DIR}/proto")
set(GENERATED_PROTOBUF_PATH "${CMAKE_BINARY_DIR}/generated")
file(MAKE_DIRECTORY ${GENERATED_PROTOBUF_PATH})

set(PROTO_FILES echo_service.proto)
foreach(PROTO_FILE ${PROTO_FILES})
  get_filename_component(PROTO_NAME ${PROTO_FILE} NAME_WE)
  set(PROTO_FULL "${PROTO_PATH}/${PROTO_FILE}")
  add_custom_command(
    OUTPUT
      "${GENERATED_PROTOBUF_PATH}/${PROTO_NAME}.pb.cc"
      "${GENERATED_PROTOBUF_PATH}/${PROTO_NAME}.pb.h"
      "${GENERATED_PROTOBUF_PATH}/${PROTO_NAME}.grpc.pb.cc"
      "${GENERATED_PROTOBUF_PATH}/${PROTO_NAME}.grpc.pb.h"
    COMMAND protobuf::protoc
    ARGS --cpp_out=${GENERATED_PROTOBUF_PATH}
         --grpc_out=${GENERATED_PROTOBUF_PATH}
         --plugin=protoc-gen-grpc=$<TARGET_FILE:gRPC::grpc_cpp_plugin>
         -I${PROTO_PATH}
         ${PROTO_FULL}
    DEPENDS ${PROTO_FULL}
  )
  list(APPEND GENERATED_SOURCES
    "${GENERATED_PROTOBUF_PATH}/${PROTO_NAME}.pb.cc"
    "${GENERATED_PROTOBUF_PATH}/${PROTO_NAME}.grpc.pb.cc"
  )
endforeach()

add_executable(grpc_server server.cc ${GENERATED_SOURCES})
target_include_directories(grpc_server PRIVATE ${GENERATED_PROTOBUF_PATH})
target_link_libraries(grpc_server PRIVATE
  protobuf::libprotobuf
  gRPC::grpc++
  gRPC::grpc++_reflection
)

add_executable(grpc_client client.cc ${GENERATED_SOURCES})
target_include_directories(grpc_client PRIVATE ${GENERATED_PROTOBUF_PATH})
target_link_libraries(grpc_client PRIVATE
  protobuf::libprotobuf
  gRPC::grpc++
  gRPC::grpc++_reflection
)

3. Unary RPC 완전한 예제

Unary RPC란?

Unary RPC는 가장 기본적인 패턴입니다. 클라이언트가 요청 1개를 보내면 서버가 응답 1개를 반환합니다. REST의 GET/POST와 유사합니다.

gRPC 요청-응답 시퀀스

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버
    C->>S: EchoRequest (Protobuf 직렬화)
    S->>S: 비즈니스 로직
    S->>C: EchoResponse + Status
    Note over C: status.ok() 확인

서버 구현 (동기)

#include <grpcpp/grpcpp.h>
#include <iostream>
#include <memory>
#include <string>

#include "echo_service.grpc.pb.h"

using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using grpc::StatusCode;

class EchoServiceImpl final : public echo::EchoService::Service {
public:
    // Unary RPC: 요청 1개 → 응답 1개
    Status Echo(ServerContext* context,
                const echo::EchoRequest* request,
                echo::EchoResponse* response) override {
        // 1. 요청 검증
        if (request->message().empty()) {
            return Status(StatusCode::INVALID_ARGUMENT,
                         "message must not be empty");
        }

        // 2. 비즈니스 로직: 메시지 그대로 반환
        response->set_message(request->message());
        response->set_sequence(request->sequence());

        return Status::OK;
    }
};

void RunServer() {
    std::string server_address("0.0.0.0:50051");
    EchoServiceImpl service;

    ServerBuilder builder;
    builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
    builder.RegisterService(&service);

    std::unique_ptr<Server> server(builder.BuildAndStart());
    std::cout << "Server listening on " << server_address << std::endl;
    server->Wait();  // 블로킹, Ctrl+C로 종료
}

int main() {
    RunServer();
    return 0;
}

클라이언트 구현 (동기)

#include <grpcpp/grpcpp.h>
#include <iostream>
#include <memory>
#include <string>

#include "echo_service.grpc.pb.h"

using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;

class EchoClient {
public:
    EchoClient(std::shared_ptr<Channel> channel)
        : stub_(echo::EchoService::NewStub(channel)) {}

    std::string Echo(const std::string& message) {
        echo::EchoRequest request;
        request.set_message(message);
        request.set_sequence(1);

        echo::EchoResponse response;
        ClientContext context;

        // 데드라인 설정: 5초 내 응답 없으면 실패
        context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));

        Status status = stub_->Echo(&context, request, &response);

        if (status.ok()) {
            return response.message();
        } else {
            std::cerr << "RPC failed: " << status.error_code()
                      << " - " << status.error_message() << std::endl;
            return "";
        }
    }

private:
    std::unique_ptr<echo::EchoService::Stub> stub_;
};

int main() {
    auto channel = grpc::CreateChannel(
        "localhost:50051",
        grpc::InsecureChannelCredentials());

    if (channel->WaitForConnected(
            std::chrono::system_clock::now() + std::chrono::seconds(5))) {
        EchoClient client(channel);
        std::string reply = client.Echo("Hello, gRPC!");
        std::cout << "Echo received: " << reply << std::endl;
    } else {
        std::cerr << "Failed to connect to server" << std::endl;
        return 1;
    }
    return 0;
}

실행 순서

# 터미널 1: 서버 실행
./grpc_server

# 터미널 2: 클라이언트 실행
./grpc_client

예상 출력 (클라이언트): Echo received: Hello, gRPC!


4. 서버 스트리밍

서버 스트리밍이란?

클라이언트가 1회 요청을 보내면 서버가 여러 응답을 스트리밍합니다. 로그 조회, 대용량 데이터 청크 전송, 실시간 알림 등에 적합합니다.

시퀀스 다이어그램

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버
    C->>S: EchoRequest (1회)
    loop 스트리밍
        S->>C: EchoResponse #1
        S->>C: EchoResponse #2
        S->>C: EchoResponse #3
    end
    S->>C: stream 완료

서버 측 구현

Status ServerStream(ServerContext* context,
                    const echo::EchoRequest* request,
                    grpc::ServerWriter<echo::EchoResponse>* writer) override {
    for (int i = 0; i < 5; ++i) {
        // 클라이언트 연결 끊김 확인 (긴 작업 시 필수)
        if (context->IsCancelled()) {
            return Status(StatusCode::CANCELLED, "Client disconnected");
        }

        echo::EchoResponse response;
        response.set_message(request->message() + " #" + std::to_string(i));
        response.set_sequence(i);

        // Write 실패 시 (클라이언트 연결 끊김 등) 즉시 종료
        if (!writer->Write(response)) {
            break;
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    return Status::OK;
}

클라이언트 측 구현

void ServerStream(const std::string& message) {
    echo::EchoRequest request;
    request.set_message(message);

    ClientContext context;
    context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(10));

    auto reader = stub_->ServerStream(&context, request);
    echo::EchoResponse response;

    while (reader->Read(&response)) {
        std::cout << "Received: " << response.message() << std::endl;
    }

    Status status = reader->Finish();
    if (!status.ok()) {
        std::cerr << "Stream failed: " << status.error_message() << std::endl;
    }
}

주의점:

  • reader->Read(): 블로킹, 스트림이 끝나면 false 반환
  • reader->Finish(): 최종 Status 확인 필수
  • 서버: IsCancelled(), Write() 반환값 확인

5. 클라이언트 스트리밍

클라이언트 스트리밍이란?

클라이언트가 여러 요청을 스트리밍하고, 서버가 1회 응답을 반환합니다. 대용량 파일 업로드, 배치 데이터 전송 등에 적합합니다.

시퀀스 다이어그램

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버
    loop 스트리밍
        C->>S: EchoRequest #1
        C->>S: EchoRequest #2
        C->>S: EchoRequest #3
    end
    C->>S: WritesDone()
    S->>C: EchoResponse (1회)

서버 측 구현

Status ClientStream(ServerContext* context,
                    grpc::ServerReader<echo::EchoRequest>* reader,
                    echo::EchoResponse* response) override {
    std::string concatenated;
    echo::EchoRequest request;
    int count = 0;

    while (reader->Read(&request)) {
        concatenated += request.message() + " ";
        ++count;
    }

    response->set_message(concatenated);
    response->set_sequence(count);
    return Status::OK;
}

클라이언트 측 구현

std::string ClientStream(const std::vector<std::string>& messages) {
    ClientContext context;
    context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(10));

    echo::EchoResponse response;
    auto writer = stub_->ClientStream(&context, &response);

    for (const auto& msg : messages) {
        echo::EchoRequest request;
        request.set_message(msg);
        if (!writer->Write(request)) break;
    }
    writer->WritesDone();  // 더 이상 보낼 메시지 없음

    Status status = writer->Finish();
    if (!status.ok()) return "";
    return response.message();
}

주의점:

  • WritesDone(): 클라이언트가 더 이상 보낼 메시지가 없을 때 호출
  • writer->Finish(): 서버 응답 수신 및 최종 Status 확인

6. 양방향 스트리밍

양방향 스트리밍이란?

클라이언트와 서버가 동시에 읽기와 쓰기를 수행합니다. 채팅, 실시간 협업, 게임 서버 등에 적합합니다.

시퀀스 다이어그램

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버
    par 동시 읽기/쓰기
        C->>S: msg1
        S->>C: response1
        C->>S: msg2
        S->>C: response2
    end
    Note over C,S: Read/Write 순서는 독립적

서버 측 구현

Status BidirectionalStream(
    ServerContext* context,
    grpc::ServerReaderWriter<echo::EchoResponse, echo::EchoRequest>* stream) override {
    echo::EchoRequest request;
    while (stream->Read(&request)) {
        if (context->IsCancelled()) {
            return Status(StatusCode::CANCELLED, "Client disconnected");
        }

        echo::EchoResponse response;
        response.set_message(request.message());
        response.set_sequence(request.sequence());

        if (!stream->Write(response)) {
            break;  // 클라이언트 연결 끊김
        }
    }
    return Status::OK;
}

클라이언트 측 구현 (읽기/쓰기 스레드 분리)

void BidirectionalStream() {
    ClientContext context;
    context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(60));

    auto stream = stub_->BidirectionalStream(&context);

    // 쓰기 스레드: 사용자 입력을 서버로 전송
    std::thread writer([&stream]() {
        std::string line;
        while (std::getline(std::cin, line)) {
            echo::EchoRequest request;
            request.set_message(line);
            request.set_sequence(0);
            if (!stream->Write(request)) break;
        }
        stream->WritesDone();
    });

    // 읽기 스레드: 서버 메시지 수신
    std::thread reader([&stream]() {
        echo::EchoResponse response;
        while (stream->Read(&response)) {
            std::cout << "Server: " << response.message() << std::endl;
        }
    });

    writer.join();
    reader.join();

    Status status = stream->Finish();
    if (!status.ok()) {
        std::cerr << "Bidirectional stream failed: " << status.error_message() << std::endl;
    }
}

주의점:

  • 읽기와 쓰기를 같은 스레드에서 블로킹하면 데드락 가능 → 스레드 분리
  • Write 반환값 확인: false 시 클라이언트 연결 끊김
  • IsCancelled() 주기적 확인 (장시간 루프)

7. 자주 발생하는 에러와 해결법

문제 1: “Connection refused” / “UNAVAILABLE”

원인: 서버가 실행 중이 아니거나, 잘못된 주소·포트, 방화벽 차단

해결법:

# 서버 포트 리스닝 확인
netstat -an | grep 50051
# 또는
lsof -i :50051
// ❌ 잘못된 예: 채널 생성만 하고 연결 확인 안 함
auto channel = grpc::CreateChannel("localhost:50051",
                                   grpc::InsecureChannelCredentials());

// ✅ 올바른 예: 연결 상태 확인
grpc_connectivity_state state = channel->GetState(true);
if (state != GRPC_CHANNEL_READY) {
    channel->WaitForConnected(
        std::chrono::system_clock::now() + std::chrono::seconds(5));
}

문제 2: “DEADLINE_EXCEEDED” 타임아웃

원인: 서버 처리 시간이 클라이언트 데드라인 초과

해결법:

// ✅ 데드라인 충분히 설정 (RPC 유형별 분리)
// 단순 조회: 5초
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));
stub->GetUser(&context, req, &res);

// 스트리밍: 60초
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(60));
auto stream = stub->StreamLogs(&context, req);

// ✅ 서버에서도 데드라인 확인
if (context->IsCancelled()) {
    return Status(StatusCode::CANCELLED, "Client cancelled");
}

문제 3: “INVALID_ARGUMENT” - 필드 누락·타입 오류

원인: .proto 스키마와 실제 데이터 불일치, 필수 필드 미설정

해결법:

// ❌ 잘못된 코드
echo::EchoRequest request;
// message 설정 누락!

// ✅ 올바른 코드
echo::EchoRequest request;
request.set_message("Hello");
request.set_sequence(1);

// ✅ 서버에서 검증
if (request->message().empty()) {
    return Status(StatusCode::INVALID_ARGUMENT, "message required");
}

문제 4: “CANCELLED” - 스트리밍 중 클라이언트 종료

원인: 클라이언트가 연결을 끊거나 데드라인 초과

해결법:

// ✅ 서버: 매 Write 전 IsCancelled() 확인
while (...) {
    if (context->IsCancelled()) {
        return Status(StatusCode::CANCELLED, "Client disconnected");
    }
    if (!writer->Write(response)) break;
}

문제 5: protoc 컴파일 에러 “field number X has been used”

원인: .proto에서 필드 번호 중복

해결법:

// ❌ 잘못된 예
message Bad {
  int32 a = 1;
  int32 b = 1;  // 에러: 1 중복
}

// ✅ 올바른 예
message Good {
  int32 a = 1;
  int32 b = 2;
}

문제 6: “RESOURCE_EXHAUSTED” - 메모리·연결 한도

원인: 서버 처리 용량 초과, 메시지 크기 한도(기본 4MB)

해결법:

// 채널 옵션: 메시지 크기 한도 늘리기
grpc::ChannelArguments args;
args.SetMaxReceiveMessageSize(64 * 1024 * 1024);  // 64MB
auto channel = grpc::CreateCustomChannel(
    "localhost:50051",
    grpc::InsecureChannelCredentials(),
    args);

문제 7: 스트리밍 시 “Stream removed”

원인: 한쪽이 Write/Read를 중단했는데 상대방이 계속 시도

해결법:

// ✅ Write 실패 시 즉시 종료
while (reader->Read(&msg)) {
    if (!writer->Write(response)) {
        break;  // 클라이언트 연결 끊김
    }
}

문제 8: UNAVAILABLE - 서버 재시작 중

원인: Kubernetes 롤링 업데이트 등으로 일시적 연결 끊김

해결법:

// ✅ 지수 백오프 재시도
Status CallWithRetry(std::function<Status()> rpc_call, int max_retries = 3) {
    for (int i = 0; i < max_retries; ++i) {
        Status status = rpc_call();
        if (status.ok()) return status;

        if (status.error_code() != StatusCode::UNAVAILABLE &&
            status.error_code() != StatusCode::DEADLINE_EXCEEDED &&
            status.error_code() != StatusCode::RESOURCE_EXHAUSTED) {
            return status;  // 재시도 불가
        }

        std::this_thread::sleep_for(
            std::chrono::milliseconds(100 * (1 << i)));  // 100, 200, 400ms
    }
    return Status(StatusCode::UNAVAILABLE, "Max retries exceeded");
}

8. 베스트 프랙티스

채널·스텁 재사용

gRPC는 HTTP/2 기반이라 단일 TCP 연결에서 여러 RPC를 동시에 처리합니다. 채널을 재사용하세요.

// ❌ 매 요청마다 새 채널 (비효율)
for (int i = 0; i < 1000; ++i) {
    auto channel = grpc::CreateChannel(...);
    auto stub = Service::NewStub(channel);
    stub->Call(...);
}

// ✅ 채널·스텁 한 번 생성 후 재사용
auto channel = grpc::CreateChannel("localhost:50051", ...);
auto stub = Service::NewStub(channel);
for (int i = 0; i < 1000; ++i) {
    stub->Call(...);  // 같은 연결 재사용
}

데드라인 항상 설정

// ✅ 모든 RPC에 데드라인 설정
ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));
stub->Echo(&context, request, &response);

status.ok() 확인 후 응답 사용

// ❌ status 확인 없이 응답 사용
stub->Echo(&context, request, &response);
std::cout << response.message();  // 실패 시 빈 값·이상한 값

// ✅ status 확인 후 사용
Status status = stub->Echo(&context, request, &response);
if (status.ok()) {
    std::cout << response.message();
} else {
    std::cerr << "RPC failed: " << status.error_message();
}

스트리밍 시 IsCancelled()·Write 반환값 확인

// ✅ 서버 스트리밍
for (int i = 0; i < 1000; ++i) {
    if (context->IsCancelled()) return Status::OK;
    if (!writer->Write(chunk)) break;
}

Protobuf 필드 번호 유지

// ✅ 필드 추가 시 기존 번호 유지
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;      // 새 필드: 3번 추가
  // 절대 1, 2번 변경하지 않음
}

메시지 재사용 (반복 RPC)

// ✅ Response 재사용으로 할당 횟수 감소
echo::EchoResponse response;
for (int i = 0; i < 1000; ++i) {
    request.set_message("msg" + std::to_string(i));
    response.Clear();
    stub_->Echo(&context, request, &response);
}

9. 프로덕션 패턴

TLS 보안 채널

// 서버: TLS 인증서 사용
grpc::SslServerCredentialsOptions::PemKeyCertPair keycert = {
    read_file("server.key"),
    read_file("server.crt")
};
grpc::SslServerCredentialsOptions ssl_opts;
ssl_opts.pem_root_certs = read_file("ca.crt");
ssl_opts.pem_key_cert_pairs.push_back(keycert);

ServerBuilder builder;
builder.AddListeningPort("0.0.0.0:50051",
                         grpc::SslServerCredentials(ssl_opts));

// 클라이언트: TLS 연결
auto creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
auto channel = grpc::CreateChannel("myservice:443", creds);

메타데이터 (인증·트레이싱)

// 클라이언트: 메타데이터 추가
ClientContext context;
context.AddMetadata("authorization", "Bearer " + token);
context.AddMetadata("x-request-id", GenerateRequestId());
stub_->Echo(&context, request, &response);

// 서버: 메타데이터 읽기
void Echo(ServerContext* context, ...) {
    auto auth = context->client_metadata().find("authorization");
    if (auth == context->client_metadata().end()) {
        return Status(StatusCode::UNAUTHENTICATED, "Missing token");
    }
    // 토큰 검증...
}

헬스 체크

service HealthService {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
// Kubernetes liveness/readiness 프로브
// grpc_health_probe -addr=:50051

그레이스풀 셧다운

void ShutdownServer(grpc::Server* server) {
    server->Shutdown();   // 새 RPC 거부, 기존 RPC 완료 대기
    server->Wait();       // 모든 스레드 종료 대기
}

Keepalive 설정

grpc::ChannelArguments args;
args.SetInt("grpc.keepalive_time_ms", 10000);   // 10초마다 keepalive
args.SetInt("grpc.keepalive_timeout_ms", 5000);
args.SetInt("grpc.keepalive_permit_without_calls", 1);

auto channel = grpc::CreateCustomChannel(
    "localhost:50051",
    grpc::InsecureChannelCredentials(),
    args);

JSON vs Protobuf 성능 비교 (참고)

항목JSONProtobuf
직렬화 크기100%약 30~50%
직렬화 속도1x약 5~10x 빠름
역직렬화 속도1x약 5~10x 빠름

10. 구현 체크리스트

Protocol Buffers

  • .protosyntax = "proto3" 명시
  • 필드 번호 1~15를 자주 쓰는 필드에 할당
  • 기존 필드 번호 변경 금지 (하위 호환)
  • protoc로 C++ 코드 생성 확인
  • CMake/빌드 시스템에 생성 코드 통합

gRPC 서버

  • ServerBuilder로 주소·인증 설정
  • RegisterService로 구현체 등록
  • 에러 시 Status에 적절한 StatusCode 반환
  • context->IsCancelled() 체크 (긴 작업)
  • 스트리밍: Write 반환값 확인

gRPC 클라이언트

  • CreateChannel 후 연결 상태 확인
  • set_deadline으로 타임아웃 설정
  • status.ok() 확인 후 응답 사용
  • 재시도 가능 에러에 백오프 적용
  • 채널 재사용 (연결 풀링)

스트리밍

  • 서버: IsCancelled(), Write 반환값 확인
  • 클라이언트: Finish() 호출 후 Status 확인
  • 양방향: 읽기/쓰기 스레드 분리

프로덕션

  • 헬스 체크 RPC 구현
  • 메타데이터로 요청 ID·트레이싱
  • TLS 사용 시 인증서 경로 설정
  • 그레이스풀 셧다운 처리
  • 메시지 크기 한도 검토

11. 정리

항목요약
Protocol Buffers.proto로 스키마 정의 → C++ 코드 생성·바이너리 직렬화
Unary RPC요청 1개 → 응답 1개, 가장 기본
서버 스트리밍요청 1개 → 응답 스트림, 로그·대용량
클라이언트 스트리밍요청 스트림 → 응답 1개, 업로드·배치
양방향 스트리밍요청·응답 스트림 동시, 채팅·실시간
에러 처리Status·StatusCode 확인, 데드라인·재시도
성능채널 재사용, Protobuf 필드 최적화
프로덕션헬스 체크, 트레이싱, TLS, 그레이스풀 셧다운

자주 묻는 질문 (FAQ)

Q. gRPC와 REST/JSON의 차이는?

A. gRPC는 HTTP/2 기반 바이너리 프로토콜로, JSON보다 직렬화가 빠르고 크기가 작습니다. 스키마(.proto)로 타입 안전성이 보장되며, 스트리밍을 기본 지원합니다. REST는 텍스트 기반이라 디버깅이 쉽지만, 마이크로서비스 간 고성능 통신에는 gRPC가 유리합니다.

Q. .proto 필드 번호를 바꾸면 안 되나요?

A. 필드 번호는 직렬화 시 식별자로 사용됩니다. 번호를 바꾸면 기존 클라이언트·서버와 호환성이 깨집니다. 새 필드는 새 번호로 추가하고, 사용하지 않는 필드는 deprecated로 표시만 하세요.

Q. C++에서 비동기 gRPC가 꼭 필요한가요?

A. 초당 수천 건 이상의 고부하에서는 비동기 API와 CompletionQueue가 유리합니다. 저부하나 단순 로직이라면 동기 API만으로도 충분합니다. gRPC 마스터(#52-2)에서 고급 패턴을 다룹니다.

Q. 프로덕션에서 TLS는 필수인가요?

A. 외부 노출 서비스나 민감한 데이터를 다룰 때는 TLS를 사용하는 것이 좋습니다. 내부망 전용이라면 InsecureCredentials도 쓰이지만, 보안 정책에 따라 결정하세요.

한 줄 요약: gRPC·Protobuf로 타입 안전한 RPC와 직렬화를 구성할 수 있습니다. Unary·스트리밍 4가지 패턴을 완전한 예제로 구현하고, 프로덕션 패턴까지 적용할 수 있습니다.

다음 글: gRPC 마스터: 양방향 스트리밍·인터셉터·로드밸런싱(#52-2)

이전 글: gRPC 완벽 가이드(#52-1)


관련 글

  • C++ gRPC 고급 완벽 가이드 | 인터셉터·로드밸런싱·데드라인·재시도·헬스체크 [#52-3]
  • C++ gRPC 마스터 | 스트리밍·인터셉터·로드밸런싱 [#52-2]
  • C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
  • C++ Protocol Buffers 완벽 가이드 | 직렬화·스키마 진화·성능 최적화·프로덕션 패턴
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3