← 블로그 홈

gRPC 완벽 가이드: 개념부터 실전 활용까지

gRPC는 Google이 개발한 고성능 RPC(Remote Procedure Call) 프레임워크로, 마이크로서비스 아키텍처에서 서비스 간 통신의 새로운 표준이 되고 있습니다. 이 글에서는 gRPC의 핵심 개념부터 실전 활용 방법까지 종합적으로 알아보겠습니다.

gRPC는 Google이 개발한 고성능 RPC(Remote Procedure Call) 프레임워크로, 마이크로서비스 아키텍처에서 서비스 간 통신의 새로운 표준이 되고 있습니다. 이 글에서는 gRPC의 핵심 개념부터 실전 활용 방법까지 종합적으로 알아보겠습니다.

🔍 gRPC란 무엇인가?

gRPC의 정의와 특징

gRPC (gRPC Remote Procedure Calls)는 구글에서 개발한 언어 중립적이고 플랫폼 중립적인 오픈소스 RPC 시스템입니다.

핵심 특징

  • HTTP/2 기반: 멀티플렉싱, 서버 푸시, 헤더 압축 지원
  • Protocol Buffers: 효율적인 직렬화 메커니즘
  • 언어 독립적: 다양한 프로그래밍 언어 지원
  • 양방향 스트리밍: 실시간 데이터 교환 가능
  • 타입 안전성: 강타입 스키마 정의

gRPC vs REST API 비교

특성 gRPC REST API
프로토콜 HTTP/2 HTTP/1.1
데이터 형식 Protocol Buffers (이진) JSON (텍스트)
성능 높음 (압축, 이진 형식) 보통 (텍스트 기반)
스트리밍 양방향 스트리밍 지원 제한적
브라우저 지원 제한적 (grpc-web 필요) 완전 지원
캐싱 복잡 간단 (HTTP 캐시)
가독성 낮음 (이진 형식) 높음 (JSON)

📋 Protocol Buffers 이해하기

.proto 파일 기본 구조

// user.proto
syntax = "proto3";

package user.v1;

option go_package = "github.com/example/user/v1";

// 사용자 정보 메시지
message User {
  int64 id = 1;
  string username = 2;
  string email = 3;
  string full_name = 4;
  bool is_active = 5;
  repeated string roles = 6;
  google.protobuf.Timestamp created_at = 7;
}

// 사용자 생성 요청
message CreateUserRequest {
  string username = 1;
  string email = 2;
  string password = 3;
  string full_name = 4;
}

// 사용자 생성 응답
message CreateUserResponse {
  User user = 1;
  string message = 2;
}

// 사용자 목록 요청
message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
  string filter = 3;
}

// 사용자 목록 응답
message ListUsersResponse {
  repeated User users = 1;
  int32 total_count = 2;
  int32 page = 3;
  int32 page_size = 4;
}

데이터 타입과 규칙

// 기본 데이터 타입
message DataTypes {
  // 숫자형
  int32 age = 1;           // 32비트 정수
  int64 timestamp = 2;     // 64비트 정수
  float price = 3;         // 32비트 부동소수점
  double latitude = 4;     // 64비트 부동소수점
  
  // 문자열과 바이너리
  string name = 5;         // UTF-8 문자열
  bytes data = 6;          // 바이너리 데이터
  
  // 불린
  bool is_verified = 7;    // true/false
  
  // 열거형
  enum Status {
    UNKNOWN = 0;
    ACTIVE = 1;
    INACTIVE = 2;
    PENDING = 3;
  }
  Status status = 8;
  
  // 반복 필드
  repeated string tags = 9;
  
  // 맵
  map<string, string> metadata = 10;
  
  // 중첩 메시지
  message Address {
    string street = 1;
    string city = 2;
    string country = 3;
  }
  Address address = 11;
  
  // 선택적 필드 (proto3에서는 기본적으로 optional)
  optional string nickname = 12;
}

🏗️ gRPC 서비스 정의

기본 RPC 패턴

// user_service.proto
syntax = "proto3";

import "google/protobuf/empty.proto";
import "user.proto";

service UserService {
  // 1. Unary RPC (일반적인 요청-응답)
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  rpc GetUser(GetUserRequest) returns (User);
  rpc UpdateUser(UpdateUserRequest) returns (User);
  rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);
  
  // 2. Server Streaming (서버에서 클라이언트로 스트림)
  rpc ListUsers(ListUsersRequest) returns (stream User);
  rpc WatchUserChanges(WatchUserRequest) returns (stream UserEvent);
  
  // 3. Client Streaming (클라이언트에서 서버로 스트림)
  rpc BulkCreateUsers(stream CreateUserRequest) returns (BulkCreateResponse);
  
  // 4. Bidirectional Streaming (양방향 스트림)
  rpc UserChat(stream ChatMessage) returns (stream ChatMessage);
  rpc SyncUsers(stream UserSyncRequest) returns (stream UserSyncResponse);
}

// 추가 메시지 정의
message GetUserRequest {
  int64 user_id = 1;
}

message UpdateUserRequest {
  int64 user_id = 1;
  User user = 2;
  google.protobuf.FieldMask update_mask = 3;
}

message DeleteUserRequest {
  int64 user_id = 1;
}

message UserEvent {
  enum EventType {
    CREATED = 0;
    UPDATED = 1;
    DELETED = 2;
  }
  EventType type = 1;
  User user = 2;
  google.protobuf.Timestamp timestamp = 3;
}

💻 Go에서 gRPC 서버 구현

서버 설정 및 구현

// server/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/protobuf/types/known/emptypb"
    "google.golang.org/protobuf/types/known/timestamppb"
    
    pb "github.com/example/user/v1"
)

// UserServer는 UserService를 구현합니다
type UserServer struct {
    pb.UnimplementedUserServiceServer
    users map[int64]*pb.User
    nextID int64
}

// NewUserServer는 새로운 UserServer 인스턴스를 생성합니다
func NewUserServer() *UserServer {
    return &UserServer{
        users:  make(map[int64]*pb.User),
        nextID: 1,
    }
}

// CreateUser는 새로운 사용자를 생성합니다
func (s *UserServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
    // 유효성 검사
    if req.Username == "" {
        return nil, status.Errorf(codes.InvalidArgument, "username is required")
    }
    if req.Email == "" {
        return nil, status.Errorf(codes.InvalidArgument, "email is required")
    }

    // 중복 검사
    for _, user := range s.users {
        if user.Username == req.Username {
            return nil, status.Errorf(codes.AlreadyExists, "username already exists")
        }
        if user.Email == req.Email {
            return nil, status.Errorf(codes.AlreadyExists, "email already exists")
        }
    }

    // 사용자 생성
    user := &pb.User{
        Id:       s.nextID,
        Username: req.Username,
        Email:    req.Email,
        FullName: req.FullName,
        IsActive: true,
        CreatedAt: timestamppb.Now(),
    }

    s.users[s.nextID] = user
    s.nextID++

    return &pb.CreateUserResponse{
        User:    user,
        Message: "User created successfully",
    }, nil
}

// GetUser는 사용자 정보를 조회합니다
func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, exists := s.users[req.UserId]
    if !exists {
        return nil, status.Errorf(codes.NotFound, "user not found")
    }
    return user, nil
}

// ListUsers는 사용자 목록을 스트리밍으로 반환합니다
func (s *UserServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    for _, user := range s.users {
        if err := stream.Send(user); err != nil {
            return err
        }
        // 스트리밍 효과를 위한 지연
        time.Sleep(100 * time.Millisecond)
    }
    return nil
}

// UpdateUser는 사용자 정보를 수정합니다
func (s *UserServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.User, error) {
    user, exists := s.users[req.UserId]
    if !exists {
        return nil, status.Errorf(codes.NotFound, "user not found")
    }

    // 필드 업데이트
    if req.User.Username != "" {
        user.Username = req.User.Username
    }
    if req.User.Email != "" {
        user.Email = req.User.Email
    }
    if req.User.FullName != "" {
        user.FullName = req.User.FullName
    }

    return user, nil
}

// DeleteUser는 사용자를 삭제합니다
func (s *UserServer) DeleteUser(ctx context.Context, req *pb.DeleteUserRequest) (*emptypb.Empty, error) {
    _, exists := s.users[req.UserId]
    if !exists {
        return nil, status.Errorf(codes.NotFound, "user not found")
    }
    
    delete(s.users, req.UserId)
    return &emptypb.Empty{}, nil
}

