본문으로 건너뛰기
Previous
Next
gRPC and Protocol Buffers in C++: From .proto to Production

gRPC and Protocol Buffers in C++: From .proto to Production

gRPC and Protocol Buffers in C++: From .proto to Production

이 글의 핵심

gRPC runs on HTTP/2 with Protocol Buffers — strongly typed, binary-encoded, with built-in streaming. This guide walks from .proto definition through C++ server and client implementation to production patterns.

Why gRPC Over REST for Service-to-Service?

REST with JSON works well for browser-facing APIs. For internal service-to-service communication, gRPC has practical advantages:

  • Strong typing: the .proto schema is the contract — no undocumented JSON shapes
  • Binary encoding: Protocol Buffers typically encode 3-10x smaller than equivalent JSON
  • HTTP/2 multiplexing: many RPCs share one connection with flow control
  • Built-in streaming: server streaming, client streaming, and bidirectional streaming are first-class
  • Code generation: client and server stubs in C++, Go, Python, Java, and others from one .proto file

1. Protocol Buffers Basics

Define messages and service interfaces in .proto files:

// user_service.proto
syntax = "proto3";

package myapp;

// Messages define the data structures
message GetUserRequest {
  int32 user_id = 1;  // field number = 1, must never change
}

message UserResponse {
  int32 user_id = 1;
  string name = 2;
  string email = 3;
  int64 created_at = 4;  // Unix timestamp
  repeated string roles = 5;  // array of strings
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

// Service defines the RPC methods
service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse);
  rpc CreateUser(CreateUserRequest) returns (UserResponse);

  // Streaming variants (covered later)
  rpc ListUsers(ListUsersRequest) returns (stream UserResponse);
}

Field numbers are permanent — the number (not the name) identifies a field in the binary encoding. Never reuse a field number for a different field; mark removed fields as reserved instead:

message UserResponse {
  int32 user_id = 1;
  string name = 2;
  reserved 3;  // was "phone", removed — prevents accidental reuse
  string email = 4;
}

Generate C++ Code

# Install protoc and grpc_cpp_plugin (varies by platform)
# Ubuntu: apt install protobuf-compiler-grpc libgrpc++-dev
# macOS: brew install grpc

# Generate C++ stubs
protoc \
  --cpp_out=./generated \
  --grpc_out=./generated \
  --plugin=protoc-gen-grpc=$(which grpc_cpp_plugin) \
  user_service.proto

# Creates:
# generated/user_service.pb.h        — message classes
# generated/user_service.pb.cc
# generated/user_service.grpc.pb.h   — service/stub classes
# generated/user_service.grpc.pb.cc

2. gRPC C++ Server

// server.cc
#include <grpcpp/grpcpp.h>
#include "generated/user_service.grpc.pb.h"
#include <memory>
#include <iostream>

class UserServiceImpl final : public myapp::UserService::Service {
public:
    grpc::Status GetUser(
        grpc::ServerContext* ctx,
        const myapp::GetUserRequest* req,
        myapp::UserResponse* resp) override
    {
        // Validate input
        if (req->user_id() <= 0) {
            return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                "user_id must be positive");
        }

        // Simulate database lookup
        if (req->user_id() == 1) {
            resp->set_user_id(1);
            resp->set_name("Alice");
            resp->set_email("[email protected]");
            resp->set_created_at(1700000000);
            resp->add_roles("admin");
            resp->add_roles("user");
            return grpc::Status::OK;
        }

        return grpc::Status(grpc::StatusCode::NOT_FOUND,
            "user " + std::to_string(req->user_id()) + " not found");
    }

    grpc::Status CreateUser(
        grpc::ServerContext* ctx,
        const myapp::CreateUserRequest* req,
        myapp::UserResponse* resp) override
    {
        if (req->name().empty() || req->email().empty()) {
            return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                "name and email are required");
        }

        // Create user in database...
        resp->set_user_id(42);
        resp->set_name(req->name());
        resp->set_email(req->email());
        return grpc::Status::OK;
    }
};

void RunServer() {
    std::string address = "0.0.0.0:50051";
    UserServiceImpl service;

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

    std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
    std::cout << "Server listening on " << address << '\n';
    server->Wait();
}

int main() {
    RunServer();
}

3. gRPC C++ Client

// client.cc
#include <grpcpp/grpcpp.h>
#include "generated/user_service.grpc.pb.h"
#include <memory>
#include <iostream>

class UserServiceClient {
    std::unique_ptr<myapp::UserService::Stub> stub_;

public:
    explicit UserServiceClient(std::shared_ptr<grpc::Channel> channel)
        : stub_(myapp::UserService::NewStub(channel)) {}

    std::optional<myapp::UserResponse> GetUser(int32_t userId) {
        myapp::GetUserRequest request;
        request.set_user_id(userId);

        myapp::UserResponse response;
        grpc::ClientContext ctx;

        // Set deadline — always set deadlines on client calls
        ctx.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));

        grpc::Status status = stub_->GetUser(&ctx, request, &response);

        if (status.ok()) {
            return response;
        }

        std::cerr << "GetUser failed: ["
            << status.error_code() << "] "
            << status.error_message() << '\n';
        return std::nullopt;
    }
};

int main() {
    // Create a channel — reuse it across calls, don't create per-call
    auto channel = grpc::CreateChannel("localhost:50051",
        grpc::InsecureChannelCredentials());

    UserServiceClient client(channel);

    auto user = client.GetUser(1);
    if (user) {
        std::cout << "User: " << user->name()
                  << " (" << user->email() << ")\n";
        for (const auto& role : user->roles()) {
            std::cout << "  role: " << role << '\n';
        }
    }
}

