gRPC 개요

gRPC(gRPC Remote Procedure Calls)는 Google에서 개발한 고성능, 다중 언어 지원 RPC 프레임워크입니다. HTTP/2를 기반으로 하며 Protocol Buffers를 사용하여 효율적인 직렬화와 강력한 타입 안전성을 제공합니다.

핵심 특징

  • 고성능: HTTP/2 기반으로 멀티플렉싱, 스트리밍, 압축 지원
  • 다중 언어 지원: Go, Python, Java, C++, JavaScript 등 주요 언어 지원
  • 타입 안전성: Protocol Buffers를 통한 강력한 스키마 정의
  • 양방향 스트리밍: 실시간 데이터 교환 및 스트리밍 지원

gRPC vs REST API 비교

항목 gRPC REST API
프로토콜 HTTP/2 HTTP/1.1
데이터 형식 Protocol Buffers (이진) JSON (텍스트)
성능 높음 (압축, 멀티플렉싱) 보통
스트리밍 양방향 스트리밍 지원 제한적
브라우저 지원 제한적 (gRPC-Web 필요) 완전 지원
학습 곡선 높음 낮음

gRPC 핵심 개념

Unary RPC vs Streaming RPC

gRPC는 4가지 통신 패턴을 제공합니다:

1. Unary RPC (단일 요청-응답)

  • 정의: 클라이언트가 하나의 요청을 보내고 서버가 하나의 응답을 반환
  • 사용 사례: 전통적인 REST API와 유사한 패턴
  • Proto 정의: rpc MethodName(RequestType) returns (ResponseType);
// Unary RPC 예제
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);

2. Server Streaming RPC

  • 정의: 클라이언트가 하나의 요청을 보내고 서버가 여러 응답을 스트리밍으로 반환
  • 사용 사례: 대용량 데이터 조회, 실시간 모니터링
  • Proto 정의: rpc MethodName(RequestType) returns (stream ResponseType);
// Server Streaming 예제
rpc StreamUsers(StreamUsersRequest) returns (stream User);
rpc GetOrderHistory(OrderRequest) returns (stream Order);

3. Client Streaming RPC

  • 정의: 클라이언트가 여러 요청을 스트리밍으로 보내고 서버가 하나의 응답을 반환
  • 사용 사례: 파일 업로드, 배치 데이터 처리
  • Proto 정의: rpc MethodName(stream RequestType) returns (ResponseType);
// Client Streaming 예제
rpc CreateBatchUsers(stream CreateUserRequest) returns (CreateUserResponse);
rpc UploadFile(stream FileChunk) returns (UploadResponse);

4. Bidirectional Streaming RPC

  • 정의: 클라이언트와 서버가 모두 스트리밍으로 데이터를 주고받음
  • 사용 사례: 실시간 채팅, 양방향 통신
  • Proto 정의: rpc MethodName(stream RequestType) returns (stream ResponseType);
// Bidirectional Streaming 예제
rpc ChatWithUsers(stream CreateUserRequest) returns (stream User);
rpc LiveChat(stream ChatMessage) returns (stream ChatMessage);

통신 패턴 비교표

패턴 요청 응답 사용 사례 성능 특성
Unary 단일 단일 일반적인 API 호출 간단, 낮은 지연시간
Server Streaming 단일 스트림 대용량 데이터 조회 높은 처리량
Client Streaming 스트림 단일 배치 업로드 효율적인 업로드
Bidirectional 스트림 스트림 실시간 통신 낮은 지연시간, 높은 처리량

Interceptor (인터셉터)

인터셉터는 gRPC 서버와 클라이언트에서 요청/응답을 가로채어 공통 로직을 처리하는 미들웨어입니다.

인터셉터 종류

  1. Unary Interceptor: 단일 요청-응답에 대한 인터셉터
  2. Stream Interceptor: 스트리밍 요청에 대한 인터셉터

서버 인터셉터 예제

// 로깅 인터셉터
func loggingUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    start := time.Now()
    log.Printf("[요청 시작] %s", info.FullMethod)
    
    resp, err := handler(ctx, req)
    
    duration := time.Since(start)
    if err != nil {
        log.Printf("[요청 실패] %s, 소요시간: %v, 에러: %v", info.FullMethod, duration, err)
    } else {
        log.Printf("[요청 성공] %s, 소요시간: %v", info.FullMethod, duration)
    }
    
    return resp, err
}

// 인증 인터셉터
func authUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // 특정 메서드는 인증 생략
    if info.FullMethod == "/user.UserService/GetHealth" {
        return handler(ctx, req)
    }
    
    // 메타데이터에서 토큰 확인
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.Unauthenticated, "메타데이터가 없습니다")
    }
    
    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return nil, status.Errorf(codes.Unauthenticated, "인증 토큰이 없습니다")
    }
    
    // 토큰 검증 로직
    if !validateToken(tokens[0]) {
        return nil, status.Errorf(codes.Unauthenticated, "유효하지 않은 토큰")
    }
    
    return handler(ctx, req)
}

// 스트림 인터셉터
func loggingStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
    start := time.Now()
    log.Printf("[스트림 시작] %s", info.FullMethod)
    
    err := handler(srv, ss)
    
    duration := time.Since(start)
    if err != nil {
        log.Printf("[스트림 실패] %s, 소요시간: %v, 에러: %v", info.FullMethod, duration, err)
    } else {
        log.Printf("[스트림 성공] %s, 소요시간: %v", info.FullMethod, duration)
    }
    
    return err
}

