gRPC 완전 가이드: 초보자를 위한 단계별 마스터 튜토리얼
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 서버와 클라이언트에서 요청/응답을 가로채어 공통 로직을 처리하는 미들웨어입니다.
인터셉터 종류
- Unary Interceptor: 단일 요청-응답에 대한 인터셉터
- 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("연결 상태 변경 타임아웃")
}
}
마무리 및 다음 단계
학습한 내용 요약
- gRPC 기본 개념: Protocol Buffers, HTTP/2 기반 통신
- 서비스 정의: .proto 파일을 통한 스키마 정의
- 서버 구현: 4가지 통신 패턴 구현
- 클라이언트 구현: 동기/비동기 호출 방법
- 고급 기능: 인터셉터, TLS, 헬스체크
- 실전 적용: Docker, 모니터링, 성능 최적화
다음 학습 단계
- gRPC-Gateway: REST API와 gRPC 간 브리지
- gRPC-Web: 브라우저에서 gRPC 사용
- Load Balancing: 분산 환경에서의 로드 밸런싱
- 서비스 메시: Istio, Linkerd와 gRPC 통합
- 마이크로서비스: gRPC 기반 MSA 설계
유용한 리소스
- 공식 문서: gRPC 공식 사이트
- Go 튜토리얼: gRPC Go 퀵스타트
- Protocol Buffers: Protobuf 가이드
- 예제 코드: gRPC 공식 예제
gRPC는 현대적인 분산 시스템에서 필수적인 기술입니다. 이 가이드를 통해 gRPC의 핵심 개념을 이해하고 실제 프로젝트에 적용해보시기 바랍니다!
참고 자료: