C++ 고성능 RPC 시스템: gRPC와 Protocol Buffers를 이용한 마이크로서비스 구축

C++ 고성능 RPC 시스템: gRPC와 Protocol Buffers를 이용한 마이크로서비스 구축

이 글의 핵심

마이크로서비스 간 통신에서 JSON 대신 gRPC·Protobuf를 쓰는 이유, .proto 정의부터 동기/비동기 서버·클라이언트·스트리밍·에러 처리까지 실전 코드로 다룹니다. 문제 시나리오, 일반적인 에러, 성능 최적화, 프로덕션 패턴 포함.

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

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

30번에서 웹소켓·SSL·프로토콜 직렬화를 다뤘다면, 마이크로서비스(작은 서비스 단위로 시스템을 나누는 아키텍처) 간에는 gRPC(Google이 만든 고성능 RPC 프레임워크)와 Protocol Buffers(구글이 만든 바이너리 직렬화 포맷)가 널리 쓰입니다. 스키마가 명확하고 바이너리 직렬화로 효율이 좋으며, 스트리밍(서버·클라이언트·양방향)을 표준으로 지원합니다.

이 글에서 다루는 것:

  • Protocol Buffers: .proto 문법·메시지·서비스 정의·C++ 코드 생성
  • gRPC C++: 동기/비동기 서버·클라이언트·스트리밍
  • 에러·메타데이터·인증 개요
  • 문제 시나리오·일반적인 에러·성능 최적화·프로덕션 패턴

실제 문제 시나리오

시나리오 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 양방향 스트리밍 → 단일 연결로 양방향 통신

이 글에서는 위와 같은 문제를 gRPC·Protobuf로 해결하는 방법을 완전한 예제와 함께 다룹니다.

개념을 잡는 비유

소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.


목차

  1. Protocol Buffers 기초
  2. gRPC C++ 서버·클라이언트
  3. 스트리밍 (서버·클라이언트·양방향)
  4. 완전한 gRPC·Protobuf 예제
  5. 자주 발생하는 에러와 해결법
  6. 성능 최적화
  7. 프로덕션 패턴
  8. 구현 체크리스트
  9. 정리

1. 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 예시

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

syntax = "proto3";
package myapp;

// 메시지 정의: 필드 번호는 절대 변경하지 않음
message UserRequest {
  int32 user_id = 1;
  string name = 2;
  repeated string tags = 3;  // 반복 필드
}

message UserResponse {
  int32 user_id = 1;
  string name = 2;
  int64 created_at = 3;
  map<string, string> metadata = 4;  // 맵 타입
}

// 서비스 정의
service UserService {
  rpc GetUser(UserRequest) returns (UserResponse);
  rpc ListUsers(UserRequest) returns (stream UserResponse);  // 서버 스트리밍
}

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";

protoc로 C++ 코드 생성

# protoc 설치 (macOS)
brew install protobuf

# gRPC C++ 플러그인 (vcpkg)
vcpkg install grpc

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

생성되는 파일:

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

CMake 통합

# CMakeLists.txt
find_package(Protobuf REQUIRED)
find_package(gRPC CONFIG REQUIRED)

# Protobuf 생성
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 user_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 ${SOURCES} ${GENERATED_SOURCES})
target_include_directories(grpc_server PRIVATE ${GENERATED_PROTOBUF_PATH})
target_link_libraries(grpc_server PRIVATE
  protobuf::libprotobuf
  gRPC::grpc++
  gRPC::grpc++_reflection
)

2. gRPC C++ 서버·클라이언트

동기 vs 비동기 API

구분동기비동기
서버한 RPC 처리 후 다음 요청CompletionQueue로 여러 RPC 동시 처리
클라이언트블로킹 호출Async* + CompletionQueue
적합저부하, 단순 로직고부하, 고성능

gRPC 요청-응답 시퀀스

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

동기 서버 구현

#include <grpcpp/grpcpp.h>
#include "user_service.grpc.pb.h"

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

class UserServiceImpl final : public myapp::UserService::Service {
 public:
  Status GetUser(ServerContext* context,
                 const myapp::UserRequest* request,
                 myapp::UserResponse* response) override {
    // 1. 요청 검증
    if (request->user_id() <= 0) {
      return Status(grpc::StatusCode::INVALID_ARGUMENT, "user_id must be positive");
    }

    // 2. 비즈니스 로직 (DB 조회 등)
    response->set_user_id(request->user_id());
    response->set_name("User_" + std::to_string(request->user_id()));
    response->set_created_at(1700000000);

    return Status::OK;
  }
};

