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

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

이 글의 핵심

C++ REST API 대신 gRPC로 마이크로서비스 통신 시 연결 타임아웃·직렬화 비용·에러 처리가 막막하다면? Protocol Buffers부터 완전한 서버/클라이언트 예제, 자주 발생하는 에러, 성능 팁, 프로덕션 패턴까지 실전 코드로 구현합니다.

들어가며: REST API로는 마이크로서비스 통신이 느리다

실제 겪는 문제 시나리오

시나리오 1: 주문 서비스 → 재고 서비스 호출 시 지연
주문 API가 재고 확인을 위해 HTTP REST로 다른 서비스를 호출합니다. JSON 직렬화/역직렬화 비용과 HTTP 오버헤드로 인해 요청당 50~100ms가 소요됩니다. 초당 1000건 주문 시 병목이 발생합니다.

시나리오 2: 실시간 로그 수집 시 연결 끊김
여러 마이크로서비스에서 중앙 로그 서버로 로그를 스트리밍해야 합니다. HTTP 폴링은 비효율적이고, WebSocket은 별도 구현이 필요합니다. 연결이 끊기면 재연결·재시도 로직을 직접 구현해야 합니다.

시나리오 3: 스키마 변경 시 클라이언트/서버 불일치
REST API는 스키마가 코드와 문서에 흩어져 있어, 서버를 업데이트한 뒤 구버전 클라이언트가 잘못된 필드를 보내면 런타임 에러가 발생합니다. 버전 호환성을 수동으로 관리해야 합니다.

시나리오 4: gRPC 도입 후 “Connection refused” 에러
gRPC 서버를 띄웠는데 클라이언트에서 연결이 안 됩니다. HTTP/2, TLS 설정, 포트 방화벽 등 확인할 것이 많아 막막합니다.

시나리오 5: 대용량 응답 시 메모리 폭증
REST API로 10만 건의 레코드를 JSON 배열로 한 번에 반환하면, 서버와 클라이언트 모두 메모리 사용량이 급증합니다. 페이지네이션을 구현해도 HTTP 요청 오버헤드가 누적됩니다.

시나리오 6: 서비스 간 버전 불일치
A 서비스가 v1 API를, B 서비스가 v2 API를 기대할 때, REST는 런타임에 필드 누락/추가로 에러가 발생합니다. 스키마 버전 관리가 어렵습니다.

gRPC로 해결:

  • 바이너리 직렬화(Protocol Buffers): JSON 대비 310배 작은 페이로드, 510배 빠른 직렬화
  • HTTP/2 기반: 멀티플렉싱, 단일 연결로 다중 스트림
  • 스키마 기반: .proto에서 서비스·메시지 정의 → 코드 생성으로 타입 안전성
  • 스트리밍 표준 지원: 서버/클라이언트/양방향 스트리밍
flowchart LR
  subgraph rest[REST API]
    R1[클라이언트] -->|JSON/HTTP| R2[서버]
    R2 -->|JSON/HTTP| R1
  end
  subgraph grpc[gRPC]
    G1[클라이언트] -->|Protobuf/HTTP2| G2[서버]
    G2 -->|Protobuf/HTTP2| G1
  end

gRPC 통신 흐름

sequenceDiagram
  participant C as 클라이언트
  participant S as 서버

  C->>S: EchoRequest (Protobuf 직렬화)
  Note over S: 비즈니스 로직 처리
  S->>C: EchoResponse (Protobuf 직렬화)
  C->>C: status.ok() 확인

REST vs gRPC 비교

항목REST + JSONgRPC + Protobuf
직렬화텍스트, 느림바이너리, 빠름
스키마OpenAPI 등 별도.proto에 통합
스트리밍WebSocket 별도 구현표준 지원
HTTPHTTP/1.1 (기본)HTTP/2 (멀티플렉싱)

이 글에서 다루는 것:

  • Protocol Buffers: .proto 정의·코드 생성
  • 완전한 gRPC C++ 서버·클라이언트 예제
  • 자주 발생하는 에러와 해결법
  • 성능 최적화 팁
  • 프로덕션 배포 패턴

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

목차

  1. 환경 설정
  2. Protocol Buffers 기초
  3. 완전한 gRPC 예제
  4. 스트리밍 예제
  5. 자주 발생하는 에러와 해결법
  6. 성능 최적화
  7. 프로덕션 패턴
  8. 구현 체크리스트

1. 환경 설정

필수 의존성

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

vcpkg로 설치

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

CMakeLists.txt 기본 설정

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)