클라이언트 인터셉터 예제

// 클라이언트 Unary 인터셉터
func clientLoggingUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    start := time.Now()
    log.Printf("[클라이언트 요청] %s", method)
    
    err := invoker(ctx, method, req, reply, cc, opts...)
    
    duration := time.Since(start)
    if err != nil {
        log.Printf("[클라이언트 실패] %s, 소요시간: %v, 에러: %v", method, duration, err)
    } else {
        log.Printf("[클라이언트 성공] %s, 소요시간: %v", method, duration)
    }
    
    return err
}

// 클라이언트 연결 시 인터셉터 적용
conn, err := grpc.Dial("localhost:50051",
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithUnaryInterceptor(clientLoggingUnaryInterceptor),
)

인터셉터 실행 시점 제어

인터셉터에서는 요청 처리 전(Pre-processing)요청 처리 후(Post-processing)를 명확히 구분할 수 있습니다.

Unary 인터셉터의 실행 흐름

func comprehensiveUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // ========== 요청 처리 전 (Pre-processing) ==========
    log.Printf("🔵 [PRE] 요청 시작: %s", info.FullMethod)
    
    // 1. 요청 검증
    if err := validateRequest(req); err != nil {
        log.Printf("❌ [PRE] 요청 검증 실패: %v", err)
        return nil, status.Errorf(codes.InvalidArgument, "잘못된 요청: %v", err)
    }
    
    // 2. 인증 확인
    userID, err := extractUserID(ctx)
    if err != nil {
        log.Printf("❌ [PRE] 인증 실패: %v", err)
        return nil, status.Errorf(codes.Unauthenticated, "인증 필요")
    }
    
    // 3. 요청 로깅
    start := time.Now()
    log.Printf("✅ [PRE] 사용자 %s가 %s 호출", userID, info.FullMethod)
    
    // 4. 컨텍스트에 정보 추가
    ctx = context.WithValue(ctx, "user_id", userID)
    ctx = context.WithValue(ctx, "start_time", start)
    
    // ========== 실제 핸들러 호출 ==========
    resp, err := handler(ctx, req)
    
    // ========== 요청 처리 후 (Post-processing) ==========
    duration := time.Since(start)
    
    if err != nil {
        // 5. 에러 처리 및 로깅
        log.Printf("❌ [POST] 요청 실패: %s, 사용자: %s, 소요시간: %v, 에러: %v", 
            info.FullMethod, userID, duration, err)
        
        // 6. 에러 메트릭 수집
        recordErrorMetric(info.FullMethod, err)
        
        // 7. 에러 알림 (필요시)
        if isCriticalError(err) {
            sendAlert(info.FullMethod, userID, err)
        }
    } else {
        // 8. 성공 로깅
        log.Printf("✅ [POST] 요청 성공: %s, 사용자: %s, 소요시간: %v", 
            info.FullMethod, userID, duration)
        
        // 9. 성공 메트릭 수집
        recordSuccessMetric(info.FullMethod, duration)
        
        // 10. 응답 후처리 (필요시)
        resp = enrichResponse(resp, userID)
    }
    
    // 11. 감사 로그 기록
    auditLog(userID, info.FullMethod, err == nil, duration)
    
    log.Printf("🔵 [POST] 요청 완료: %s", info.FullMethod)
    
    return resp, err
}

// 헬퍼 함수들
func validateRequest(req interface{}) error {
    // 요청 검증 로직
    return nil
}

func extractUserID(ctx context.Context) (string, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return "", fmt.Errorf("메타데이터 없음")
    }
    
    userIDs := md.Get("user-id")
    if len(userIDs) == 0 {
        return "", fmt.Errorf("사용자 ID 없음")
    }
    
    return userIDs[0], nil
}

func recordErrorMetric(method string, err error) {
    // 에러 메트릭 기록
    log.Printf("📊 에러 메트릭 기록: %s - %v", method, err)
}

func recordSuccessMetric(method string, duration time.Duration) {
    // 성공 메트릭 기록
    log.Printf("📊 성공 메트릭 기록: %s - %v", method, duration)
}

func isCriticalError(err error) bool {
    // 중요한 에러인지 판단
    return status.Code(err) == codes.Internal
}

func sendAlert(method, userID string, err error) {
    // 알림 전송
    log.Printf("🚨 알림 전송: %s에서 %s 사용자 에러 - %v", method, userID, err)
}

func enrichResponse(resp interface{}, userID string) interface{} {
    // 응답 데이터 보강
    return resp
}

func auditLog(userID, method string, success bool, duration time.Duration) {
    // 감사 로그 기록
    status := "SUCCESS"
    if !success {
        status = "FAILED"
    }
    log.Printf("📝 감사로그: 사용자=%s, 메서드=%s, 상태=%s, 소요시간=%v", 
        userID, method, status, duration)
}

Stream 인터셉터의 실행 흐름

func comprehensiveStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
    // ========== 스트림 시작 전 (Pre-processing) ==========
    log.Printf("🔵 [PRE] 스트림 시작: %s", info.FullMethod)
    
    // 1. 스트림 인증 확인
    ctx := ss.Context()
    userID, err := extractUserID(ctx)
    if err != nil {
        log.Printf("❌ [PRE] 스트림 인증 실패: %v", err)
        return status.Errorf(codes.Unauthenticated, "인증 필요")
    }
    
    // 2. 스트림 래핑 (메시지 가로채기 위해)
    wrappedStream := &wrappedServerStream{
        ServerStream: ss,
        userID:       userID,
        method:       info.FullMethod,
    }
    
    start := time.Now()
    log.Printf("✅ [PRE] 사용자 %s가 스트림 %s 시작", userID, info.FullMethod)
    
    // ========== 실제 스트림 핸들러 호출 ==========
    err = handler(srv, wrappedStream)
    
    // ========== 스트림 종료 후 (Post-processing) ==========
    duration := time.Since(start)
    
    if err != nil {
        log.Printf("❌ [POST] 스트림 실패: %s, 사용자: %s, 소요시간: %v, 에러: %v", 
            info.FullMethod, userID, duration, err)
        recordErrorMetric(info.FullMethod, err)
    } else {
        log.Printf("✅ [POST] 스트림 성공: %s, 사용자: %s, 소요시간: %v", 
            info.FullMethod, userID, duration)
        recordSuccessMetric(info.FullMethod, duration)
    }
    
    // 스트림 감사 로그
    auditLog(userID, info.FullMethod, err == nil, duration)
    
    log.Printf("🔵 [POST] 스트림 완료: %s", info.FullMethod)
    
    return err
}

// 스트림 래퍼 구조체
type wrappedServerStream struct {
    grpc.ServerStream
    userID string
    method string
}

// SendMsg 오버라이드 (응답 메시지 가로채기)
func (w *wrappedServerStream) SendMsg(m interface{}) error {
    log.Printf("📤 [STREAM] 응답 전송: 사용자=%s, 메서드=%s", w.userID, w.method)
    
    // 응답 전처리
    if err := preprocessResponse(m); err != nil {
        return err
    }
    
    // 실제 전송
    err := w.ServerStream.SendMsg(m)
    
    // 응답 후처리
    if err != nil {
        log.Printf("❌ [STREAM] 응답 전송 실패: %v", err)
    } else {
        log.Printf("✅ [STREAM] 응답 전송 성공")
    }
    
    return err
}

// RecvMsg 오버라이드 (요청 메시지 가로채기)
func (w *wrappedServerStream) RecvMsg(m interface{}) error {
    log.Printf("📥 [STREAM] 요청 수신: 사용자=%s, 메서드=%s", w.userID, w.method)
    
    // 실제 수신
    err := w.ServerStream.RecvMsg(m)
    
    if err != nil {
        if err != io.EOF {
            log.Printf("❌ [STREAM] 요청 수신 실패: %v", err)
        }
    } else {
        // 요청 후처리
        if err := preprocessRequest(m); err != nil {
            return err
        }
        log.Printf("✅ [STREAM] 요청 수신 성공")
    }
    
    return err
}

func preprocessResponse(resp interface{}) error {
    // 응답 전처리 로직
    return nil
}

func preprocessRequest(req interface{}) error {
    // 요청 후처리 로직
    return nil
}

실행 시점별 활용 사례

시점 Unary RPC Stream RPC 주요 활용 사례
Pre-processing handler() 호출 전 handler() 호출 전 인증, 권한 확인, 요청 검증, 로깅 시작
During - SendMsg()/RecvMsg() 오버라이드 메시지별 처리, 실시간 모니터링
Post-processing handler() 호출 후 handler() 호출 후 응답 처리, 메트릭 수집, 감사 로그, 정리 작업

조건부 인터셉터 적용

func conditionalInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // 특정 메서드에만 적용
    if info.FullMethod == "/user.UserService/GetHealth" {
        // 헬스체크는 바로 통과
        return handler(ctx, req)
    }
    
    // 개발 환경에서만 상세 로깅
    if isDevelopment() {
        log.Printf("🔧 [DEV] 상세 요청 정보: %+v", req)
    }
    
    // 프로덕션 환경에서만 메트릭 수집
    if isProduction() {
        defer func() {
            collectMetrics(info.FullMethod)
        }()
    }
    
    return handler(ctx, req)
}

func isDevelopment() bool {
    return os.Getenv("ENV") == "development"
}

func isProduction() bool {
    return os.Getenv("ENV") == "production"
}

func collectMetrics(method string) {
    // 메트릭 수집 로직
}

이렇게 인터셉터를 활용하면 요청의 전체 생명주기에 걸쳐 세밀한 제어와 모니터링이 가능합니다.

사전 준비사항

시스템 요구사항

  • 운영체제: Linux, macOS, Windows
  • 프로그래밍 언어: Go 1.19+, Python 3.7+ (선택)
  • 개발 도구: IDE (VS Code, GoLand 등)

필수 도구 설치

1. Protocol Buffers 컴파일러 설치

# macOS (Homebrew)
brew install protobuf

# Ubuntu/Debian
sudo apt install protobuf-compiler

# Windows (Chocolatey)
choco install protoc

2. Go 환경 설정

# Go 모듈 초기화
mkdir grpc-tutorial && cd grpc-tutorial
go mod init grpc-tutorial

# gRPC 관련 패키지 설치
go get google.golang.org/grpc
go get google.golang.org/protobuf
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc

3. Protocol Buffers 플러그인 설치

# Go 플러그인 설치
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Protocol Buffers 정의

1단계: 서비스 스키마 정의

proto/user.proto 파일을 생성합니다:

syntax = "proto3";

package user;

