Split View: gRPC & Protocol Buffers 완전 가이드 2025: 마이크로서비스 통신의 새로운 표준
gRPC & Protocol Buffers 완전 가이드 2025: 마이크로서비스 통신의 새로운 표준
목차
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 성능 최적화 가이드
gRPC & Protocol Buffers Complete Guide 2025: The New Standard for Microservices Communication
Table of Contents
1. REST vs gRPC: Why gRPC?
In microservices architecture, inter-service communication is a critical factor that determines system performance and reliability. REST APIs have long been the standard due to their simplicity and universality, but their limitations become apparent as the number of services increases.
1.1 Limitations of REST
There are three major issues with REST-based JSON communication.
First, serialization/deserialization overhead. JSON is text-based, making parsing expensive, and data sizes are 3-10x larger than binary formats. This difference is significant for internal service communication handling tens of thousands of requests per second.
Second, lack of schema enforcement. While you can document with OpenAPI/Swagger, there is no enforcement, and the contract between client and server is loose. When APIs change, errors are only discovered at runtime.
Third, HTTP/1.1 constraints. Head-of-Line Blocking, one request per connection, and no header compression make it unsuitable for high-performance communication.
1.2 Comparison Table
| Aspect | REST (JSON) | gRPC (Protobuf) |
|---|---|---|
| Protocol | HTTP/1.1 (mostly) | HTTP/2 |
| Data Format | JSON (text) | Protocol Buffers (binary) |
| Schema | Optional (OpenAPI) | Required (.proto) |
| Code Generation | Optional | Automatic (multi-language) |
| Streaming | Limited (SSE, WebSocket) | Native support (4 patterns) |
| Browser Support | Native | Requires gRPC-Web |
| Serialization Speed | Slow | Fast (10x) |
| Payload Size | Large | Small (3-10x) |
| Learning Curve | Low | Medium |
| Debugging | Easy (human-readable) | Difficult (binary) |
1.3 When to Use What
Choose REST when:
- Browser clients are the primary consumers
- Public API for external developers
- Simple CRUD operations
- Team lacks gRPC experience
Choose gRPC when:
- Internal microservice communication
- Real-time streaming is needed
- High performance and low latency are critical
- Supporting clients in multiple languages
- Strong type safety is required
2. Protocol Buffers Fundamentals
Protocol Buffers (Protobuf) is a language-neutral, platform-neutral binary serialization format developed by Google. It serves as the default IDL (Interface Definition Language) and serialization mechanism for gRPC.
2.1 Basic .proto File Structure
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";
// Product service definition
service ProductService {
// Get a single product
rpc GetProduct(GetProductRequest) returns (Product);
// List products
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
// Create a product
rpc CreateProduct(CreateProductRequest) returns (Product);
// Update a product
rpc UpdateProduct(UpdateProductRequest) returns (Product);
// Delete a product
rpc DeleteProduct(DeleteProductRequest) returns (google.protobuf.Empty);
}
2.2 Scalar Types
message ScalarTypes {
// Numeric types
double price = 1; // 64-bit floating point
float rating = 2; // 32-bit floating point
int32 quantity = 3; // Variable-length encoding (inefficient for negatives)
int64 total_sales = 4; // Variable-length encoding
uint32 age = 5; // Unsigned 32-bit integer
uint64 view_count = 6; // Unsigned 64-bit integer
sint32 temperature = 7; // Efficient encoding for negatives
sint64 altitude = 8; // Efficient encoding for negatives
fixed32 hash = 9; // Always 4 bytes
fixed64 large_hash = 10; // Always 8 bytes
// String/bytes
string name = 11; // UTF-8 encoded string
bytes thumbnail = 12; // Arbitrary byte sequence
// Boolean
bool is_active = 13;
}
2.3 Message Types and Nesting
message Product {
string id = 1;
string name = 2;
string description = 3;
Money price = 4;
Category category = 5;
repeated string tags = 6; // Repeated field (array)
map<string, string> metadata = 7; // Map type
google.protobuf.Timestamp created_at = 8;
ProductStatus status = 9;
// Nested message
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; // Integer part
int32 nanos = 3; // Fractional part in nanos
}
2.4 Enum Types
enum ProductStatus {
PRODUCT_STATUS_UNSPECIFIED = 0; // Always 0 for default
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 Types
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 Field Numbers and Versioning
In Protocol Buffers, field numbers are the key element that identifies fields in the wire format.
Field number rules:
- 1-15: Encoded in 1 byte (assign to frequently used fields)
- 16-2047: Encoded in 2 bytes
- 19000-19999: Reserved range (cannot use)
Backward compatibility rules:
message Product {
string id = 1;
string name = 2;
// Deleted fields: reserve both number and name
reserved 3, 6 to 8;
reserved "old_price", "legacy_tag";
// Adding new fields is always safe
string description = 4;
Money price = 5;
// Add from field 9 onwards
string sku = 9;
}
Versioning strategies:
- Adding fields: Always safe (clients unaware of new fields ignore them)
- Removing fields: Reserve the number with reserved, then remove
- Changing field types: Never do this (add a new field instead)
- Package version separation:
ecommerce.v1,ecommerce.v2
3. Four Communication Patterns
gRPC supports four communication patterns built on HTTP/2.
3.1 Unary RPC
The most basic pattern where the client sends one request and the server returns one response.
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 server implementation:
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 client:
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 call
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
The client sends one request and the server returns a stream of multiple messages.
service OrderService {
// Real-time order status tracking
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 server implementation:
func (s *orderServer) TrackOrder(req *pb.TrackOrderRequest, stream pb.OrderService_TrackOrderServer) error {
orderID := req.OrderId
// Subscribe to event channel
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
The client sends a stream of multiple messages and the server returns one response.
service UploadService {
// File upload
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 server implementation:
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 {
// All chunks received
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
Client and server simultaneously exchange messages. Ideal for real-time chat, games, and stock tickers.
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 server implementation:
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
}
// Broadcast to all clients in the same room
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 Multiplexing
gRPC is built on HTTP/2, leveraging its key features.
4.1 Core HTTP/2 Features
Multiplexing: Handles multiple requests/responses simultaneously over a single TCP connection. Solves the HTTP/1.1 Head-of-Line Blocking problem.
Header Compression (HPACK): Eliminates duplicate headers and compresses with Huffman encoding. Significantly reduces header overhead on repeated requests.
Server Push: Server can send data without client request. Forms the basis of gRPC streaming.
Binary Framing: Unlike HTTP/1.1's text protocol, HTTP/2 communicates with binary frames.
HTTP/2 Connection
├── Stream 1: GetUser RPC
├── Stream 3: ListProducts RPC (concurrent)
├── Stream 5: TrackOrder RPC (server streaming)
└── Stream 7: Chat RPC (bidirectional streaming)
4.2 Connection Management
// Server configuration
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. Interceptors (Middleware)
Interceptors are gRPC middleware that execute common logic before and after requests.
5.1 Unary Interceptor
// Logging interceptor
func loggingUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// Log request
log.Printf("gRPC call: %s", info.FullMethod)
// Execute handler
resp, err := handler(ctx, req)
// Log response
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
}
// Authentication interceptor
func authUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Skip auth for public methods
if isPublicMethod(info.FullMethod) {
return handler(ctx, req)
}
// Extract token from metadata
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")
}
// Validate token
claims, err := validateToken(tokens[0])
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
// Add user info to context
ctx = context.WithValue(ctx, userClaimsKey, claims)
return handler(ctx, req)
}
5.2 Stream Interceptor
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 stream for message counting
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 Interceptor Chain
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
recoveryUnaryInterceptor, // Panic recovery (outermost)
loggingUnaryInterceptor, // Logging
metricsUnaryInterceptor, // Metrics collection
authUnaryInterceptor, // Authentication
rateLimitUnaryInterceptor, // Rate limiting
validationUnaryInterceptor, // Input validation (innermost)
),
grpc.ChainStreamInterceptor(
recoveryStreamInterceptor,
loggingStreamInterceptor,
authStreamInterceptor,
),
)
6. Error Handling and Status Codes
6.1 gRPC Status Codes
| Code | Name | Description | HTTP Mapping |
|---|---|---|---|
| 0 | OK | Success | 200 |
| 1 | CANCELLED | Request cancelled | 499 |
| 2 | UNKNOWN | Unknown error | 500 |
| 3 | INVALID_ARGUMENT | Invalid argument | 400 |
| 4 | DEADLINE_EXCEEDED | Timeout | 504 |
| 5 | NOT_FOUND | Resource not found | 404 |
| 6 | ALREADY_EXISTS | Already exists | 409 |
| 7 | PERMISSION_DENIED | Insufficient permissions | 403 |
| 8 | RESOURCE_EXHAUSTED | Resource exhausted | 429 |
| 13 | INTERNAL | Internal server error | 500 |
| 14 | UNAVAILABLE | Service unavailable | 503 |
| 16 | UNAUTHENTICATED | Authentication failure | 401 |
6.2 Rich Error Responses
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) {
// Input validation
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()
}
// Business logic
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 and Timeout
7.1 Deadline Propagation
// Client: set 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")
}
}
// Server: check deadline
func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {
// Check 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")
}
}
// Propagate deadline with remaining time for downstream calls
product, err := s.repo.FindByID(ctx, req.ProductId)
if err != nil {
return nil, err
}
// Allocate 80% of remaining time for price service
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 {
// Fall back to cached price on failure
price = s.priceCache.Get(req.ProductId)
}
product.Price = price
return product, nil
}
8. Load Balancing
8.1 Client-Side Load Balancing
import (
"google.golang.org/grpc"
"google.golang.org/grpc/resolver"
_ "google.golang.org/grpc/balancer/roundrobin"
)
// Register custom resolver
resolver.Register(&myResolver{})
conn, err := grpc.Dial(
"my-service:///product-service",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
8.2 Proxy-Based Load Balancing (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: Using gRPC in Browsers
9.1 Why gRPC-Web Is Needed
Browsers cannot directly control HTTP/2 framing, so they cannot natively use gRPC. gRPC-Web is a proxy layer that bridges this gap.
9.2 Using Envoy as a Proxy
# 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 Client (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 call
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. Reflection and Health Checking
10.1 Server Reflection
import "google.golang.org/grpc/reflection"
func main() {
server := grpc.NewServer()
pb.RegisterProductServiceServer(server, &productServer{})
// Enable reflection (for development)
reflection.Register(server)
lis, _ := net.Listen("tcp", ":50051")
server.Serve(lis)
}
10.2 Health Checking
import "google.golang.org/grpc/health"
import healthpb "google.golang.org/grpc/health/grpc_health_v1"
func main() {
server := grpc.NewServer()
// Register health check service
healthServer := health.NewServer()
healthpb.RegisterHealthServer(server, healthServer)
// Set service status
healthServer.SetServingStatus("product.v1.ProductService", healthpb.HealthCheckResponse_SERVING)
// Periodic health check
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 Health Probes
# 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. Service Mesh Integration
11.1 Istio and 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 and gRPC
Linkerd natively supports HTTP/2, so it automatically recognizes gRPC traffic.
# 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. Testing
12.1 Testing with grpcurl
# List services (requires reflection)
grpcurl -plaintext localhost:50051 list
# List service methods
grpcurl -plaintext localhost:50051 list ecommerce.v1.ProductService
# Describe a method
grpcurl -plaintext localhost:50051 describe ecommerce.v1.ProductService.GetProduct
# Unary call
grpcurl -plaintext \
-d '{"product_id": "prod-123"}' \
localhost:50051 ecommerce.v1.ProductService/GetProduct
# Call with headers
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 Integration Testing in Go
func TestGetProduct(t *testing.T) {
// In-memory gRPC server
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()
// Client connection
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)
// Execute test
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 Interactive Client
# evans REPL mode
evans --host localhost --port 50051 -r repl
# Select package
> package ecommerce.v1
# Select service
> service ProductService
# Call RPC
> call GetProduct
product_id (TYPE_STRING) => prod-123
# Result displayed as JSON
13. Performance Benchmarks
13.1 Protobuf vs JSON Performance Comparison
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)
}
}
Typical benchmark results:
| Metric | Protobuf | JSON | Ratio |
|---|---|---|---|
| Serialization Speed | 150ns | 1500ns | 10x faster |
| Deserialization Speed | 200ns | 2000ns | 10x faster |
| Data Size | 68 bytes | 220 bytes | 3.2x smaller |
| Memory Allocations | 1 alloc | 8 allocs | 8x fewer |
13.2 gRPC vs REST Throughput
| Scenario | gRPC (req/s) | REST (req/s) | gRPC Advantage |
|---|---|---|---|
| Small payload (100B) | 45,000 | 15,000 | 3x |
| Medium payload (1KB) | 35,000 | 10,000 | 3.5x |
| Large payload (100KB) | 8,000 | 2,500 | 3.2x |
| Streaming (100K messages) | 150,000/s | N/A | - |
14. Real-World Microservices Implementation
14.1 Project Structure
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 Code Generation with 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
# Generate code
buf generate proto
# Lint
buf lint proto
# Backward compatibility check
buf breaking proto --against '.git#branch=main'
15. Quiz
Test your understanding of gRPC and Protocol Buffers with the following quiz.
Q1: What is the biggest advantage of gRPC using HTTP/2?
Answer: Multiplexing
HTTP/2 multiplexing allows multiple gRPC calls to be processed simultaneously over a single TCP connection. In HTTP/1.1, only one request can be processed per connection causing Head-of-Line Blocking, but in HTTP/2, multiple streams operate independently. Additional benefits include header compression (HPACK), binary framing, and server push.
Q2: Why should you assign frequently used fields to field numbers 1-15 in Protocol Buffers?
Answer: Because the encoding size is smaller
Field numbers 1-15 are encoded in 1 byte, while 16-2047 are encoded in 2 bytes. Assigning lower numbers to frequently used fields reduces overall message size. This is especially beneficial for repeated fields that appear multiple times.
Q3: What is the difference between Bidirectional Streaming and Server Streaming?
Answer: Whether the client can also send messages as a stream
In Server Streaming, the client sends one request and the server responds with a stream. In Bidirectional Streaming, both client and server can simultaneously exchange messages through independent streams. Since the two streams are independent, the server does not need to wait for all client messages before responding.
Q4: Why are deadlines important in gRPC?
Answer: They propagate request timeouts in distributed systems to prevent resource waste
A deadline is the maximum wait time set by the client, and gRPC automatically propagates it to downstream service calls. When Service A calls Service B, and B calls C, if A's deadline has already passed, the work by B and C is meaningless. Deadline propagation prevents such unnecessary work.
Q5: Why is gRPC-Web necessary?
Answer: Because browsers cannot directly control HTTP/2 framing
Browser Fetch API and XMLHttpRequest do not support frame-level control of HTTP/2. gRPC uses HTTP/2 trailers to deliver status codes, which browsers cannot read. gRPC-Web translates the gRPC protocol into a browser-compatible format through a proxy like Envoy.
16. References
- gRPC Official Documentation - Official gRPC guides and tutorials
- Protocol Buffers Language Guide - Protobuf proto3 syntax guide
- gRPC Go Tutorial - Go language gRPC tutorial
- Buf - Protobuf Tools - Modern Protobuf management tool
- gRPC-Web GitHub - Official gRPC-Web repository
- Connect-RPC - Browser-compatible gRPC framework
- Envoy gRPC Configuration - Envoy's gRPC support
- grpcurl GitHub - gRPC command-line tool
- evans GitHub - gRPC interactive client
- Istio gRPC Traffic Management - Istio and gRPC integration
- gRPC Health Checking Protocol - Health check protocol specification
- Google API Design Guide - Google's API design guide
- gRPC Performance Best Practices - gRPC performance optimization guide