int main() {
  std::string server_address("0.0.0.0:50051");
  UserServiceImpl 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();
  return 0;
}

동기 클라이언트 구현

#include <grpcpp/grpcpp.h>
#include "user_service.grpc.pb.h"

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

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

  auto stub = myapp::UserService::NewStub(channel);
  myapp::UserRequest request;
  request.set_user_id(123);
  request.set_name("test");

  myapp::UserResponse response;
  ClientContext context;

  // 데드라인 설정 (5초)
  context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));

  Status status = stub->GetUser(&context, request, &response);

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

비동기 서버 (CompletionQueue)

#include <grpcpp/grpcpp.h>
#include "user_service.grpc.pb.h"
#include <thread>

class AsyncUserServiceImpl final : public myapp::UserService::Service {
 public:
  // 동기 구현은 그대로, 비동기 처리는 CompletionQueue로
};

void RunAsyncServer() {
  std::string server_address("0.0.0.0:50051");
  AsyncUserServiceImpl service;
  grpc::ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);

  grpc::CompletionQueue* cq = builder.AddCompletionQueue();
  std::unique_ptr<grpc::Server> server(builder.BuildAndStart());

  // CompletionQueue 폴링 (별도 스레드)
  std::thread poll_thread([cq]() {
    void* tag;
    bool ok;
    while (cq->Next(&tag, &ok)) {
      // 태그에 따라 RPC 완료 처리
      if (!ok) break;
    }
  });

  server->Wait();
  server->Shutdown();
  cq->Shutdown();
  poll_thread.join();
}

3. 스트리밍 (서버·클라이언트·양방향)

스트리밍 유형 비교

유형.proto 정의사용 사례
서버 스트리밍returns (stream T)로그 스트리밍, 대용량 목록
클라이언트 스트리밍stream Request대용량 업로드
양방향 스트리밍stream Request returns stream Response채팅, 실시간 게임

서버 스트리밍 .proto

syntax = "proto3";
package myapp;

message LogRequest {
  string filter = 1;
  int32 max_lines = 2;
}

message LogChunk {
  string line = 1;
  int64 timestamp = 2;
}

service LogService {
  rpc StreamLogs(LogRequest) returns (stream LogChunk);
}

서버 스트리밍 구현

Status StreamLogs(ServerContext* context,
                  const myapp::LogRequest* request,
                  grpc::ServerWriter<myapp::LogChunk>* writer) override {
  // 클라이언트가 연결을 끊으면 context->IsCancelled()가 true
  for (int i = 0; i < request->max_lines() && !context->IsCancelled(); ++i) {
    myapp::LogChunk chunk;
    chunk.set_line("log line " + std::to_string(i));
    chunk.set_timestamp(std::time(nullptr));

    if (!writer->Write(chunk)) {
      break;  // 클라이언트 연결 끊김
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  }
  return Status::OK;
}

클라이언트 스트리밍 수신

void ReceiveStream() {
  myapp::LogRequest request;
  request.set_filter("error");
  request.set_max_lines(100);

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

  auto reader = stub->StreamLogs(&context, request);
  myapp::LogChunk chunk;
  while (reader->Read(&chunk)) {
    std::cout << chunk.timestamp() << ": " << chunk.line() << std::endl;
  }
  Status status = reader->Finish();
  if (!status.ok()) {
    std::cerr << "Stream failed: " << status.error_message() << std::endl;
  }
}

양방향 스트리밍

service ChatService {
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
  string user = 1;
  string text = 2;
  int64 timestamp = 3;
}
// 서버 측
Status Chat(ServerContext* context,
            grpc::ServerReaderWriter<myapp::ChatMessage, myapp::ChatMessage>* stream) override {
  myapp::ChatMessage msg;
  while (stream->Read(&msg)) {
    // 수신한 메시지 처리 후 응답
    msg.set_user("server");
    msg.set_text("Echo: " + msg.text());
    stream->Write(msg);
  }
  return Status::OK;
}

4. 완전한 gRPC·Protobuf 예제

예제 1: 에러 처리·메타데이터 포함 서버

#include <grpcpp/grpcpp.h>
#include "user_service.grpc.pb.h"

Status GetUser(ServerContext* context,
               const myapp::UserRequest* request,
               myapp::UserResponse* response) override {
  // 메타데이터 읽기 (인증 토큰, 트레이싱 ID 등)
  auto auth = context->client_metadata().find("authorization");
  if (auth == context->client_metadata().end()) {
    return Status(grpc::StatusCode::UNAUTHENTICATED, "Missing authorization");
  }

  // 응답 메타데이터 추가
  context->AddInitialMetadata("x-request-id", "req-12345");

  if (request->user_id() <= 0) {
    return Status(grpc::StatusCode::INVALID_ARGUMENT,
                  "user_id must be positive");
  }

  // NOT_FOUND 예시
  if (request->user_id() == 999) {
    return Status(grpc::StatusCode::NOT_FOUND, "User not found");
  }

  response->set_user_id(request->user_id());
  response->set_name("User_" + std::to_string(request->user_id()));
  return Status::OK;
}

예제 2: 재시도·백오프가 있는 클라이언트

#include <chrono>
#include <thread>

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() != grpc::StatusCode::UNAVAILABLE &&
        status.error_code() != grpc::StatusCode::DEADLINE_EXCEEDED &&
        status.error_code() != grpc::StatusCode::RESOURCE_EXHAUSTED) {
      return status;  // 재시도 불가
    }

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