option go_package = "grpc-tutorial/pb";

// 사용자 정보 메시지
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  repeated string hobbies = 5;
  UserStatus status = 6;
}

// 사용자 상태 열거형
enum UserStatus {
  INACTIVE = 0;
  ACTIVE = 1;
  SUSPENDED = 2;
}

// 사용자 생성 요청
message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;
  repeated string hobbies = 4;
}

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

// 사용자 조회 요청
message GetUserRequest {
  int32 id = 1;
}

// 사용자 목록 조회 요청
message ListUsersRequest {
  int32 page = 1;
  int32 limit = 2;
}

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

// 사용자 스트리밍 요청
message StreamUsersRequest {
  int32 delay_seconds = 1;
}

// 사용자 서비스 정의
service UserService {
  // 단일 요청-응답
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
  
  // 서버 스트리밍
  rpc StreamUsers(StreamUsersRequest) returns (stream User);
  
  // 클라이언트 스트리밍
  rpc CreateBatchUsers(stream CreateUserRequest) returns (CreateUserResponse);
  
  // 양방향 스트리밍
  rpc ChatWithUsers(stream CreateUserRequest) returns (stream User);
}

2단계: Go 코드 생성

# pb 디렉토리 생성
mkdir pb

# Protocol Buffers 컴파일
protoc --go_out=pb --go_opt=paths=source_relative \
       --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
       proto/user.proto

서버 구현

기본 서버 구조

server/main.go 파일을 생성합니다:

package main

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

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    
    pb "grpc-tutorial/pb/proto"
)

// UserServer 구조체
type UserServer struct {
    pb.UnimplementedUserServiceServer
    users map[int32]*pb.User
    mutex sync.RWMutex
    nextID int32
}

// NewUserServer 생성자
func NewUserServer() *UserServer {
    return &UserServer{
        users:  make(map[int32]*pb.User),
        nextID: 1,
    }
}

// CreateUser 구현
func (s *UserServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
    s.mutex.Lock()
    defer s.mutex.Unlock()

    // 입력 검증
    if req.Name == "" {
        return nil, status.Errorf(codes.InvalidArgument, "이름은 필수입니다")
    }
    if req.Email == "" {
        return nil, status.Errorf(codes.InvalidArgument, "이메일은 필수입니다")
    }

    // 새 사용자 생성
    user := &pb.User{
        Id:      s.nextID,
        Name:    req.Name,
        Email:   req.Email,
        Age:     req.Age,
        Hobbies: req.Hobbies,
        Status:  pb.UserStatus_ACTIVE,
    }

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

    return &pb.CreateUserResponse{
        User:    user,
        Message: "사용자가 성공적으로 생성되었습니다",
    }, nil
}

// GetUser 구현
func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    s.mutex.RLock()
    defer s.mutex.RUnlock()

    user, exists := s.users[req.Id]
    if !exists {
        return nil, status.Errorf(codes.NotFound, "사용자를 찾을 수 없습니다: %d", req.Id)
    }

    return user, nil
}

// ListUsers 구현
func (s *UserServer) ListUsers(ctx context.Context, req *pb.ListUsersRequest) (*pb.ListUsersResponse, error) {
    s.mutex.RLock()
    defer s.mutex.RUnlock()

    var users []*pb.User
    for _, user := range s.users {
        users = append(users, user)
    }

    // 간단한 페이징 구현
    start := int(req.Page * req.Limit)
    end := start + int(req.Limit)
    
    if start > len(users) {
        start = len(users)
    }
    if end > len(users) {
        end = len(users)
    }

    return &pb.ListUsersResponse{
        Users: users[start:end],
        Total: int32(len(users)),
        Page:  req.Page,
    }, nil
}

// StreamUsers 구현 (서버 스트리밍)
func (s *UserServer) StreamUsers(req *pb.StreamUsersRequest, stream pb.UserService_StreamUsersServer) error {
    s.mutex.RLock()
    users := make([]*pb.User, 0, len(s.users))
    for _, user := range s.users {
        users = append(users, user)
    }
    s.mutex.RUnlock()

    for _, user := range users {
        if err := stream.Send(user); err != nil {
            return err
        }
        
        // 지연 시간 추가
        if req.DelaySeconds > 0 {
            time.Sleep(time.Duration(req.DelaySeconds) * time.Second)
        }
    }

    return nil
}

// CreateBatchUsers 구현 (클라이언트 스트리밍)
func (s *UserServer) CreateBatchUsers(stream pb.UserService_CreateBatchUsersServer) error {
    var count int32

    for {
        req, err := stream.Recv()
        if err == io.EOF {
            // 클라이언트가 전송 완료
            return stream.SendAndClose(&pb.CreateUserResponse{
                Message: fmt.Sprintf("%d명의 사용자가 생성되었습니다", count),
            })
        }
        if err != nil {
            return err
        }

        // 사용자 생성
        _, err = s.CreateUser(context.Background(), req)
        if err != nil {
            return err
        }
        count++
    }
}

// ChatWithUsers 구현 (양방향 스트리밍)
func (s *UserServer) ChatWithUsers(stream pb.UserService_ChatWithUsersServer) error {
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }

        // 사용자 생성
        resp, err := s.CreateUser(context.Background(), req)
        if err != nil {
            return err
        }

        // 생성된 사용자 전송
        if err := stream.Send(resp.User); err != nil {
            return err
        }
    }
}

