gRPC — Protocol Buffers와 서비스 정의
gRPC는 Google이 개발한 고성능 RPC 프레임워크로, Protocol Buffers(protobuf)를 인터페이스 정의 언어(IDL)로 사용합니다. REST/JSON 대비 최대 10배 빠른 직렬화 속도와 강타입 계약을 제공합니다.
gRPC vs REST 비교
| 항목 | gRPC | REST/JSON |
|---|---|---|
| 프로토콜 | HTTP/2 | HTTP/1.1 or 2 |
| 직렬화 | Protocol Buffers (바이너리) | JSON (텍스트) |
| API 계약 | .proto 파일 (강타입) | OpenAPI (느슨) |
| 스트리밍 | 양방향 지원 | 제한적 |
| 브라우저 | 직접 지원 불가 | 지원 |
| 주요 용도 | 마이크로서비스 간 통신 | 공개 API |
설치 및 설정
# protoc 컴파일러 설치 (https://github.com/protocolbuffers/protobuf/releases)
# Go 플러그인 설치
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# gRPC 라이브러리
go get google.golang.org/grpc
go get google.golang.org/protobuf
Proto 파일 정의
// proto/user/v1/user.proto
syntax = "proto3";
package user.v1;
option go_package = "myapp/gen/user/v1;userv1";
import "google/protobuf/timestamp.proto";
// 메시지 정의
message User {
int64 id = 1;
string name = 2;
string email = 3;
google.protobuf.Timestamp created_at = 4;
}
message GetUserRequest {
int64 id = 1;
}
message GetUserResponse {
User user = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
// 서비스 정의
service UserService {
// 단항 RPC (Unary)
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (User);
// 서버 스트리밍
rpc ListUsers(ListUsersRequest) returns (stream User);
// 클라이언트 스트리밍
rpc BatchCreateUsers(stream CreateUserRequest) returns (ListUsersResponse);
// 양방향 스트리밍
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
# Go 코드 생성
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/user/v1/user.proto
생성되는 파일:
gen/user/v1/user.pb.go— 메시지 구조체gen/user/v1/user_grpc.pb.go— 서버/클라이언트 인터페이스
서버 구현
// internal/service/user_service.go
package service
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
userv1 "myapp/gen/user/v1"
)
type UserService struct {
userv1.UnimplementedUserServiceServer // 미래 메서드 호환성
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// 단항 RPC
func (s *UserService) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
if req.Id <= 0 {
return nil, status.Errorf(codes.InvalidArgument, "ID는 양수여야 합니다: %d", req.Id)
}
user, err := s.repo.FindByID(ctx, req.Id)
if err != nil {
return nil, status.Errorf(codes.Internal, "DB 조회 실패: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "사용자 없음: %d", req.Id)
}
return &userv1.GetUserResponse{
User: &userv1.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
CreatedAt: timestamppb.New(user.CreatedAt),
},
}, nil
}
// 서버 스트리밍 RPC
func (s *UserService) ListUsers(req *userv1.ListUsersRequest, stream userv1.UserService_ListUsersServer) error {
users, err := s.repo.FindAll(stream.Context())
if err != nil {
return status.Errorf(codes.Internal, "조회 실패: %v", err)
}
for _, user := range users {
if err := stream.Send(&userv1.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}); err != nil {
return err
}
}
return nil
}
// 클라이언트 스트리밍 RPC
func (s *UserService) BatchCreateUsers(stream userv1.UserService_BatchCreateUsersServer) error {
var created []*userv1.User
for {
req, err := stream.Recv()
if err == io.EOF {
break // 클라이언트가 전송 완료
}
if err != nil {
return status.Errorf(codes.Internal, "스트림 수신 실패: %v", err)
}
user, err := s.repo.Create(stream.Context(), req.Name, req.Email)
if err != nil {
return err
}
created = append(created, &userv1.User{Id: user.ID, Name: user.Name})
}
return stream.SendAndClose(&userv1.ListUsersResponse{Users: created})
}
gRPC 서버 시작
// cmd/server/main.go
package main
import (
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
userv1 "myapp/gen/user/v1"
"myapp/internal/service"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("리슨 실패: %v", err)
}
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
loggingInterceptor,
recoveryInterceptor,
),
)
// 서비스 등록
userv1.RegisterUserServiceServer(grpcServer, service.NewUserService(repo))
// 개발 시 리플렉션 활성화 (grpcurl로 서비스 탐색)
reflection.Register(grpcServer)
log.Printf("gRPC 서버 시작: :50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("서버 실패: %v", err)
}
}
클라이언트 구현
// cmd/client/main.go
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
userv1 "myapp/gen/user/v1"
)
func main() {
// 연결 (프로덕션에서는 TLS 사용)
conn, err := grpc.Dial("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
log.Fatalf("연결 실패: %v", err)
}
defer conn.Close()
client := userv1.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 단항 RPC 호출
resp, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})
if err != nil {
// gRPC 에러 코드 처리
if st, ok := status.FromError(err); ok {
switch st.Code() {
case codes.NotFound:
log.Printf("사용자 없음")
case codes.DeadlineExceeded:
log.Printf("타임아웃")
default:
log.Printf("에러: %v", st.Message())
}
}
return
}
log.Printf("사용자: %v", resp.User)
// 서버 스트리밍 클라이언트
stream, err := client.ListUsers(ctx, &userv1.ListUsersRequest{})
if err != nil {
log.Fatal(err)
}
for {
user, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
log.Printf("사용자: %v", user)
}
}
인터셉터 (미들웨어)
// 서버 단항 인터셉터
func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s err=%v",
info.FullMethod, time.Since(start), err)
return resp, err
}
// 패닉 복구 인터셉터
func recoveryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
err = status.Errorf(codes.Internal, "내부 오류: %v", r)
}
}()
return handler(ctx, req)
}
gRPC 테스트
func TestGetUser(t *testing.T) {
// 인메모리 서버
lis := bufconn.Listen(1024 * 1024)
grpcServer := grpc.NewServer()
userv1.RegisterUserServiceServer(grpcServer, mockService)
go grpcServer.Serve(lis)
defer grpcServer.Stop()
// 클라이언트 연결
conn, _ := grpc.DialContext(ctx, "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
defer conn.Close()
client := userv1.NewUserServiceClient(conn)
resp, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})
assert.NoError(t, err)
assert.Equal(t, int64(1), resp.User.Id)
}
핵심 정리
| RPC 종류 | 정의 | 용도 |
|---|---|---|
| 단항(Unary) | rpc Method(Req) returns (Resp) | 일반 요청/응답 |
| 서버 스트리밍 | returns (stream Resp) | 대량 데이터 전송 |
| 클라이언트 스트리밍 | (stream Req) returns (Resp) | 업로드 |
| 양방향 스트리밍 | (stream Req) returns (stream Resp) | 채팅, 실시간 |
UnimplementedXxxServer임베딩으로 새 메서드 추가 시 컴파일 에러 방지status.Errorf(codes.NotFound, "...")로 표준 gRPC 에러 반환- 인터셉터는 HTTP 미들웨어와 동일한 역할 — 로깅/인증/회복