// 사용
Status status = CallWithRetry([&]() {
  ClientContext ctx;
  ctx.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));
  return stub->GetUser(&ctx, request, &response);
});

예제 3: 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("localhost:50051", creds);

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

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

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

해결법:

// ❌ 잘못된 예: 채널 생성만 하고 연결 확인 안 함
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” 타임아웃

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

해결법:

// ✅ 데드라인 충분히 설정
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(30));

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

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

원인: .proto 스키마와 실제 데이터 불일치, 또는 required 필드 누락(proto3에서는 required 없음)

해결법:

// ✅ 요청 전 필수 필드 검증
if (!request->has_user_id()) {  // proto3에서는 optional이면 has_* 사용
  return Status(grpc::StatusCode::INVALID_ARGUMENT, "user_id required");
}

// ✅ enum 값 검증
if (!myapp::UserStatus_IsValid(request->status())) {
  return Status(grpc::StatusCode::INVALID_ARGUMENT, "Invalid status");
}

문제 4: “CANCELLED” - 클라이언트 연결 끊김

원인: 클라이언트가 RPC를 취소하거나 연결 종료

해결법:

// 서버: 주기적으로 취소 여부 확인 (긴 작업에서)
for (int i = 0; i < 1000; ++i) {
  if (context->IsCancelled()) {
    return Status(grpc::StatusCode::CANCELLED, "Client disconnected");
  }
  DoWork(i);
}

문제 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;  // 클라이언트 연결 끊김
  }
}

6. 성능 최적화

HTTP/2 멀티플렉싱 활용

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

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

// ✅ 채널 재사용
auto channel = grpc::CreateChannel("localhost:50051", ...);
auto stub = MyService::NewStub(channel);
for (int i = 0; i < 1000; ++i) {
  stub->Call(...);  // 같은 연결 재사용
}

메시지 크기 최적화

// ❌ 비효율: 문자열로 큰 데이터
message Bad {
  string huge_json = 1;  // UTF-8 오버헤드
}

// ✅ 효율: bytes로 바이너리
message Good {
  bytes payload = 1;  // 이미 직렬화된 데이터
}

// ✅ repeated보다 map이 적합한 경우
message Config {
  map<string, string> key_value = 1;  // 조회 시 O(1)
}

직렬화 비용 줄이기

  • 필드 번호는 1~15가 1바이트로 인코딩되므로 자주 쓰는 필드를 앞에
  • repeated 필드는 한 번에 설정하지 말고 Reserve()Add() 사용
// ✅ repeated 필드 사전 할당
response->mutable_items()->Reserve(1000);
for (int i = 0; i < 1000; ++i) {
  auto* item = response->add_items();
  item->set_id(i);
}

스레드 풀 크기 조정

