본문으로 건너뛰기

gRPC — Protocol Buffers와 서비스 정의

gRPC는 Google이 개발한 고성능 RPC 프레임워크로, Protocol Buffers(protobuf)를 인터페이스 정의 언어(IDL)로 사용합니다. REST/JSON 대비 최대 10배 빠른 직렬화 속도와 강타입 계약을 제공합니다.


gRPC vs REST 비교

항목gRPCREST/JSON
프로토콜HTTP/2HTTP/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 미들웨어와 동일한 역할 — 로깅/인증/회복