Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

목차

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";

// 상품 서비스 정의

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 클라이언트:**

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 풍부한 에러 응답

"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 클라이언트 사이드 로드 밸런싱

"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)

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 서버 리플렉션

func main() {

server := grpc.NewServer()

pb.RegisterProductServiceServer(server, &productServer{})

// 리플렉션 활성화 (개발 환경용)

reflection.Register(server)

lis, _ := net.Listen("tcp", ":50051")

server.Serve(lis)

}

10.2 헬스 체킹

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에 대한 이해도를 점검해보세요.

**정답: 멀티플렉싱 (Multiplexing)**

HTTP/2의 멀티플렉싱을 통해 하나의 TCP 연결에서 여러 gRPC 호출을 동시에 처리할 수 있습니다. HTTP/1.1에서는 연결당 하나의 요청만 처리할 수 있어 Head-of-Line Blocking이 발생하지만, HTTP/2에서는 여러 스트림이 독립적으로 동작합니다. 이 외에도 헤더 압축(HPACK), 바이너리 프레이밍, 서버 푸시 등의 이점이 있습니다.

**정답: 인코딩 크기가 작기 때문**

필드 번호 1~15는 1바이트로 인코딩되고, 16~2047은 2바이트로 인코딩됩니다. 자주 사용하는 필드에 낮은 번호를 부여하면 전체 메시지 크기를 줄일 수 있습니다. 또한 repeated 필드처럼 여러 번 나타나는 필드는 특히 낮은 번호의 혜택이 큽니다.

**정답: 클라이언트도 스트림으로 메시지를 보낼 수 있는지 여부**

Server Streaming에서는 클라이언트가 하나의 요청을 보내면 서버가 스트림으로 응답합니다. Bidirectional Streaming에서는 클라이언트와 서버가 독립적인 스트림으로 동시에 메시지를 주고받을 수 있습니다. 두 스트림은 독립적이므로 서버가 클라이언트의 모든 메시지를 받을 때까지 기다릴 필요 없이 응답할 수 있습니다.

**정답: 분산 시스템에서 요청 타임아웃을 전파하여 리소스 낭비를 방지**

deadline은 클라이언트가 설정한 최대 대기 시간으로, gRPC는 이를 자동으로 하위 서비스 호출에 전파합니다. 서비스 A가 서비스 B를 호출하고, B가 C를 호출할 때 A의 deadline이 이미 지났다면 B와 C의 작업은 의미가 없습니다. deadline 전파를 통해 이런 불필요한 작업을 방지합니다.

**정답: 브라우저가 HTTP/2 프레이밍을 직접 제어할 수 없기 때문**

브라우저의 Fetch API와 XMLHttpRequest는 HTTP/2 프레임 수준의 제어를 지원하지 않습니다. gRPC는 HTTP/2의 트레일러(trailers)를 사용하여 상태 코드를 전달하는데, 브라우저에서는 이를 읽을 수 없습니다. gRPC-Web은 프록시(Envoy 등)를 통해 gRPC 프로토콜을 브라우저가 이해할 수 있는 형태로 변환합니다.

16. 참고 자료

1. [gRPC 공식 문서](https://grpc.io/docs/) - gRPC 공식 가이드 및 튜토리얼

2. [Protocol Buffers Language Guide](https://protobuf.dev/programming-guides/proto3/) - Protobuf proto3 문법 가이드

3. [gRPC Go Tutorial](https://grpc.io/docs/languages/go/basics/) - Go 언어 gRPC 튜토리얼

4. [Buf - Protobuf 도구](https://buf.build/) - 현대적인 Protobuf 관리 도구

5. [gRPC-Web GitHub](https://github.com/grpc/grpc-web) - gRPC-Web 공식 저장소

6. [Connect-RPC](https://connectrpc.com/) - 브라우저 호환 gRPC 프레임워크

7. [Envoy gRPC 설정](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_protocols/grpc) - Envoy의 gRPC 지원

8. [grpcurl GitHub](https://github.com/fullstorydev/grpcurl) - gRPC 커맨드라인 도구

9. [evans GitHub](https://github.com/ktr0731/evans) - gRPC 대화형 클라이언트

10. [Istio gRPC Traffic Management](https://istio.io/latest/docs/tasks/traffic-management/) - Istio와 gRPC 통합

11. [gRPC Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) - 헬스 체크 프로토콜 명세

12. [Google API Design Guide](https://cloud.google.com/apis/design) - Google의 API 설계 가이드

13. [gRPC Performance Best Practices](https://grpc.io/docs/guides/performance/) - gRPC 성능 최적화 가이드

현재 단락 (1/860)

마이크로서비스 아키텍처에서 서비스 간 통신은 시스템의 성능과 안정성을 좌우하는 핵심 요소입니다. REST API는 단순함과 범용성으로 오랫동안 표준이었지만, 서비스 수가 증가하면서...

작성 글자: 0원문 글자: 22,177작성 단락: 0/860