- Published on
gRPC & Protocol Buffers 완전 가이드 2025: 마이크로서비스 통신의 새로운 표준
- Authors

- Name
- Youngju Kim
- @fjvbn20031
목차
1. REST vs gRPC: 왜 gRPC인가?
마이크로서비스 아키텍처에서 서비스 간 통신은 시스템의 성능과 안정성을 좌우하는 핵심 요소입니다. REST API는 단순함과 범용성으로 오랫동안 표준이었지만, 서비스 수가 증가하면서 그 한계가 드러나기 시작했습니다.
1.1 REST의 한계
REST 기반 JSON 통신의 문제점은 크게 세 가지입니다.
첫째, 직렬화/역직렬화 오버헤드입니다. JSON은 텍스트 기반이라 파싱 비용이 높고, 데이터 크기가 바이너리 포맷 대비 3~10배 큽니다. 초당 수만 건의 요청을 처리하는 내부 서비스 통신에서 이 차이는 무시할 수 없습니다.
둘째, 스키마 부재입니다. OpenAPI/Swagger로 문서화할 수 있지만 강제성이 없고, 클라이언트-서버 간 계약이 느슨합니다. API가 변경되면 런타임에서야 오류를 발견하게 됩니다.
셋째, HTTP/1.1의 제약입니다. Head-of-Line Blocking, 연결당 하나의 요청, 헤더 압축 부재 등으로 고성능 통신에 부적합합니다.
1.2 비교 표
| 항목 | REST (JSON) | gRPC (Protobuf) |
|---|---|---|
| 프로토콜 | HTTP/1.1 (주로) | HTTP/2 |
| 데이터 포맷 | JSON (텍스트) | Protocol Buffers (바이너리) |
| 스키마 | 선택적 (OpenAPI) | 필수 (.proto) |
| 코드 생성 | 선택적 | 자동 (다국어) |
| 스트리밍 | 제한적 (SSE, WebSocket) | 네이티브 지원 (4가지 패턴) |
| 브라우저 지원 | 네이티브 | gRPC-Web 필요 |
| 직렬화 속도 | 느림 | 빠름 (10배) |
| 페이로드 크기 | 큼 | 작음 (3~10배) |
| 학습 곡선 | 낮음 | 중간 |
| 디버깅 | 쉬움 (사람이 읽을 수 있음) | 어려움 (바이너리) |
1.3 언제 무엇을 쓸 것인가?
REST를 선택하는 경우:
- 브라우저 클라이언트가 주요 소비자
- 공개 API로 외부 개발자에게 제공
- 단순 CRUD 작업 위주
- 팀의 gRPC 경험이 부족
gRPC를 선택하는 경우:
- 마이크로서비스 간 내부 통신
- 실시간 스트리밍이 필요
- 고성능/저지연이 핵심 요구사항
- 다양한 언어의 클라이언트 지원
- 강한 타입 안정성이 필요
2. Protocol Buffers 기초
Protocol Buffers(Protobuf)는 Google이 개발한 언어 중립, 플랫폼 중립 바이너리 직렬화 포맷입니다. gRPC의 기본 IDL(Interface Definition Language)이자 직렬화 메커니즘입니다.
2.1 .proto 파일 기본 구조
syntax = "proto3";
package ecommerce.v1;
option go_package = "github.com/mycompany/ecommerce/v1;ecommercev1";
option java_package = "com.mycompany.ecommerce.v1";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// 상품 서비스 정의
service ProductService {
// 단일 상품 조회
rpc GetProduct(GetProductRequest) returns (Product);
// 상품 목록 조회
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
// 상품 생성
rpc CreateProduct(CreateProductRequest) returns (Product);
// 상품 업데이트
rpc UpdateProduct(UpdateProductRequest) returns (Product);
// 상품 삭제
rpc DeleteProduct(DeleteProductRequest) returns (google.protobuf.Empty);
}
2.2 스칼라 타입
message ScalarTypes {
// 숫자 타입
double price = 1; // 64비트 부동소수점
float rating = 2; // 32비트 부동소수점
int32 quantity = 3; // 가변 길이 인코딩 (음수에 비효율적)
int64 total_sales = 4; // 가변 길이 인코딩
uint32 age = 5; // 부호 없는 32비트 정수
uint64 view_count = 6; // 부호 없는 64비트 정수
sint32 temperature = 7; // 음수에 효율적인 인코딩
sint64 altitude = 8; // 음수에 효율적인 인코딩
fixed32 hash = 9; // 항상 4바이트
fixed64 large_hash = 10; // 항상 8바이트
// 문자열/바이트
string name = 11; // UTF-8 인코딩 문자열
bytes thumbnail = 12; // 임의 바이트 시퀀스
// 불리언
bool is_active = 13;
}
2.3 메시지 타입과 중첩
message Product {
string id = 1;
string name = 2;
string description = 3;
Money price = 4;
Category category = 5;
repeated string tags = 6; // 반복 필드 (배열)
map<string, string> metadata = 7; // 맵 타입
google.protobuf.Timestamp created_at = 8;
ProductStatus status = 9;
// 중첩 메시지
message Dimension {
double width = 1;
double height = 2;
double depth = 3;
string unit = 4;
}
Dimension dimension = 10;
}
message Money {
string currency_code = 1; // ISO 4217
int64 units = 2; // 정수 부분
int32 nanos = 3; // 나노 단위 소수 부분
}
2.4 Enum 타입
enum ProductStatus {
PRODUCT_STATUS_UNSPECIFIED = 0; // 항상 0번은 기본값
PRODUCT_STATUS_DRAFT = 1;
PRODUCT_STATUS_ACTIVE = 2;
PRODUCT_STATUS_ARCHIVED = 3;
PRODUCT_STATUS_DELETED = 4;
}
enum Category {
CATEGORY_UNSPECIFIED = 0;
CATEGORY_ELECTRONICS = 1;
CATEGORY_CLOTHING = 2;
CATEGORY_BOOKS = 3;
CATEGORY_FOOD = 4;
}
2.5 Oneof 타입
message PaymentMethod {
string id = 1;
oneof method {
CreditCard credit_card = 2;
BankTransfer bank_transfer = 3;
DigitalWallet digital_wallet = 4;
}
}
message CreditCard {
string card_number_masked = 1;
string expiry_month = 2;
string expiry_year = 3;
string brand = 4;
}
message BankTransfer {
string bank_name = 1;
string account_number_masked = 2;
string routing_number = 3;
}
message DigitalWallet {
string provider = 1; // "apple_pay", "google_pay"
string email = 2;
}
2.6 필드 번호와 버전 관리
Protocol Buffers에서 필드 번호는 와이어 포맷에서 필드를 식별하는 핵심 요소입니다.
필드 번호 규칙:
- 1~15: 1바이트로 인코딩 (자주 사용하는 필드에 할당)
- 16~2047: 2바이트로 인코딩
- 19000~19999: 예약된 범위 (사용 불가)
하위 호환성 유지 규칙:
message Product {
string id = 1;
string name = 2;
// 삭제된 필드: 번호와 이름을 예약
reserved 3, 6 to 8;
reserved "old_price", "legacy_tag";
// 새 필드 추가는 항상 안전
string description = 4;
Money price = 5;
// 필드 9부터 추가
string sku = 9;
}
버전 관리 전략:
- 필드 추가: 항상 안전 (새 필드를 모르는 클라이언트는 무시)
- 필드 제거: reserved로 번호 예약 후 제거
- 필드 타입 변경: 절대 하지 않음 (새 필드 추가)
- 패키지 버전 분리:
ecommerce.v1,ecommerce.v2
3. 4가지 통신 패턴
gRPC는 HTTP/2 기반으로 네 가지 통신 패턴을 지원합니다.
3.1 Unary RPC (단항)
가장 기본적인 패턴으로, 클라이언트가 하나의 요청을 보내고 서버가 하나의 응답을 반환합니다.
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
string user_id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
Go 서버 구현:
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.repo.FindByID(ctx, req.UserId)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "user %s not found", req.UserId)
}
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
return toProtoUser(user), nil
}
Node.js 클라이언트:
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDefinition);
const client = new proto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary 호출
client.getUser({ user_id: 'user-123' }, (err, response) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('User:', response);
});
3.2 Server Streaming RPC (서버 스트리밍)
클라이언트가 하나의 요청을 보내면 서버가 스트림으로 여러 메시지를 반환합니다.
service OrderService {
// 실시간 주문 상태 추적
rpc TrackOrder(TrackOrderRequest) returns (stream OrderStatus);
}
message TrackOrderRequest {
string order_id = 1;
}
message OrderStatus {
string order_id = 1;
string status = 2;
string location = 3;
google.protobuf.Timestamp updated_at = 4;
}
Go 서버 구현:
func (s *orderServer) TrackOrder(req *pb.TrackOrderRequest, stream pb.OrderService_TrackOrderServer) error {
orderID := req.OrderId
// 이벤트 채널 구독
events := s.eventBus.Subscribe(orderID)
defer s.eventBus.Unsubscribe(orderID, events)
for {
select {
case event := <-events:
status := &pb.OrderStatus{
OrderId: orderID,
Status: event.Status,
Location: event.Location,
UpdatedAt: timestamppb.Now(),
}
if err := stream.Send(status); err != nil {
return err
}
if event.Status == "delivered" {
return nil
}
case <-stream.Context().Done():
return stream.Context().Err()
}
}
}
3.3 Client Streaming RPC (클라이언트 스트리밍)
클라이언트가 여러 메시지를 스트림으로 보내고 서버가 하나의 응답을 반환합니다.
service UploadService {
// 파일 업로드
rpc UploadFile(stream FileChunk) returns (UploadResponse);
}
message FileChunk {
string filename = 1;
bytes content = 2;
int64 offset = 3;
}
message UploadResponse {
string file_id = 1;
int64 total_size = 2;
string checksum = 3;
}
Go 서버 구현:
func (s *uploadServer) UploadFile(stream pb.UploadService_UploadFileServer) error {
var totalSize int64
var filename string
buffer := bytes.Buffer{}
for {
chunk, err := stream.Recv()
if err == io.EOF {
// 모든 청크 수신 완료
fileID, checksum := s.storage.Save(filename, buffer.Bytes())
return stream.SendAndClose(&pb.UploadResponse{
FileId: fileID,
TotalSize: totalSize,
Checksum: checksum,
})
}
if err != nil {
return status.Errorf(codes.Internal, "failed to receive chunk: %v", err)
}
filename = chunk.Filename
totalSize += int64(len(chunk.Content))
buffer.Write(chunk.Content)
}
}
3.4 Bidirectional Streaming RPC (양방향 스트리밍)
클라이언트와 서버가 동시에 메시지를 주고받습니다. 실시간 채팅, 게임, 주식 시세 등에 적합합니다.
service ChatService {
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user_id = 1;
string room_id = 2;
string content = 3;
google.protobuf.Timestamp sent_at = 4;
MessageType type = 5;
}
enum MessageType {
MESSAGE_TYPE_UNSPECIFIED = 0;
MESSAGE_TYPE_TEXT = 1;
MESSAGE_TYPE_IMAGE = 2;
MESSAGE_TYPE_SYSTEM = 3;
}
Go 서버 구현:
func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// 같은 방의 모든 클라이언트에게 브로드캐스트
s.mu.RLock()
clients := s.rooms[msg.RoomId]
s.mu.RUnlock()
for _, client := range clients {
if err := client.Send(msg); err != nil {
log.Printf("failed to send to client: %v", err)
}
}
}
}
4. HTTP/2 멀티플렉싱
gRPC는 HTTP/2 위에 구축되어 다음 기능을 활용합니다.
4.1 HTTP/2의 핵심 특징
멀티플렉싱: 하나의 TCP 연결에서 여러 요청/응답을 동시에 처리합니다. HTTP/1.1의 Head-of-Line Blocking 문제를 해결합니다.
헤더 압축 (HPACK): 중복 헤더를 제거하고 허프만 인코딩으로 압축합니다. 반복 요청에서 헤더 오버헤드를 크게 줄입니다.
서버 푸시: 서버가 클라이언트 요청 없이도 데이터를 보낼 수 있습니다. gRPC 스트리밍의 기반입니다.
바이너리 프레이밍: HTTP/1.1의 텍스트 프로토콜과 달리, HTTP/2는 바이너리 프레임으로 통신합니다.
HTTP/2 커넥션
├── Stream 1: GetUser RPC
├── Stream 3: ListProducts RPC (동시 실행)
├── Stream 5: TrackOrder RPC (서버 스트리밍)
└── Stream 7: Chat RPC (양방향 스트리밍)
4.2 커넥션 관리
// 서버 설정
server := grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionIdle: 15 * time.Minute,
MaxConnectionAge: 30 * time.Minute,
MaxConnectionAgeGrace: 5 * time.Minute,
Time: 5 * time.Minute,
Timeout: 1 * time.Minute,
}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 5 * time.Second,
PermitWithoutStream: true,
}),
grpc.MaxRecvMsgSize(4 * 1024 * 1024), // 4MB
grpc.MaxSendMsgSize(4 * 1024 * 1024), // 4MB
)
5. 인터셉터 (Middleware)
인터셉터는 gRPC의 미들웨어로, 요청 전후에 공통 로직을 실행할 수 있습니다.
5.1 Unary 인터셉터
// 로깅 인터셉터
func loggingUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// 요청 로깅
log.Printf("gRPC call: %s", info.FullMethod)
// 핸들러 실행
resp, err := handler(ctx, req)
// 응답 로깅
duration := time.Since(start)
statusCode := status.Code(err)
log.Printf("gRPC response: method=%s duration=%v status=%s",
info.FullMethod, duration, statusCode)
return resp, err
}
// 인증 인터셉터
func authUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// 공개 메서드는 인증 건너뛰기
if isPublicMethod(info.FullMethod) {
return handler(ctx, req)
}
// 메타데이터에서 토큰 추출
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "missing token")
}
// 토큰 검증
claims, err := validateToken(tokens[0])
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
// 컨텍스트에 사용자 정보 추가
ctx = context.WithValue(ctx, userClaimsKey, claims)
return handler(ctx, req)
}
5.2 스트림 인터셉터
func loggingStreamInterceptor(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
start := time.Now()
log.Printf("gRPC stream started: %s", info.FullMethod)
// 래핑된 스트림으로 메시지 카운트
wrapped := &wrappedStream{
ServerStream: ss,
recvCount: 0,
sendCount: 0,
}
err := handler(srv, wrapped)
log.Printf("gRPC stream ended: method=%s duration=%v recv=%d send=%d",
info.FullMethod, time.Since(start), wrapped.recvCount, wrapped.sendCount)
return err
}
type wrappedStream struct {
grpc.ServerStream
recvCount int
sendCount int
}
func (w *wrappedStream) RecvMsg(m interface{}) error {
err := w.ServerStream.RecvMsg(m)
if err == nil {
w.recvCount++
}
return err
}
func (w *wrappedStream) SendMsg(m interface{}) error {
err := w.ServerStream.SendMsg(m)
if err == nil {
w.sendCount++
}
return err
}
5.3 인터셉터 체인
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
recoveryUnaryInterceptor, // 패닉 복구 (가장 바깥)
loggingUnaryInterceptor, // 로깅
metricsUnaryInterceptor, // 메트릭 수집
authUnaryInterceptor, // 인증
rateLimitUnaryInterceptor, // 속도 제한
validationUnaryInterceptor, // 입력 검증 (가장 안쪽)
),
grpc.ChainStreamInterceptor(
recoveryStreamInterceptor,
loggingStreamInterceptor,
authStreamInterceptor,
),
)
6. 에러 처리와 상태 코드
6.1 gRPC 상태 코드
| 코드 | 이름 | 설명 | HTTP 매핑 |
|---|---|---|---|
| 0 | OK | 성공 | 200 |
| 1 | CANCELLED | 요청 취소 | 499 |
| 2 | UNKNOWN | 알 수 없는 에러 | 500 |
| 3 | INVALID_ARGUMENT | 잘못된 인자 | 400 |
| 4 | DEADLINE_EXCEEDED | 타임아웃 | 504 |
| 5 | NOT_FOUND | 리소스 없음 | 404 |
| 6 | ALREADY_EXISTS | 이미 존재 | 409 |
| 7 | PERMISSION_DENIED | 권한 부족 | 403 |
| 8 | RESOURCE_EXHAUSTED | 리소스 소진 | 429 |
| 13 | INTERNAL | 내부 서버 에러 | 500 |
| 14 | UNAVAILABLE | 서비스 불가 | 503 |
| 16 | UNAUTHENTICATED | 인증 실패 | 401 |
6.2 풍부한 에러 응답
import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/status"
)
func (s *productServer) CreateProduct(ctx context.Context, req *pb.CreateProductRequest) (*pb.Product, error) {
// 입력 검증
violations := validateCreateProduct(req)
if len(violations) > 0 {
st := status.New(codes.InvalidArgument, "invalid product data")
br := &errdetails.BadRequest{}
for _, v := range violations {
br.FieldViolations = append(br.FieldViolations, &errdetails.BadRequest_FieldViolation{
Field: v.Field,
Description: v.Description,
})
}
st, err := st.WithDetails(br)
if err != nil {
return nil, status.Errorf(codes.Internal, "unexpected error: %v", err)
}
return nil, st.Err()
}
// 비즈니스 로직
product, err := s.repo.Create(ctx, req)
if err != nil {
if isDuplicate(err) {
st := status.New(codes.AlreadyExists, "product already exists")
ri := &errdetails.ResourceInfo{
ResourceType: "Product",
ResourceName: req.Sku,
Description: "A product with this SKU already exists",
}
st, _ = st.WithDetails(ri)
return nil, st.Err()
}
return nil, status.Errorf(codes.Internal, "failed to create product: %v", err)
}
return product, nil
}
7. Deadline과 Timeout
7.1 Deadline 전파
// 클라이언트: deadline 설정
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetProduct(ctx, &pb.GetProductRequest{ProductId: "prod-123"})
if err != nil {
st := status.Convert(err)
if st.Code() == codes.DeadlineExceeded {
log.Println("Request timed out")
}
}
// 서버: deadline 확인
func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {
// deadline 확인
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
if remaining < 100*time.Millisecond {
return nil, status.Errorf(codes.DeadlineExceeded, "insufficient time remaining")
}
}
// 하위 서비스 호출 시 남은 시간으로 deadline 전파
product, err := s.repo.FindByID(ctx, req.ProductId)
if err != nil {
return nil, err
}
// 가격 서비스에 남은 시간의 80%만 할당
remaining := time.Until(deadline)
priceCtx, cancel := context.WithTimeout(ctx, remaining*80/100)
defer cancel()
price, err := s.priceClient.GetPrice(priceCtx, &pb.GetPriceRequest{
ProductId: req.ProductId,
})
if err != nil {
// 가격 조회 실패 시 캐시된 가격 사용
price = s.priceCache.Get(req.ProductId)
}
product.Price = price
return product, nil
}
8. 로드 밸런싱
8.1 클라이언트 사이드 로드 밸런싱
import (
"google.golang.org/grpc"
"google.golang.org/grpc/resolver"
_ "google.golang.org/grpc/balancer/roundrobin"
)
// 커스텀 리졸버 등록
resolver.Register(&myResolver{})
conn, err := grpc.Dial(
"my-service:///product-service",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
8.2 프록시 기반 로드 밸런싱 (Envoy)
# envoy.yaml
static_resources:
listeners:
- name: grpc_listener
address:
socket_address:
address: 0.0.0.0
port_value: 9090
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: grpc
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: grpc_service
domains: ["*"]
routes:
- match:
prefix: "/"
grpc: {}
route:
cluster: grpc_backend
timeout: 30s
retry_policy:
retry_on: "unavailable,resource-exhausted"
num_retries: 3
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: grpc_backend
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
load_assignment:
cluster_name: grpc_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: product-service
port_value: 50051
9. gRPC-Web: 브라우저에서 gRPC 사용
9.1 gRPC-Web이 필요한 이유
브라우저는 HTTP/2 프레이밍을 직접 제어할 수 없으므로, gRPC를 네이티브로 사용할 수 없습니다. gRPC-Web은 이 간극을 메우는 프록시 계층입니다.
9.2 Envoy를 프록시로 사용
# envoy-grpc-web.yaml
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
9.3 TypeScript 클라이언트 (Connect-Web)
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { createClient } from "@connectrpc/connect";
import { ProductService } from "./gen/product_connect";
const transport = createGrpcWebTransport({
baseUrl: "https://api.example.com",
});
const client = createClient(ProductService, transport);
// Unary 호출
async function getProduct(id: string) {
try {
const product = await client.getProduct({ productId: id });
console.log("Product:", product.name, product.price);
} catch (err) {
if (err instanceof ConnectError) {
console.error("gRPC Error:", err.code, err.message);
}
}
}
// Server Streaming
async function trackOrder(orderId: string) {
for await (const status of client.trackOrder({ orderId })) {
console.log("Order status:", status.status, status.location);
updateUI(status);
}
}
10. 리플렉션과 헬스 체크
10.1 서버 리플렉션
import "google.golang.org/grpc/reflection"
func main() {
server := grpc.NewServer()
pb.RegisterProductServiceServer(server, &productServer{})
// 리플렉션 활성화 (개발 환경용)
reflection.Register(server)
lis, _ := net.Listen("tcp", ":50051")
server.Serve(lis)
}
10.2 헬스 체킹
import "google.golang.org/grpc/health"
import healthpb "google.golang.org/grpc/health/grpc_health_v1"
func main() {
server := grpc.NewServer()
// 헬스 체크 서비스 등록
healthServer := health.NewServer()
healthpb.RegisterHealthServer(server, healthServer)
// 서비스 상태 설정
healthServer.SetServingStatus("product.v1.ProductService", healthpb.HealthCheckResponse_SERVING)
// 주기적 헬스 체크
go func() {
for {
if dbHealthy() {
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
} else {
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
}
time.Sleep(10 * time.Second)
}
}()
}
10.3 Kubernetes 헬스 프로브
# kubernetes deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
spec:
template:
spec:
containers:
- name: product-service
image: product-service:latest
ports:
- containerPort: 50051
name: grpc
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
periodSeconds: 5
11. 서비스 메시 통합
11.1 Istio와 gRPC
# istio virtual service
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: product-service
spec:
hosts:
- product-service
http:
- match:
- headers:
content-type:
exact: application/grpc
route:
- destination:
host: product-service
subset: v2
weight: 90
- destination:
host: product-service
subset: v1
weight: 10
retries:
attempts: 3
perTryTimeout: 2s
retryOn: "unavailable,resource-exhausted"
timeout: 10s
11.2 Linkerd와 gRPC
Linkerd는 HTTP/2를 네이티브로 지원하므로 gRPC 트래픽을 자동으로 인식합니다.
# linkerd service profile
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: product-service.default.svc.cluster.local
spec:
routes:
- name: GetProduct
condition:
method: POST
pathRegex: /ecommerce.v1.ProductService/GetProduct
responseClasses:
- condition:
status:
min: 200
max: 299
- name: ListProducts
condition:
method: POST
pathRegex: /ecommerce.v1.ProductService/ListProducts
isRetryable: true
12. 테스팅
12.1 grpcurl로 테스트
# 서비스 목록 조회 (리플렉션 필요)
grpcurl -plaintext localhost:50051 list
# 서비스 메서드 목록
grpcurl -plaintext localhost:50051 list ecommerce.v1.ProductService
# 메서드 설명
grpcurl -plaintext localhost:50051 describe ecommerce.v1.ProductService.GetProduct
# Unary 호출
grpcurl -plaintext \
-d '{"product_id": "prod-123"}' \
localhost:50051 ecommerce.v1.ProductService/GetProduct
# 헤더 포함 호출
grpcurl -plaintext \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-d '{"name": "New Product", "price": {"currency_code": "USD", "units": 29, "nanos": 990000000}}' \
localhost:50051 ecommerce.v1.ProductService/CreateProduct
12.2 Go에서 통합 테스트
func TestGetProduct(t *testing.T) {
// 인메모리 gRPC 서버
lis := bufconn.Listen(1024 * 1024)
server := grpc.NewServer()
pb.RegisterProductServiceServer(server, newTestProductServer())
go func() {
if err := server.Serve(lis); err != nil {
t.Fatal(err)
}
}()
defer server.Stop()
// 클라이언트 연결
conn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)
defer conn.Close()
client := pb.NewProductServiceClient(conn)
// 테스트 실행
resp, err := client.GetProduct(context.Background(), &pb.GetProductRequest{
ProductId: "test-product-1",
})
require.NoError(t, err)
assert.Equal(t, "test-product-1", resp.Id)
assert.Equal(t, "Test Product", resp.Name)
}
12.3 evans 대화형 클라이언트
# evans REPL 모드
evans --host localhost --port 50051 -r repl
# 패키지 선택
> package ecommerce.v1
# 서비스 선택
> service ProductService
# RPC 호출
> call GetProduct
product_id (TYPE_STRING) => prod-123
# 결과가 JSON으로 출력됨
13. 성능 벤치마크
13.1 Protobuf vs JSON 성능 비교
func BenchmarkProtobufMarshal(b *testing.B) {
product := createSampleProduct()
b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Marshal(product)
}
}
func BenchmarkJSONMarshal(b *testing.B) {
product := createSampleProductJSON()
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(product)
}
}
일반적인 벤치마크 결과:
| 메트릭 | Protobuf | JSON | 비율 |
|---|---|---|---|
| 직렬화 속도 | 150ns | 1500ns | 10x 빠름 |
| 역직렬화 속도 | 200ns | 2000ns | 10x 빠름 |
| 데이터 크기 | 68 bytes | 220 bytes | 3.2x 작음 |
| 메모리 할당 | 1 alloc | 8 allocs | 8x 적음 |
13.2 gRPC vs REST 처리량
| 시나리오 | gRPC (req/s) | REST (req/s) | gRPC 이점 |
|---|---|---|---|
| 작은 페이로드 (100B) | 45,000 | 15,000 | 3x |
| 중간 페이로드 (1KB) | 35,000 | 10,000 | 3.5x |
| 큰 페이로드 (100KB) | 8,000 | 2,500 | 3.2x |
| 스트리밍 (10만 메시지) | 150,000/s | N/A | - |
14. 실전 마이크로서비스 구현 예제
14.1 프로젝트 구조
ecommerce-grpc/
├── proto/
│ ├── buf.yaml
│ ├── buf.gen.yaml
│ └── ecommerce/
│ └── v1/
│ ├── product.proto
│ ├── order.proto
│ ├── payment.proto
│ └── common.proto
├── services/
│ ├── product/
│ │ ├── main.go
│ │ ├── server.go
│ │ ├── repository.go
│ │ └── server_test.go
│ ├── order/
│ │ ├── main.go
│ │ ├── server.go
│ │ └── saga.go
│ └── payment/
│ ├── main.go
│ └── server.go
├── gen/
│ └── ecommerce/
│ └── v1/
├── docker-compose.yaml
└── Makefile
14.2 Buf를 이용한 코드 생성
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/protocolbuffers/go
out: gen
opt:
- paths=source_relative
- remote: buf.build/grpc/go
out: gen
opt:
- paths=source_relative
- require_unimplemented_servers=false
- remote: buf.build/connectrpc/go
out: gen
opt:
- paths=source_relative
# 코드 생성
buf generate proto
# 린트
buf lint proto
# 하위 호환성 검사
buf breaking proto --against '.git#branch=main'
15. 퀴즈
아래 퀴즈로 gRPC와 Protocol Buffers에 대한 이해도를 점검해보세요.
Q1: gRPC가 HTTP/2를 사용함으로써 얻는 가장 큰 이점은 무엇인가요?
정답: 멀티플렉싱 (Multiplexing)
HTTP/2의 멀티플렉싱을 통해 하나의 TCP 연결에서 여러 gRPC 호출을 동시에 처리할 수 있습니다. HTTP/1.1에서는 연결당 하나의 요청만 처리할 수 있어 Head-of-Line Blocking이 발생하지만, HTTP/2에서는 여러 스트림이 독립적으로 동작합니다. 이 외에도 헤더 압축(HPACK), 바이너리 프레이밍, 서버 푸시 등의 이점이 있습니다.
Q2: Protocol Buffers에서 필드 번호 1~15를 자주 사용하는 필드에 할당하는 이유는?
정답: 인코딩 크기가 작기 때문
필드 번호 115는 1바이트로 인코딩되고, 162047은 2바이트로 인코딩됩니다. 자주 사용하는 필드에 낮은 번호를 부여하면 전체 메시지 크기를 줄일 수 있습니다. 또한 repeated 필드처럼 여러 번 나타나는 필드는 특히 낮은 번호의 혜택이 큽니다.
Q3: Bidirectional Streaming과 Server Streaming의 차이점은 무엇인가요?
정답: 클라이언트도 스트림으로 메시지를 보낼 수 있는지 여부
Server Streaming에서는 클라이언트가 하나의 요청을 보내면 서버가 스트림으로 응답합니다. Bidirectional Streaming에서는 클라이언트와 서버가 독립적인 스트림으로 동시에 메시지를 주고받을 수 있습니다. 두 스트림은 독립적이므로 서버가 클라이언트의 모든 메시지를 받을 때까지 기다릴 필요 없이 응답할 수 있습니다.
Q4: gRPC에서 deadline이 중요한 이유는 무엇인가요?
정답: 분산 시스템에서 요청 타임아웃을 전파하여 리소스 낭비를 방지
deadline은 클라이언트가 설정한 최대 대기 시간으로, gRPC는 이를 자동으로 하위 서비스 호출에 전파합니다. 서비스 A가 서비스 B를 호출하고, B가 C를 호출할 때 A의 deadline이 이미 지났다면 B와 C의 작업은 의미가 없습니다. deadline 전파를 통해 이런 불필요한 작업을 방지합니다.
Q5: gRPC-Web이 필요한 이유는 무엇인가요?
정답: 브라우저가 HTTP/2 프레이밍을 직접 제어할 수 없기 때문
브라우저의 Fetch API와 XMLHttpRequest는 HTTP/2 프레임 수준의 제어를 지원하지 않습니다. gRPC는 HTTP/2의 트레일러(trailers)를 사용하여 상태 코드를 전달하는데, 브라우저에서는 이를 읽을 수 없습니다. gRPC-Web은 프록시(Envoy 등)를 통해 gRPC 프로토콜을 브라우저가 이해할 수 있는 형태로 변환합니다.
16. 참고 자료
- gRPC 공식 문서 - gRPC 공식 가이드 및 튜토리얼
- Protocol Buffers Language Guide - Protobuf proto3 문법 가이드
- gRPC Go Tutorial - Go 언어 gRPC 튜토리얼
- Buf - Protobuf 도구 - 현대적인 Protobuf 관리 도구
- gRPC-Web GitHub - gRPC-Web 공식 저장소
- Connect-RPC - 브라우저 호환 gRPC 프레임워크
- Envoy gRPC 설정 - Envoy의 gRPC 지원
- grpcurl GitHub - gRPC 커맨드라인 도구
- evans GitHub - gRPC 대화형 클라이언트
- Istio gRPC Traffic Management - Istio와 gRPC 통합
- gRPC Health Checking Protocol - 헬스 체크 프로토콜 명세
- Google API Design Guide - Google의 API 설계 가이드
- gRPC Performance Best Practices - gRPC 성능 최적화 가이드