func main() {
    // gRPC 서버 생성
    s := grpc.NewServer()
    
    // 서비스 등록
    pb.RegisterUserServiceServer(s, NewUserServer())
    
    // 리스너 생성
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    
    fmt.Println("gRPC server listening on :50051")
    
    // 서버 시작
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

미들웨어와 인터셉터

// middleware.go
package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// LoggingInterceptor는 모든 요청을 로깅합니다
func LoggingInterceptor(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)
    if err != nil {
        log.Printf("gRPC call: %s, duration: %v, error: %v", info.FullMethod, duration, err)
    } else {
        log.Printf("gRPC call: %s, duration: %v, success", info.FullMethod, duration)
    }
    
    return resp, err
}

// AuthInterceptor는 인증을 처리합니다
func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // 인증이 필요하지 않은 메서드들
    publicMethods := map[string]bool{
        "/user.v1.UserService/CreateUser": true,
    }
    
    if !publicMethods[info.FullMethod] {
        // 인증 토큰 검증 로직
        if err := validateToken(ctx); err != nil {
            return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
        }
    }
    
    return handler(ctx, req)
}

func validateToken(ctx context.Context) error {
    // 실제 토큰 검증 로직 구현
    return nil
}

// 서버에 미들웨어 적용
func createServerWithMiddleware() *grpc.Server {
    return grpc.NewServer(
        grpc.UnaryInterceptor(grpc.ChainUnaryInterceptor(
            LoggingInterceptor,
            AuthInterceptor,
        )),
    )
}

🐍 Python에서 gRPC 클라이언트 구현

동기 클라이언트

# client.py
import grpc
from concurrent import futures
import logging
from typing import Iterator

import user_pb2
import user_pb2_grpc

class UserClient:
    def __init__(self, address: str = "localhost:50051"):
        """gRPC 클라이언트 초기화"""
        self.channel = grpc.insecure_channel(address)
        self.stub = user_pb2_grpc.UserServiceStub(self.channel)
    
    def create_user(self, username: str, email: str, password: str, full_name: str = "") -> user_pb2.User:
        """사용자 생성"""
        try:
            request = user_pb2.CreateUserRequest(
                username=username,
                email=email,
                password=password,
                full_name=full_name
            )
            response = self.stub.CreateUser(request)
            print(f"User created: {response.user.username}")
            return response.user
        except grpc.RpcError as e:
            print(f"gRPC error: {e.code()} - {e.details()}")
            raise
    
    def get_user(self, user_id: int) -> user_pb2.User:
        """사용자 조회"""
        try:
            request = user_pb2.GetUserRequest(user_id=user_id)
            user = self.stub.GetUser(request)
            print(f"User found: {user.username} ({user.email})")
            return user
        except grpc.RpcError as e:
            if e.code() == grpc.StatusCode.NOT_FOUND:
                print(f"User with ID {user_id} not found")
            else:
                print(f"gRPC error: {e.code()} - {e.details()}")
            raise
    
    def list_users(self) -> list[user_pb2.User]:
        """사용자 목록 조회 (스트리밍)"""
        try:
            request = user_pb2.ListUsersRequest()
            users = []
            
            print("Receiving user stream...")
            for user in self.stub.ListUsers(request):
                print(f"Received user: {user.username}")
                users.append(user)
            
            return users
        except grpc.RpcError as e:
            print(f"gRPC error: {e.code()} - {e.details()}")
            raise
    
    def update_user(self, user_id: int, **kwargs) -> user_pb2.User:
        """사용자 정보 수정"""
        try:
            user_update = user_pb2.User()
            for key, value in kwargs.items():
                if hasattr(user_update, key):
                    setattr(user_update, key, value)
            
            request = user_pb2.UpdateUserRequest(
                user_id=user_id,
                user=user_update
            )
            
            updated_user = self.stub.UpdateUser(request)
            print(f"User updated: {updated_user.username}")
            return updated_user
        except grpc.RpcError as e:
            print(f"gRPC error: {e.code()} - {e.details()}")
            raise
    
    def delete_user(self, user_id: int):
        """사용자 삭제"""
        try:
            request = user_pb2.DeleteUserRequest(user_id=user_id)
            self.stub.DeleteUser(request)
            print(f"User {user_id} deleted successfully")
        except grpc.RpcError as e:
            print(f"gRPC error: {e.code()} - {e.details()}")
            raise
    
    def close(self):
        """연결 종료"""
        self.channel.close()

# 사용 예시
def main():
    client = UserClient()
    
    try:
        # 사용자 생성
        user = client.create_user(
            username="john_doe",
            email="john@example.com",
            password="secure_password",
            full_name="John Doe"
        )
        
        # 사용자 조회
        retrieved_user = client.get_user(user.id)
        
        # 사용자 목록 조회
        users = client.list_users()
        print(f"Total users: {len(users)}")
        
        # 사용자 정보 수정
        client.update_user(user.id, full_name="John Smith")
        
        # 사용자 삭제
        client.delete_user(user.id)
        
    finally:
        client.close()

if __name__ == "__main__":
    main()

비동기 클라이언트

# async_client.py
import asyncio
import grpc
import user_pb2
import user_pb2_grpc

class AsyncUserClient:
    def __init__(self, address: str = "localhost:50051"):
        self.address = address
        self.channel = None
        self.stub = None
    
    async def __aenter__(self):
        """비동기 컨텍스트 매니저 진입"""
        self.channel = grpc.aio.insecure_channel(self.address)
        self.stub = user_pb2_grpc.UserServiceStub(self.channel)
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """비동기 컨텍스트 매니저 종료"""
        await self.channel.close()
    
    async def create_user(self, username: str, email: str, password: str, full_name: str = ""):
        """비동기 사용자 생성"""
        request = user_pb2.CreateUserRequest(
            username=username,
            email=email,
            password=password,
            full_name=full_name
        )
        
        response = await self.stub.CreateUser(request)
        return response.user
    
    async def get_user(self, user_id: int):
        """비동기 사용자 조회"""
        request = user_pb2.GetUserRequest(user_id=user_id)
        return await self.stub.GetUser(request)
    
    async def list_users_stream(self):
        """비동기 사용자 스트림 조회"""
        request = user_pb2.ListUsersRequest()
        users = []
        
        async for user in self.stub.ListUsers(request):
            users.append(user)
            print(f"Received user: {user.username}")
        
        return users

# 비동기 사용 예시
async def async_main():
    async with AsyncUserClient() as client:
        # 동시에 여러 사용자 생성
        tasks = [
            client.create_user(f"user{i}", f"user{i}@example.com", "password")
            for i in range(5)
        ]
        
        users = await asyncio.gather(*tasks)
        print(f"Created {len(users)} users concurrently")
        
        # 스트림으로 사용자 목록 조회
        all_users = await client.list_users_stream()
        print(f"Total users in stream: {len(all_users)}")

if __name__ == "__main__":
    asyncio.run(async_main())

🌐 실전 활용 사례

1. 마이크로서비스 간 통신

// order_service.proto
syntax = "proto3";

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (Order);
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ProcessPayment(PaymentRequest) returns (PaymentResponse);
}

service PaymentService {
  rpc ProcessPayment(PaymentRequest) returns (PaymentResponse);
  rpc RefundPayment(RefundRequest) returns (RefundResponse);
}

service InventoryService {
  rpc CheckStock(StockRequest) returns (StockResponse);
  rpc ReserveItems(ReserveRequest) returns (ReserveResponse);
}

2. 실시간 데이터 스트리밍

// monitoring_service.proto
syntax = "proto3";

service MonitoringService {
  // 실시간 메트릭 스트리밍
  rpc StreamMetrics(MetricsRequest) returns (stream MetricData);
  
  // 로그 스트리밍
  rpc StreamLogs(LogRequest) returns (stream LogEntry);
  
  // 양방향 채팅
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message MetricData {
  string service_name = 1;
  map<string, double> metrics = 2;
  google.protobuf.Timestamp timestamp = 3;
}

3. 배치 처리 최적화

// batch_client.go
func (c *Client) BulkProcessUsers(users []*pb.User) error {
    stream, err := c.stub.BulkCreateUsers(context.Background())
    if err != nil {
        return err
    }

    // 배치로 사용자 전송
    for _, user := range users {
        req := &pb.CreateUserRequest{
            Username: user.Username,
            Email:    user.Email,
            // ... other fields
        }
        
        if err := stream.Send(req); err != nil {
            return err
        }
    }

    // 스트림 종료 및 응답 받기
    response, err := stream.CloseAndRecv()
    if err != nil {
        return err
    }

    log.Printf("Bulk created %d users", response.CreatedCount)
    return nil
}

🚀 성능 최적화 및 모니터링

연결 풀링과 로드 밸런싱

// client_pool.go
package main

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/balancer/roundrobin"
    "google.golang.org/grpc/keepalive"
    "time"
)

func createOptimizedClient(addresses []string) (*grpc.ClientConn, error) {
    // 연결 옵션 설정
    opts := []grpc.DialOption{
        grpc.WithInsecure(),
        grpc.WithDefaultServiceConfig(`{
            "loadBalancingPolicy": "round_robin",
            "healthCheckConfig": {
                "serviceName": "user.v1.UserService"
            }
        }`),
        grpc.WithKeepaliveParams(keepalive.ClientParameters{
            Time:                10 * time.Second,
            Timeout:             3 * time.Second,
            PermitWithoutStream: true,
        }),
    }

    // 여러 서버 주소를 위한 resolver 설정
    target := fmt.Sprintf("dns:///%s", strings.Join(addresses, ","))
    
    return grpc.Dial(target, opts...)
}

메트릭 수집

// metrics.go
package main

import (
    "context"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "google.golang.org/grpc"
    "google.golang.org/grpc/status"
)

var (
    grpcRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "grpc_requests_total",
            Help: "Total number of gRPC requests",
        },
        []string{"method", "status"},
    )

    grpcRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "grpc_request_duration_seconds",
            Help: "Duration of gRPC requests",
        },
        []string{"method"},
    )
)

func MetricsInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    start := time.Now()
    
    resp, err := handler(ctx, req)
    
    duration := time.Since(start)
    statusCode := "OK"
    if err != nil {
        statusCode = status.Code(err).String()
    }
    
    grpcRequestsTotal.WithLabelValues(info.FullMethod, statusCode).Inc()
    grpcRequestDuration.WithLabelValues(info.FullMethod).Observe(duration.Seconds())
    
    return resp, err
}

🔒 보안 및 인증

TLS 설정

// tls_server.go
func createSecureServer() (*grpc.Server, error) {
    // TLS 인증서 로드
    creds, err := credentials.LoadTLSCredentials("server.crt", "server.key")
    if err != nil {
        return nil, err
    }

    // 보안 서버 생성
    s := grpc.NewServer(
        grpc.Creds(creds),
        grpc.UnaryInterceptor(AuthInterceptor),
    )

    return s, nil
}

// JWT 토큰 기반 인증
func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
    }

    token := md["authorization"]
    if len(token) == 0 {
        return nil, status.Errorf(codes.Unauthenticated, "missing token")
    }

    // JWT 토큰 검증
    if err := validateJWT(token[0]); err != nil {
        return nil, status.Errorf(codes.Unauthenticated, "invalid token")
    }

    return handler(ctx, req)
}

🎉 결론

gRPC는 현대적인 마이크로서비스 아키텍처에서 필수적인 기술로 자리잡고 있습니다.

✅ gRPC의 주요 장점

  1. 높은 성능: HTTP/2와 Protocol Buffers로 최적화된 통신
  2. 타입 안전성: 강타입 스키마로 런타임 오류 방지
  3. 언어 독립성: 다양한 언어 간 seamless 통신
  4. 스트리밍 지원: 실시간 데이터 처리 가능
  5. 자동 코드 생성: 개발 생산성 향상

🛠️ 실무 적용 가이드

  • 마이크로서비스: 서비스 간 내부 통신에 최적
  • 실시간 시스템: 스트리밍 기능으로 실시간 데이터 처리
  • 모바일 앱: 효율적인 배터리 사용과 빠른 통신
  • IoT 시스템: 경량화된 프로토콜로 디바이스 통신

gRPC를 도입할 때는 팀의 기술 수준, 기존 인프라, 그리고 요구사항을 종합적으로 고려하여 점진적으로 적용하는 것이 좋습니다. 특히 REST API와의 하이브리드 구조로 시작하여 경험을 쌓아가는 것을 추천합니다.

이 글을 공유해보세요!