4. Streaming RPCs

gRPC has three streaming modes in addition to basic unary RPC:

Server Streaming (server sends multiple responses)

// Server sends a stream of UserResponse
rpc ListUsers(ListUsersRequest) returns (stream UserResponse);
// Server implementation
grpc::Status ListUsers(
    grpc::ServerContext* ctx,
    const myapp::ListUsersRequest* req,
    grpc::ServerWriter<myapp::UserResponse>* writer) override
{
    // Fetch and stream rows from database
    for (const auto& user : db.getAllUsers()) {
        myapp::UserResponse resp;
        resp.set_user_id(user.id);
        resp.set_name(user.name);

        if (!writer->Write(resp)) {
            break;  // client disconnected or cancelled
        }

        if (ctx->IsCancelled()) {
            return grpc::Status(grpc::StatusCode::CANCELLED, "cancelled");
        }
    }
    return grpc::Status::OK;
}

// Client usage
grpc::ClientContext ctx;
myapp::ListUsersRequest req;
auto reader = stub_->ListUsers(&ctx, req);

myapp::UserResponse resp;
while (reader->Read(&resp)) {
    std::cout << resp.name() << '\n';
}

grpc::Status status = reader->Finish();
if (!status.ok()) { /* handle error */ }

Client Streaming (client sends multiple messages)

// Client sends a stream of records to import
rpc BulkImport(stream ImportRecord) returns (ImportResult);
// Client usage
grpc::ClientContext ctx;
myapp::ImportResult result;
auto writer = stub_->BulkImport(&ctx, &result);

for (const auto& record : localData) {
    myapp::ImportRecord req;
    req.set_data(record.serialize());
    if (!writer->Write(req)) break;  // server closed early
}

writer->WritesDone();
grpc::Status status = writer->Finish();

5. Error Handling

gRPC status codes map to common error categories:

Status codeMeaningExample use
OKSuccess
INVALID_ARGUMENTBad client inputMissing required field
NOT_FOUNDResource doesn’t existUser ID not in database
ALREADY_EXISTSDuplicateEmail already registered
PERMISSION_DENIEDAuth failedInvalid token
UNAVAILABLEServer temporarily downDatabase connection failed
DEADLINE_EXCEEDEDTimeoutSlow query
RESOURCE_EXHAUSTEDRate limit or quotaToo many requests
// Server — return meaningful status codes
if (!db.userExists(req->user_id())) {
    return grpc::Status(grpc::StatusCode::NOT_FOUND,
        "user " + std::to_string(req->user_id()) + " not found");
}

// Client — check and handle each code
grpc::Status status = stub_->GetUser(&ctx, request, &response);
switch (status.error_code()) {
    case grpc::StatusCode::OK:
        break;
    case grpc::StatusCode::NOT_FOUND:
        std::cerr << "User not found\n";
        break;
    case grpc::StatusCode::DEADLINE_EXCEEDED:
        std::cerr << "Request timed out — retry?\n";
        break;
    default:
        std::cerr << "Error " << status.error_code()
                  << ": " << status.error_message() << '\n';
}

6. TLS for Production

Never use InsecureChannelCredentials in production. Use TLS:

// Server with TLS
grpc::SslServerCredentialsOptions ssl_opts;
ssl_opts.pem_key_cert_pairs.push_back({
    ReadFile("server.key"),
    ReadFile("server.crt"),
});
// Optionally add CA cert for mutual TLS
ssl_opts.pem_root_certs = ReadFile("ca.crt");

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

// Client with TLS
grpc::SslCredentialsOptions ssl_client_opts;
ssl_client_opts.pem_root_certs = ReadFile("ca.crt");  // CA to verify server

auto channel = grpc::CreateChannel("service.example.com:443",
    grpc::SslCredentials(ssl_client_opts));

7. Production Checklist

  • Always set deadlines: ctx.set_deadline(std::chrono::system_clock::now() + timeout) — missing deadlines cause requests to hang indefinitely
  • Reuse channels and stubs: creating a channel per request is expensive (TLS handshake, HTTP/2 setup) — create once and share
  • Check IsCancelled() in streaming handlers: clients may disconnect; don’t keep writing to a closed stream
  • Handle UNAVAILABLE with retry: transient failures (network blip, pod restart) should trigger retry with exponential backoff
  • Use reserved for removed fields: prevents accidental reuse of field numbers in future .proto changes
  • Health checks: implement the gRPC health checking protocol for load balancer integration
  • Message size limits: large payloads hit the default 4MB limit — set grpc.max_receive_message_length in ChannelArguments

Key Takeaways

  • .proto files define the schema — field numbers are permanent identifiers, not the names
  • protoc generates typed C++ classes; the client stub and server base class come from grpc_cpp_plugin
  • Every unary RPC returns grpc::Status — always check status.ok() before using the response
  • Streaming modes (server, client, bidi) are first-class in gRPC — use them for large payloads or live feeds
  • Always set deadlines on client calls — no deadline means the call may wait indefinitely
  • Reuse channels — they are expensive to create; create one per target service and share it
  • Use TLS (SslServerCredentials, SslCredentials) in any non-local environment

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Build C++ gRPC microservices with Protocol Buffers: .proto definition, protoc codegen, sync server and client, streaming… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


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

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


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

C++, gRPC, Protocol Buffers, RPC, Microservices 등으로 검색하시면 이 글이 도움이 됩니다.