func main() {
    // TCP 리스너 생성
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("포트 50051에서 리스닝 실패: %v", err)
    }

    // gRPC 서버 생성
    s := grpc.NewServer()
    
    // 서비스 등록
    pb.RegisterUserServiceServer(s, NewUserServer())

    log.Println("gRPC 서버가 포트 50051에서 시작됩니다...")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("서버 시작 실패: %v", err)
    }
}

## gRPC 서버 생성 방식 비교

### grpc.NewServer() vs 커스텀 NewAppServer()

gRPC 서버를 생성하는 방법에는 여러 가지가 있습니다:

#### 1. grpc.NewServer() - 표준 방식

```go
// 기본 gRPC 서버 생성
func main() {
    // 표준 gRPC 서버 생성
    s := grpc.NewServer()
    
    // 개별 서비스 등록
    pb.RegisterUserServiceServer(s, &UserServer{})
    pb.RegisterOrderServiceServer(s, &OrderServer{})
    
    lis, _ := net.Listen("tcp", ":50051")
    s.Serve(lis)
}

특징:

  • gRPC 라이브러리에서 제공하는 표준 서버
  • 각 서비스를 개별적으로 등록
  • 인터셉터, 옵션 등을 서버 생성 시 설정
  • 가장 일반적이고 권장되는 방식

2. NewAppServer() - 커스텀 애플리케이션 서버

// 커스텀 애플리케이션 서버
type AppServer struct {
    *grpc.Server
    userService  *UserServer
    orderService *OrderServer
    config       *Config
}

// 커스텀 애플리케이션 서버 생성자
func NewAppServer(config *Config) *AppServer {
    // 서버 옵션 설정
    opts := []grpc.ServerOption{
        grpc.UnaryInterceptor(loggingUnaryInterceptor),
        grpc.StreamInterceptor(loggingStreamInterceptor),
    }
    
    if config.TLSEnabled {
        creds, _ := credentials.NewServerTLSFromFile(config.CertFile, config.KeyFile)
        opts = append(opts, grpc.Creds(creds))
    }
    
    s := grpc.NewServer(opts...)
    
    app := &AppServer{
        Server:       s,
        userService:  NewUserServer(),
        orderService: NewOrderServer(),
        config:       config,
    }
    
    return app
}

// 통합 서비스 등록
func (app *AppServer) RegisterServices() {
    pb.RegisterUserServiceServer(app.Server, app.userService)
    pb.RegisterOrderServiceServer(app.Server, app.orderService)
}

// 애플리케이션 서버 시작
func (app *AppServer) Start(addr string) error {
    app.RegisterServices()
    
    lis, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    
    log.Printf("애플리케이션 서버가 %s에서 시작됩니다...", addr)
    return app.Serve(lis)
}

func main() {
    config := &Config{
        TLSEnabled: true,
        CertFile:   "server.crt",
        KeyFile:    "server.key",
    }
    
    // 커스텀 애플리케이션 서버 생성
    appServer := NewAppServer(config)
    
    if err := appServer.Start(":50051"); err != nil {
        log.Fatalf("서버 시작 실패: %v", err)
    }
}

특징:

  • 애플리케이션별 설정과 로직을 캡슐화
  • 여러 서비스를 한 번에 관리
  • 설정 파일, 의존성 주입 등을 통합 관리
  • 더 복잡한 애플리케이션에 적합

서비스 등록 함수 비교

RegisterUserServiceServer vs RegisterAppServer

// 1. 개별 서비스 등록 (표준 방식)
func main() {
    s := grpc.NewServer()
    
    // Protocol Buffers에서 자동 생성된 등록 함수
    pb.RegisterUserServiceServer(s, &UserServer{})
    pb.RegisterOrderServiceServer(s, &OrderServer{})
    pb.RegisterPaymentServiceServer(s, &PaymentServer{})
}

// 2. 통합 애플리케이션 서버 등록 (커스텀 방식)
type AppServiceServer interface {
    RegisterUserServiceServer(*grpc.Server, pb.UserServiceServer)
    RegisterOrderServiceServer(*grpc.Server, pb.OrderServiceServer)
    RegisterPaymentServiceServer(*grpc.Server, pb.PaymentServiceServer)
}

func RegisterAppServer(s *grpc.Server, app *AppServer) {
    // 모든 서비스를 한 번에 등록
    pb.RegisterUserServiceServer(s, app.userService)
    pb.RegisterOrderServiceServer(s, app.orderService)
    pb.RegisterPaymentServiceServer(s, app.paymentService)
    
    // 추가적인 설정이나 초기화 로직
    app.setupHealthCheck(s)
    app.setupMetrics(s)
}

비교표

항목 grpc.NewServer() NewAppServer()
복잡도 낮음 높음
유연성 높음 매우 높음
설정 관리 개별 설정 통합 설정
서비스 등록 개별 등록 통합 등록
의존성 관리 수동 자동화 가능
테스트 용이성 보통 높음
사용 권장 소규모 서비스 대규모 애플리케이션

실제 사용 예제 비교

// === 표준 방식 ===
func standardServer() {
    // 간단한 서버 생성
    s := grpc.NewServer(
        grpc.UnaryInterceptor(loggingInterceptor),
    )
    
    // 개별 서비스 등록
    pb.RegisterUserServiceServer(s, &UserServer{})
    
    lis, _ := net.Listen("tcp", ":50051")
    s.Serve(lis)
}

// === 애플리케이션 서버 방식 ===
func applicationServer() {
    // 설정 로드
    config := loadConfig()
    
    // 데이터베이스 연결
    db := connectDatabase(config.DatabaseURL)
    
    // 캐시 연결
    cache := connectRedis(config.RedisURL)
    
    // 의존성이 주입된 서비스들
    userService := NewUserService(db, cache)
    orderService := NewOrderService(db, cache)
    
    // 애플리케이션 서버 생성
    app := &AppServer{
        userService:  userService,
        orderService: orderService,
        db:          db,
        cache:       cache,
    }
    
    // 서버 시작
    app.Start(":50051")
}

권장사항:

  • 소규모 프로젝트: grpc.NewServer() 사용
  • 마이크로서비스: grpc.NewServer() + 개별 서비스 등록
  • 대규모 모놀리식: NewAppServer() + 통합 관리
  • 복잡한 설정: NewAppServer() + 설정 파일 관리

## 클라이언트 구현

### 기본 클라이언트

`client/main.go` 파일을 생성합니다:

```go
package main