// 서버: CompletionQueue 스레드 수
grpc::ServerBuilder builder;
builder.SetSyncServerOption(grpc::ServerBuilder::MIN_POLLERS, 4);
builder.SetSyncServerOption(grpc::ServerBuilder::MAX_POLLERS, 16);

JSON vs Protobuf 성능 비교 (참고)

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

7. 프로덕션 패턴

헬스 체크

service HealthService {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
// Kubernetes 등에서 liveness/readiness 프로브로 사용
Status Check(ServerContext* ctx,
             const HealthCheckRequest* req,
             HealthCheckResponse* res) override {
  res->set_status(SERVING);
  return Status::OK;
}

메타데이터로 트레이싱

// 클라이언트: 요청 ID 전달
context.AddMetadata("x-request-id", GenerateUUID());
context.AddMetadata("x-trace-id", GetCurrentTraceId());

// 서버: 메타데이터 읽어 로깅
auto it = context->client_metadata().find("x-request-id");
if (it != context->client_metadata().end()) {
  LOG(INFO) << "Request ID: " << std::string(it->second.begin(), it->second.end());
}

로드 밸런싱 (Round-robin)

// 여러 서버 주소로 채널 생성 시 자동 로드밸런싱
auto channel = grpc::CreateChannel(
    "dns:///my-service:50051",  // DNS 기반
    grpc::InsecureChannelCredentials());

서버 그레이스풀 셧다운

void ShutdownServer() {
  server->Shutdown();   // 새 연결 거부, 기존 RPC 완료 대기
  cq->Shutdown();       // CompletionQueue 종료
  server->Wait();       // 모든 스레드 종료 대기
}

스키마 버전 관리

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

8. 구현 체크리스트

Protocol Buffers

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

gRPC 서버

  • ServerBuilder로 주소·인증 설정
  • RegisterService로 구현체 등록
  • 에러 시 Status에 적절한 StatusCode 반환
  • context->IsCancelled() 체크 (긴 작업)
  • TLS 사용 시 인증서 경로 설정

gRPC 클라이언트

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

프로덕션

  • 헬스 체크 RPC 구현
  • 메타데이터로 요청 ID·트레이싱
  • 로깅·메트릭 연동
  • 그레이스풀 셧다운 처리
  • 메시지 크기 한도 검토

9. 정리

항목요약
Protocol Buffers.proto로 스키마 정의 → C++ 코드 생성·바이너리 직렬화
gRPC동기/비동기 서버·클라이언트·스트리밍 (HTTP/2 기반)
에러 처리Status·StatusCode 확인, 데드라인·재시도
성능채널 재사용, Protobuf 필드 최적화, 스레드 풀 조정
프로덕션헬스 체크, 트레이싱, TLS, 그레이스풀 셧다운

43-1로 고성능 RPC마이크로서비스 구축의 기초를 다뤘습니다. 다음으로 보안 코딩·OpenSSL(#43-2)를 읽어보면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
  • C++26 프리뷰: Reflection과 신규 표준 라이브러리 제안들 [#44-1]
  • C++ Boost.Asio 입문 | io_context·async_read

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


이 글에서 다루는 키워드 (관련 검색어)

gRPC, Protocol Buffers, C++ gRPC, Protobuf C++, 마이크로서비스 RPC 등으로 검색하시면 이 글이 도움이 됩니다.

자주 묻는 질문 (FAQ)

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

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

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

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

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

A. 초당 수천 건 이상의 고부하에서는 비동기 API와 CompletionQueue가 유리합니다. 저부하나 단순 로직이라면 동기 API만으로도 충분합니다.

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

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

한 줄 요약: gRPC·Protobuf로 타입 안전한 RPC와 직렬화를 구성할 수 있습니다. 다음으로 보안 코딩·OpenSSL(#43-2)를 읽어보면 좋습니다.

다음 글: [실전 도메인 #43-2] 보안 코딩 가이드: 오버플로우 방지와 암호화 라이브러리(OpenSSL) 실전 연동

이전 글: [실전 도메인 #42-3] 리눅스 시스템 프로그래밍: 시스템 콜 호출과 커널 인터페이스 이해


관련 글

  • C++ gRPC 완벽 가이드 | 마이크로서비스 RPC·문제 해결·성능 최적화 [#52-1]