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의 주요 장점
- 높은 성능: HTTP/2와 Protocol Buffers로 최적화된 통신
- 타입 안전성: 강타입 스키마로 런타임 오류 방지
- 언어 독립성: 다양한 언어 간 seamless 통신
- 스트리밍 지원: 실시간 데이터 처리 가능
- 자동 코드 생성: 개발 생산성 향상
🛠️ 실무 적용 가이드
- 마이크로서비스: 서비스 간 내부 통신에 최적
- 실시간 시스템: 스트리밍 기능으로 실시간 데이터 처리
- 모바일 앱: 효율적인 배터리 사용과 빠른 통신
- IoT 시스템: 경량화된 프로토콜로 디바이스 통신
gRPC를 도입할 때는 팀의 기술 수준, 기존 인프라, 그리고 요구사항을 종합적으로 고려하여 점진적으로 적용하는 것이 좋습니다. 특히 REST API와의 하이브리드 구조로 시작하여 경험을 쌓아가는 것을 추천합니다.