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 대비 3
10배 작은 페이로드, 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 + JSON | gRPC + Protobuf |
|---|---|---|
| 직렬화 | 텍스트, 느림 | 바이너리, 빠름 |
| 스키마 | OpenAPI 등 별도 | .proto에 통합 |
| 스트리밍 | WebSocket 별도 구현 | 표준 지원 |
| HTTP | HTTP/1.1 (기본) | HTTP/2 (멀티플렉싱) |
이 글에서 다루는 것:
- Protocol Buffers: .proto 정의·코드 생성
- 완전한 gRPC C++ 서버·클라이언트 예제
- 자주 발생하는 에러와 해결법
- 성능 최적화 팁
- 프로덕션 배포 패턴
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
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
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·스트리밍·실전 문제 해결