# .proto 파일에서 C++ 코드 생성
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 "${PROTO_PATH}/echo.proto")
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES})

# 생성된 파일 경로 설정
include_directories(${GENERATED_PROTOBUF_PATH})
include_directories(${PROTOBUF_INCLUDE_DIRS})

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

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

2. Protocol Buffers 기초

.proto 파일 정의

필드 번호는 스키마 호환성에 중요합니다. 기존 번호를 바꾸지 않고 새 필드만 추가하면 하위 호환이 유지됩니다.

syntax = "proto3";

package echo;

// Echo 서비스: 클라이언트가 메시지를 보내면 서버가 그대로 반환
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;
}

코드 생성

# protoc로 C++ 코드 생성 (gRPC 플러그인 사용)
protoc -I proto --cpp_out=generated --grpc_out=generated \
  --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` \
  proto/echo.proto

생성되는 파일:

  • echo.pb.h, echo.pb.cc: 메시지 클래스
  • echo.grpc.pb.h, echo.grpc.pb.cc: 서비스 스텁(Stub)·서비스 베이스(Service)

3. 완전한 gRPC 예제

3.1 서버 구현 (동기)

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

#include "echo.grpc.pb.h"

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

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(grpc::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;
    // 인증 없이 insecure 채널 (개발용)
    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;
}

코드 설명:

  • EchoServiceImpl: echo::EchoService::Service를 상속해 RPC 메서드 구현
  • Status::OK: 성공 시 반환
  • Status(StatusCode, message): 에러 시 상세 메시지와 함께 반환
  • ServerBuilder: 주소, 인증, 옵션 설정 후 BuildAndStart()로 서버 시작

3.2 클라이언트 구현 (동기)

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

#include "echo.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초 내 응답 없으면 실패
        std::chrono::system_clock::time_point deadline =
            std::chrono::system_clock::now() + std::chrono::seconds(5);
        context.set_deadline(deadline);
        
        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;
}

코드 설명:

  • CreateChannel: 서버 주소와 credentials로 채널 생성. 채널은 재사용 가능
  • ClientContext: 요청별 메타데이터, 데드라인 설정
  • set_deadline: 타임아웃 설정으로 무한 대기 방지
  • status.ok(): 성공 여부 확인, 실패 시 error_code(), error_message()로 상세 확인

3.3 실행 순서

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

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

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

3.4 에러 처리 래퍼 (실전용)

실제 프로젝트에서는 에러 코드별 분기와 로깅이 필요합니다.

template<typename Func>
grpc::Status CallWithLogging(const char* rpc_name, Func&& func) {
    auto start = std::chrono::steady_clock::now();
    grpc::Status status = func();
    auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::steady_clock::now() - start).count();
    
    if (status.ok()) {
        LOG(INFO) << rpc_name << " OK in " << dur << "ms";
    } else {
        LOG(ERROR) << rpc_name << " FAILED: " << status.error_code()
                   << " - " << status.error_message() << " (" << dur << "ms)";
    }
    return status;
}

// 사용 예
Status status = CallWithLogging("Echo", [&]() {
    return stub_->Echo(&context, request, &response);
});

3.5 클라이언트 스트리밍 (완전한 예제)

클라이언트가 여러 요청을 보내고 서버가 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();
}

4. 스트리밍 예제

4.1 서버 스트리밍

클라이언트가 1회 요청 → 서버가 여러 응답을 스트리밍합니다. 예: 로그 조회, 대용량 데이터 청크 전송.

// 서버 측
Status ServerStream(ServerContext* context,
                    const echo::EchoRequest* request,
                    grpc::ServerWriter<echo::EchoResponse>* writer) override {
    for (int i = 0; i < 5; ++i) {
        echo::EchoResponse response;
        response.set_message(request->message() + " #" + std::to_string(i));
        response.set_sequence(i);
        
        // 클라이언트 연결 끊김 확인
        if (context->IsCancelled()) {
            return Status(grpc::StatusCode::CANCELLED, "Client disconnected");
        }
        
        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;
    }
}

4.2 양방향 스트리밍

클라이언트와 서버가 동시에 읽기/쓰기합니다. 예: 채팅, 실시간 협업.

// 서버 측
Status BidirectionalStream(
    ServerContext* context,
    grpc::ServerReaderWriter<echo::EchoResponse, echo::EchoRequest>* stream) override {
    echo::EchoRequest request;
    while (stream->Read(&request)) {
        echo::EchoResponse response;
        response.set_message(request.message());
        response.set_sequence(request.sequence());
        stream->Write(response);
    }
    return Status::OK;
}

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

에러 1: “Connection refused” / “Failed to connect”

증상: 클라이언트에서 서버에 연결되지 않음.

원인:

  • 서버가 실행 중이 아님
  • 잘못된 주소/포트
  • 방화벽 차단

해결법:

# 서버 포트 리스닝 확인
netstat -an | grep 50051
# 또는
lsof -i :50051
// 클라이언트: 연결 전 대기
auto channel = grpc::CreateChannel("localhost:50051",
                                   grpc::InsecureChannelCredentials());
// 채널은 lazy 연결. 첫 RPC 시 연결 시도.
// 명시적 대기:
channel->WaitForConnected(
    std::chrono::system_clock::now() + std::chrono::seconds(5));

에러 2: “DEADLINE_EXCEEDED”

증상: RPC 호출이 타임아웃으로 실패.

원인: 서버 처리 시간이 데드라인을 초과.

해결법:

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

// ✅ 서버 측: 장시간 작업 시 주기적으로 IsCancelled() 확인
Status LongRunningRpc(ServerContext* context, ...) {
    for (int i = 0; i < 1000; ++i) {
        if (context->IsCancelled()) {
            return Status(grpc::StatusCode::CANCELLED, "Deadline exceeded");
        }
        DoWork(i);
    }
    return Status::OK;
}

에러 3: “UNAVAILABLE” (서버 재시작 중)

증상: 서버 재배포 중 클라이언트 요청 실패.

원인: 연결이 끊어졌을 때 일시적 UNAVAILABLE 반환.

해결법:

// ✅ 재시도 + 지수 백오프
std::string EchoWithRetry(const std::string& message, int max_retries = 3) {
    for (int i = 0; i < max_retries; ++i) {
        ClientContext context;
        context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));
        
        echo::EchoResponse response;
        Status status = stub_->Echo(&context, request, &response);
        
        if (status.ok()) return response.message();
        
        if (status.error_code() == grpc::StatusCode::UNAVAILABLE) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100 * (1 << i)));
            continue;
        }
        break;  // 재시도 불가능한 에러
    }
    return "";
}

에러 4: “INVALID_ARGUMENT” - Protobuf 필드 누락

증상: request->message() 호출 시 기본값만 반환.

원인: 클라이언트에서 필드를 설정하지 않음. proto3에서는 미설정 시 기본값(빈 문자열, 0 등)이 사용됨.

해결법:

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

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

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

에러 5: “CANCELLED” - 스트리밍 중 클라이언트 종료

증상: 서버 스트리밍 중 Write() 실패 또는 IsCancelled() true.

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

해결법:

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

에러 6: 메모리 누수 - Channel/CompletionQueue 미해제

증상: 장시간 실행 시 메모리 사용량 증가.

원인: grpc::Channel, CompletionQueue를 전역/정적으로 두고 Shutdown 호출 안 함.

해결법:

// ✅ 채널은 스텁과 함께 스코프 내에서 관리
{
    auto channel = grpc::CreateChannel("localhost:50051",
                                       grpc::InsecureChannelCredentials());
    auto stub = echo::EchoService::NewStub(channel);
    // ... RPC 호출
}  // 스코프 종료 시 자동 해제

// ✅ 서버 종료 시
server->Shutdown();
server->Wait();  // Shutdown 완료 대기

에러 7: “Protocol Buffers version mismatch”

증상: 링크 또는 런타임 에러.

원인: protoc로 생성한 코드와 링크되는 libprotobuf 버전 불일치.

해결법:

# protoc와 라이브러리 버전 확인
protoc --version
# libprotobuf 버전은 빌드된 gRPC/Protobuf와 동일하게
# vcpkg 사용 시 동일 소스에서 빌드되므로 일치

에러 8: “ResourceExhausted” - 동시 연결 한도

증상: 부하 시 RESOURCE_EXHAUSTED 또는 연결 실패.

원인: 서버의 최대 동시 스트림/연결 수 초과.

해결법:

// 서버: 동시 처리 수 제한 완화 (기본값 확인)
builder.SetMaxReceiveMessageSize(4 * 1024 * 1024);  // 4MB
builder.SetMaxSendMessageSize(4 * 1024 * 1024);

// 채널: 연결 풀링 (여러 채널을 로드밸런싱)
std::vector<std::shared_ptr<Channel>> channels;
for (const auto& addr : server_addresses) {
    channels.push_back(grpc::CreateChannel(addr, creds));
}
// Round-robin 등으로 스텁 선택

에러 9: 스트리밍 중 “Write() returns false”

증상: writer->Write(response)가 false를 반환.

원인: 클라이언트가 스트림을 닫았거나, 네트워크 오류, 또는 흐름 제어(flow control)로 인한 백프레셔.

해결법:

// ✅ Write 실패 시 즉시 종료
while (has_more_data) {
    if (!writer->Write(response)) {
        LOG(WARNING) << "Client disconnected or flow control";
        break;
    }
}
return Status::OK;

6. 성능 최적화

6.1 채널 재사용

채널은 스레드 안전하며, 여러 RPC에서 재사용해야 합니다. 매 요청마다 새 채널을 만들면 연결 오버헤드가 큽니다.

// ❌ 나쁜 예: 매 요청마다 새 채널
void BadClient() {
    for (int i = 0; i < 1000; ++i) {
        auto channel = grpc::CreateChannel("localhost:50051",
                                           grpc::InsecureChannelCredentials());
        auto stub = echo::EchoService::NewStub(channel);
        // RPC...
    }
}

// ✅ 좋은 예: 채널·스텁 재사용
void GoodClient() {
    auto channel = grpc::CreateChannel("localhost:50051",
                                       grpc::InsecureChannelCredentials());
    auto stub = echo::EchoService::NewStub(channel);
    for (int i = 0; i < 1000; ++i) {
        // RPC...
    }
}

6.2 메시지 재사용 (반복 RPC)

반복 호출 시 Request/Response 객체를 재사용하면 할당 횟수를 줄일 수 있습니다.

// ✅ Response 재사용
echo::EchoResponse response;
for (int i = 0; i < 1000; ++i) {
    request.set_message("msg" + std::to_string(i));
    request.set_sequence(i);
    response.Clear();  // 이전 값 초기화
    stub_->Echo(&context, request, &response);
}

6.3 스트리밍으로 대용량 전송

단일 요청-응답으로 큰 데이터를 보내면 메모리와 직렬화 비용이 큽니다. 청크 단위 스트리밍을 사용합니다.

// 대용량 파일 전송: 청크 스트리밍
message FileChunk {
  bytes data = 1;
  int64 offset = 2;
  bool last_chunk = 3;
}

6.4 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);

6.5 성능 비교 (참고)

방식직렬화대략적 지연 (동일 네트워크)
REST + JSON느림, 페이로드 큼1x 기준
gRPC + Protobuf빠름, 페이로드 작음0.2~0.5x
gRPC + 스트리밍청크 단위, 메모리 효율대용량 시 유리

7. 프로덕션 패턴

7.1 TLS 인증

// 서버: TLS credentials
grpc::SslServerCredentialsOptions::PemKeyCertPair keycert = {
    private_key_content,
    cert_chain_content
};
grpc::SslServerCredentialsOptions ssl_opts;
ssl_opts.pem_key_cert_pairs.push_back(keycert);
auto creds = grpc::SslServerCredentials(ssl_opts);
builder.AddListeningPort(server_address, creds);

// 클라이언트: TLS 채널
auto creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
auto channel = grpc::CreateChannel("myservice.example.com:443", creds);

7.2 메타데이터 (인증 토큰, 트레이싱)

// 클라이언트: 메타데이터 추가
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(grpc::StatusCode::UNAUTHENTICATED, "Missing token");
    }
    // 토큰 검증...
}

7.3 헬스 체크

service EchoService {
  rpc Echo(EchoRequest) returns (EchoResponse);
  rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
}
Status HealthCheck(ServerContext* context,
                  const HealthCheckRequest* request,
                  HealthCheckResponse* response) override {
    if (IsHealthy()) {
        response->set_status(HealthCheckResponse::SERVING);
    } else {
        response->set_status(HealthCheckResponse::NOT_SERVING);
    }
    return Status::OK;
}

7.4 Graceful Shutdown

void RunServer() {
    std::unique_ptr<Server> server(builder.BuildAndStart());
    
    // 시그널 핸들러 등록
    std::signal(SIGINT,  { server->Shutdown(); });
    
    server->Wait();  // Shutdown() 호출 시 반환
}

7.5 로깅·메트릭

// RPC 전후 로깅
Status Echo(ServerContext* context,
            const echo::EchoRequest* request,
            echo::EchoResponse* response) override {
    auto start = std::chrono::steady_clock::now();
    LOG(INFO) << "Echo request: " << request->message();
    
    Status s = DoEcho(request, response);
    
    auto dur = std::chrono::steady_clock::now() - start;
    LOG(INFO) << "Echo completed in " << dur.count() << " ns, status=" << s.error_code();
    return s;
}

7.6 다중 서비스 등록

한 서버에서 여러 gRPC 서비스를 제공할 수 있습니다.

EchoServiceImpl echo_service;
HealthServiceImpl health_service;

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

std::unique_ptr<Server> server(builder.BuildAndStart());

7.7 환경별 설정 (개발/스테이징/프로덕션)

struct GrpcConfig {
    std::string address;
    bool use_tls;
    int deadline_seconds;
};

GrpcConfig LoadConfig() {
    const char* env = std::getenv("GRPC_ENV");
    if (!env || strcmp(env, "production") != 0) {
        return {"localhost:50051", false, 5};  // 개발
    }
    return {"myservice:443", true, 30};  // 프로덕션
}

void RunClient() {
    auto config = LoadConfig();
    auto creds = config.use_tls
        ? grpc::SslCredentials(grpc::SslCredentialsOptions())
        : grpc::InsecureChannelCredentials();
    auto channel = grpc::CreateChannel(config.address, creds);
    // ...
}

7.8 인터셉터 (요청/응답 가로채기)

인증, 로깅, 트레이싱을 중앙에서 처리할 때 사용합니다. 자세한 내용은 gRPC 마스터(#52-2)에서 다룹니다.

// 클라이언트 인터셉터 예시 (개념)
// 모든 RPC에 자동으로 authorization 메타데이터 추가
class AuthInterceptor : public grpc::experimental::Interceptor {
    // Intercept() 오버라이드에서 메타데이터 주입
};

8. 구현 체크리스트

  • Protocol Buffers .proto 정의 (필드 번호 고정)
  • protoc로 C++ 코드 생성
  • CMake/vcpkg로 gRPC·Protobuf 링크
  • 서버: RegisterService, BuildAndStart, Wait
  • 클라이언트: CreateChannel, NewStub, 데드라인 설정
  • 에러 처리: status.ok() 확인, error_message() 로깅
  • 스트리밍: IsCancelled() 주기적 확인
  • 채널·스텁 재사용 (매 요청마다 새 채널 금지)
  • 프로덕션: TLS, 메타데이터 인증, 헬스 체크, Graceful Shutdown

문제 시나리오 해결 요약

문제gRPC 해결 방법
REST 지연·병목Protobuf 바이너리 직렬화, HTTP/2 멀티플렉싱
실시간 로그 스트리밍서버 스트리밍 RPC 표준 지원
스키마 불일치.proto 기반 코드 생성, 필드 번호로 하위 호환
Connection refused포트·방화벽 확인, WaitForConnected 활용
대용량 응답 메모리스트리밍으로 청크 단위 전송
버전 불일치.proto 필드 번호 유지, 새 필드만 추가

정리

항목요약
Protocol Buffers.proto로 스키마 정의 → C++ 코드 생성, 필드 번호로 호환성 유지
gRPC 서버ServerBuilder → RegisterService → BuildAndStart → Wait
gRPC 클라이언트CreateChannel → NewStub → RPC 호출, 데드라인 필수
스트리밍ServerWriter/Reader, ClientReader/Writer, IsCancelled() 확인
에러DEADLINE_EXCEEDED, UNAVAILABLE 재시도, CANCELLED 처리
성능채널 재사용, 메시지 재사용, 스트리밍, Keepalive
프로덕션TLS, 메타데이터, 헬스 체크, Graceful Shutdown

자주 묻는 질문 (FAQ)

Q. REST 대신 gRPC를 써야 할 때는?

A. 마이크로서비스 간 통신, 고성능이 필요한 내부 API, 스트리밍이 필요한 경우에 gRPC가 유리합니다. 브라우저에서 직접 호출해야 하면 REST/JSON이 편합니다.

Q. C++ 말고 다른 언어와 통신할 수 있나요?

A. 네. gRPC는 다국어를 지원합니다. 같은 .proto에서 C++, Go, Java, Python 등 클라이언트/서버를 생성할 수 있습니다.

Q. 비동기 API는 언제 쓰나요?

A. 고부하 서버에서 동시에 많은 RPC를 처리할 때 CompletionQueue 기반 비동기 API를 사용합니다. gRPC 마스터(#52-2)에서 다룹니다.

한 줄 요약: gRPC·Protobuf로 타입 안전하고 고성능한 마이크로서비스 RPC를 구축할 수 있습니다.

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

이전 글: C++ 시리즈 목차


관련 글

  • C++ gRPC 기초 완벽 가이드 | Protocol Buffers·Unary·스트리밍·실전 문제 해결
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3