import (
    "context"
    "io"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    
    pb "grpc-tutorial/pb/proto"
)

func main() {
    // 서버 연결
    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("서버 연결 실패: %v", err)
    }
    defer conn.Close()

    client := pb.NewUserServiceClient(conn)

    // 1. 단일 사용자 생성
    createUser(client)
    
    // 2. 사용자 조회
    getUser(client, 1)
    
    // 3. 사용자 목록 조회
    listUsers(client)
    
    // 4. 서버 스트리밍
    streamUsers(client)
    
    // 5. 클라이언트 스트리밍
    createBatchUsers(client)
    
    // 6. 양방향 스트리밍
    chatWithUsers(client)
}

// 단일 사용자 생성
func createUser(client pb.UserServiceClient) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req := &pb.CreateUserRequest{
        Name:    "홍길동",
        Email:   "hong@example.com",
        Age:     30,
        Hobbies: []string{"독서", "영화감상"},
    }

    resp, err := client.CreateUser(ctx, req)
    if err != nil {
        log.Printf("사용자 생성 실패: %v", err)
        return
    }

    log.Printf("사용자 생성 성공: %v", resp.User)
    log.Printf("메시지: %s", resp.Message)
}

// 사용자 조회
func getUser(client pb.UserServiceClient, id int32) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req := &pb.GetUserRequest{Id: id}
    
    user, err := client.GetUser(ctx, req)
    if err != nil {
        log.Printf("사용자 조회 실패: %v", err)
        return
    }

    log.Printf("사용자 조회 성공: %v", user)
}

// 사용자 목록 조회
func listUsers(client pb.UserServiceClient) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req := &pb.ListUsersRequest{
        Page:  0,
        Limit: 10,
    }

    resp, err := client.ListUsers(ctx, req)
    if err != nil {
        log.Printf("사용자 목록 조회 실패: %v", err)
        return
    }

    log.Printf("사용자 목록 조회 성공: 총 %d명", resp.Total)
    for _, user := range resp.Users {
        log.Printf("- %v", user)
    }
}

// 서버 스트리밍
func streamUsers(client pb.UserServiceClient) {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    req := &pb.StreamUsersRequest{DelaySeconds: 1}
    
    stream, err := client.StreamUsers(ctx, req)
    if err != nil {
        log.Printf("사용자 스트리밍 실패: %v", err)
        return
    }

    log.Println("사용자 스트리밍 시작...")
    for {
        user, err := stream.Recv()
        if err == io.EOF {
            log.Println("스트리밍 완료")
            break
        }
        if err != nil {
            log.Printf("스트리밍 에러: %v", err)
            break
        }

        log.Printf("스트리밍 받은 사용자: %v", user)
    }
}

// 클라이언트 스트리밍
func createBatchUsers(client pb.UserServiceClient) {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    stream, err := client.CreateBatchUsers(ctx)
    if err != nil {
        log.Printf("배치 사용자 생성 실패: %v", err)
        return
    }

    // 여러 사용자 데이터 전송
    users := []*pb.CreateUserRequest{
        {Name: "김철수", Email: "kim@example.com", Age: 25, Hobbies: []string{"축구"}},
        {Name: "이영희", Email: "lee@example.com", Age: 28, Hobbies: []string{"요리", "여행"}},
        {Name: "박민수", Email: "park@example.com", Age: 32, Hobbies: []string{"게임"}},
    }

    for _, user := range users {
        if err := stream.Send(user); err != nil {
            log.Printf("사용자 전송 실패: %v", err)
            return
        }
        log.Printf("사용자 전송: %s", user.Name)
        time.Sleep(1 * time.Second)
    }

    resp, err := stream.CloseAndRecv()
    if err != nil {
        log.Printf("배치 생성 완료 실패: %v", err)
        return
    }

    log.Printf("배치 생성 완료: %s", resp.Message)
}

