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++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 환경 설정
- Protocol Buffers 기초
- Unary RPC 완전한 예제
- 서버 스트리밍
- 클라이언트 스트리밍
- 양방향 스트리밍
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
- 정리
1. 환경 설정
필수 의존성
| 항목 | 버전 | 비고 |
|---|---|---|
| C++ | C++14 이상 | C++17 권장 |
| gRPC | 1.50+ | vcpkg 또는 소스 빌드 |
| Protocol Buffers | 3.21+ | gRPC와 버전 호환 확인 |
| CMake | 3.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 Buffers는 IDL(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 성능 비교 (참고)
| 항목 | JSON | Protobuf |
|---|---|---|
| 직렬화 크기 | 100% | 약 30~50% |
| 직렬화 속도 | 1x | 약 5~10x 빠름 |
| 역직렬화 속도 | 1x | 약 5~10x 빠름 |
10. 구현 체크리스트
Protocol Buffers
-
.proto에syntax = "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 완벽 가이드 | 직렬화·스키마 진화·성능 최적화·프로덕션 패턴