Skip to content
Published on

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

Authors

목차

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 매핑
0OK성공200
1CANCELLED요청 취소499
2UNKNOWN알 수 없는 에러500
3INVALID_ARGUMENT잘못된 인자400
4DEADLINE_EXCEEDED타임아웃504
5NOT_FOUND리소스 없음404
6ALREADY_EXISTS이미 존재409
7PERMISSION_DENIED권한 부족403
8RESOURCE_EXHAUSTED리소스 소진429
13INTERNAL내부 서버 에러500
14UNAVAILABLE서비스 불가503
16UNAUTHENTICATED인증 실패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)
    }
}

일반적인 벤치마크 결과:

메트릭ProtobufJSON비율
직렬화 속도150ns1500ns10x 빠름
역직렬화 속도200ns2000ns10x 빠름
데이터 크기68 bytes220 bytes3.2x 작음
메모리 할당1 alloc8 allocs8x 적음

13.2 gRPC vs REST 처리량

시나리오gRPC (req/s)REST (req/s)gRPC 이점
작은 페이로드 (100B)45,00015,0003x
중간 페이로드 (1KB)35,00010,0003.5x
큰 페이로드 (100KB)8,0002,5003.2x
스트리밍 (10만 메시지)150,000/sN/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. 참고 자료

  1. gRPC 공식 문서 - gRPC 공식 가이드 및 튜토리얼
  2. Protocol Buffers Language Guide - Protobuf proto3 문법 가이드
  3. gRPC Go Tutorial - Go 언어 gRPC 튜토리얼
  4. Buf - Protobuf 도구 - 현대적인 Protobuf 관리 도구
  5. gRPC-Web GitHub - gRPC-Web 공식 저장소
  6. Connect-RPC - 브라우저 호환 gRPC 프레임워크
  7. Envoy gRPC 설정 - Envoy의 gRPC 지원
  8. grpcurl GitHub - gRPC 커맨드라인 도구
  9. evans GitHub - gRPC 대화형 클라이언트
  10. Istio gRPC Traffic Management - Istio와 gRPC 통합
  11. gRPC Health Checking Protocol - 헬스 체크 프로토콜 명세
  12. Google API Design Guide - Google의 API 설계 가이드
  13. gRPC Performance Best Practices - gRPC 성능 최적화 가이드