// 양방향 스트리밍
func chatWithUsers(client pb.UserServiceClient) {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    stream, err := client.ChatWithUsers(ctx)
    if err != nil {
        log.Printf("양방향 스트리밍 실패: %v", err)
        return
    }

    // 고루틴으로 응답 수신
    go func() {
        for {
            user, err := stream.Recv()
            if err == io.EOF {
                return
            }
            if err != nil {
                log.Printf("수신 에러: %v", err)
                return
            }
            log.Printf("생성된 사용자 수신: %v", user)
        }
    }()

    // 사용자 데이터 전송
    users := []*pb.CreateUserRequest{
        {Name: "실시간1", Email: "real1@example.com", Age: 20},
        {Name: "실시간2", Email: "real2@example.com", Age: 22},
    }

    for _, user := range users {
        if err := stream.Send(user); err != nil {
            log.Printf("전송 실패: %v", err)
            return
        }
        log.Printf("사용자 전송: %s", user.Name)
        time.Sleep(2 * time.Second)
    }

    stream.CloseSend()
    time.Sleep(3 * time.Second) // 응답 수신 대기
}

실행 및 테스트

1단계: 서버 실행

# 서버 실행
go run server/main.go

2단계: 클라이언트 실행

다른 터미널에서:

# 클라이언트 실행
go run client/main.go

3단계: 결과 확인

클라이언트 실행 시 다음과 같은 출력을 확인할 수 있습니다:

사용자 생성 성공: id:1 name:"홍길동" email:"hong@example.com" age:30 hobbies:"독서" hobbies:"영화감상" status:ACTIVE
메시지: 사용자가 성공적으로 생성되었습니다
사용자 조회 성공: id:1 name:"홍길동" email:"hong@example.com" age:30 hobbies:"독서" hobbies:"영화감상" status:ACTIVE

고급 기능 구현

인터셉터 심화 학습

인터셉터 체이닝 (Multiple Interceptors)

여러 인터셉터를 함께 사용할 때는 체이닝이 필요합니다:

import "github.com/grpc-ecosystem/go-grpc-middleware"

func main() {
    // 여러 Unary 인터셉터 체이닝
    s := grpc.NewServer(
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            loggingUnaryInterceptor,
            authUnaryInterceptor,
            rateLimitUnaryInterceptor,
            recoveryUnaryInterceptor,
        )),
        grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
            loggingStreamInterceptor,
            authStreamInterceptor,
        )),
    )
    
    // 서비스 등록
    pb.RegisterUserServiceServer(s, NewUserServer())
    
    lis, _ := net.Listen("tcp", ":50051")
    s.Serve(lis)
}

// Rate Limiting 인터셉터
func rateLimitUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // 요청 빈도 제한 로직
    if !rateLimiter.Allow() {
        return nil, status.Errorf(codes.ResourceExhausted, "요청이 너무 빈번합니다")
    }
    
    return handler(ctx, req)
}

// Recovery 인터셉터 (패닉 처리)
func recoveryUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("패닉 발생: %v", r)
            err = status.Errorf(codes.Internal, "서버 내부 오류")
        }
    }()
    
    return handler(ctx, req)
}

메타데이터 활용

인터셉터에서 메타데이터를 활용한 고급 처리:

import (
    "google.golang.org/grpc/metadata"
)

// 메타데이터 처리 인터셉터
func metadataUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // 들어오는 메타데이터 읽기
    md, ok := metadata.FromIncomingContext(ctx)
    if ok {
        // 사용자 ID 추출
        if userIDs := md.Get("user-id"); len(userIDs) > 0 {
            log.Printf("사용자 ID: %s", userIDs[0])
        }
        
        // 요청 ID 생성 및 추가
        requestID := generateRequestID()
        md.Set("request-id", requestID)
        ctx = metadata.NewIncomingContext(ctx, md)
    }
    
    // 응답에 메타데이터 추가
    header := metadata.Pairs("server-version", "1.0.0")
    grpc.SendHeader(ctx, header)
    
    return handler(ctx, req)
}

// 클라이언트에서 메타데이터 전송
func clientWithMetadata() {
    conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    client := pb.NewUserServiceClient(conn)
    
    // 메타데이터 설정
    md := metadata.Pairs(
        "user-id", "12345",
        "authorization", "Bearer token123",
    )
    ctx := metadata.NewOutgoingContext(context.Background(), md)
    
    // 요청 전송
    resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 1})
    if err != nil {
        log.Printf("에러: %v", err)
        return
    }
    
    log.Printf("응답: %v", resp)
}

에러 처리 및 상태 코드

// 고급 에러 처리 예제
func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    // 입력 검증
    if req.Id <= 0 {
        return nil, status.Errorf(codes.InvalidArgument, "유효하지 않은 사용자 ID: %d", req.Id)
    }

    s.mutex.RLock()
    defer s.mutex.RUnlock()

    user, exists := s.users[req.Id]
    if !exists {
        return nil, status.Errorf(codes.NotFound, "사용자를 찾을 수 없습니다: %d", req.Id)
    }

    // 사용자 상태 확인
    if user.Status == pb.UserStatus_SUSPENDED {
        return nil, status.Errorf(codes.PermissionDenied, "정지된 사용자입니다: %d", req.Id)
    }

    return user, nil
}

보안 구현

TLS 인증서 설정

1. 인증서 생성

# 개발용 자체 서명 인증서 생성
openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -x509 -days 365 -out server-cert.pem -subj "/C=KR/ST=Seoul/L=Seoul/O=Example/OU=IT/CN=localhost"

2. TLS 서버 구현

// server/tls_main.go
func main() {
    // TLS 인증서 로드
    creds, err := credentials.NewServerTLSFromFile("server-cert.pem", "server-key.pem")
    if err != nil {
        log.Fatalf("TLS 인증서 로드 실패: %v", err)
    }

    // TLS와 함께 서버 생성
    s := grpc.NewServer(
        grpc.Creds(creds),
        grpc.UnaryInterceptor(loggingInterceptor),
    )

    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("포트 50051에서 리스닝 실패: %v", err)
    }

    pb.RegisterUserServiceServer(s, NewUserServer())

    log.Println("TLS gRPC 서버가 포트 50051에서 시작됩니다...")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("서버 시작 실패: %v", err)
    }
}

3. TLS 클라이언트 구현

// client/tls_main.go
func main() {
    // TLS 인증서로 연결
    creds, err := credentials.NewClientTLSFromFile("server-cert.pem", "localhost")
    if err != nil {
        log.Fatalf("TLS 인증서 로드 실패: %v", err)
    }

    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
    if err != nil {
        log.Fatalf("서버 연결 실패: %v", err)
    }
    defer conn.Close()

    client := pb.NewUserServiceClient(conn)
    // ... 나머지 클라이언트 코드
}

성능 최적화 및 모니터링

연결 풀링

// 클라이언트 연결 풀 관리
type ClientPool struct {
    connections []*grpc.ClientConn
    clients     []pb.UserServiceClient
    current     int
    mutex       sync.Mutex
}

func NewClientPool(size int, address string) (*ClientPool, error) {
    pool := &ClientPool{
        connections: make([]*grpc.ClientConn, size),
        clients:     make([]pb.UserServiceClient, size),
    }

    for i := 0; i < size; i++ {
        conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
            return nil, err
        }
        pool.connections[i] = conn
        pool.clients[i] = pb.NewUserServiceClient(conn)
    }

    return pool, nil
}

func (p *ClientPool) GetClient() pb.UserServiceClient {
    p.mutex.Lock()
    defer p.mutex.Unlock()
    
    client := p.clients[p.current]
    p.current = (p.current + 1) % len(p.clients)
    return client
}

헬스체크 구현

// 헬스체크 서비스 추가
import "google.golang.org/grpc/health/grpc_health_v1"

type HealthServer struct {
    grpc_health_v1.UnimplementedHealthServer
}

func (h *HealthServer) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
    return &grpc_health_v1.HealthCheckResponse{
        Status: grpc_health_v1.HealthCheckResponse_SERVING,
    }, nil
}

// 서버에 헬스체크 등록
grpc_health_v1.RegisterHealthServer(s, &HealthServer{})

실전 프로젝트: 완전한 사용자 관리 시스템

프로젝트 구조:

grpc-user-system/
├── proto/
│   └── user.proto
├── pb/
│   ├── user.pb.go
│   └── user_grpc.pb.go
├── server/
│   ├── main.go
│   ├── interceptor.go
│   └── database.go
├── client/
│   ├── main.go
│   └── cli.go
├── docker-compose.yml
└── Makefile

Docker Compose 설정

# docker-compose.yml
version: '3.8'

services:
  grpc-server:
    build: 
      context: .
      dockerfile: Dockerfile.server
    ports:
      - "50051:50051"
    environment:
      - DB_HOST=postgres
      - DB_USER=user
      - DB_PASSWORD=password
      - DB_NAME=userdb
    depends_on:
      - postgres

  postgres:
    image: postgres:13
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=userdb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Makefile

# Makefile
.PHONY: proto build run-server run-client docker-up docker-down

proto:
 protoc --go_out=pb --go_opt=paths=source_relative \
        --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
        proto/user.proto

build:
 go build -o bin/server server/main.go
 go build -o bin/client client/main.go

run-server:
 go run server/main.go

run-client:
 go run client/main.go

docker-up:
 docker-compose up -d

docker-down:
 docker-compose down

test:
 go test ./...

clean:
 rm -rf bin/
 rm -rf pb/*.go

트러블슈팅 가이드

일반적인 문제들

1. 포트 충돌

# 포트 사용 확인
lsof -i :50051

# 프로세스 종료
kill -9 <PID>

2. Protocol Buffers 버전 불일치

# protoc 버전 확인
protoc --version

# Go 플러그인 재설치
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

3. 연결 오류

// 연결 상태 확인
func checkConnection(conn *grpc.ClientConn) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    state := conn.GetState()
    log.Printf("연결 상태: %v", state)
    
    if !conn.WaitForStateChange(ctx, state) {
        log.Printf("연결 상태 변경 타임아웃")
    }
}

마무리 및 다음 단계

학습한 내용 요약

  1. gRPC 기본 개념: Protocol Buffers, HTTP/2 기반 통신
  2. 서비스 정의: .proto 파일을 통한 스키마 정의
  3. 서버 구현: 4가지 통신 패턴 구현
  4. 클라이언트 구현: 동기/비동기 호출 방법
  5. 고급 기능: 인터셉터, TLS, 헬스체크
  6. 실전 적용: Docker, 모니터링, 성능 최적화

다음 학습 단계

  • gRPC-Gateway: REST API와 gRPC 간 브리지
  • gRPC-Web: 브라우저에서 gRPC 사용
  • Load Balancing: 분산 환경에서의 로드 밸런싱
  • 서비스 메시: Istio, Linkerd와 gRPC 통합
  • 마이크로서비스: gRPC 기반 MSA 설계

유용한 리소스

gRPC는 현대적인 분산 시스템에서 필수적인 기술입니다. 이 가이드를 통해 gRPC의 핵심 개념을 이해하고 실제 프로젝트에 적용해보시기 바랍니다!


참고 자료: