Protocol Buffers 정의부터 Go에서의 gRPC 서버/클라이언트 구현, 유니어리/스트리밍 통신, 인터셉터, gRPC-Gateway를 통한 REST 변환, 헬스 체킹과 서비스 디스커버리를 다룹니다.
Protocol Buffers(Protobuf)는 구글이 개발한 언어 중립적인 직렬화 형식입니다. JSON과 비교하여 직렬화/역직렬화 속도가 빠르고 바이너리 크기가 작습니다. gRPC의 기본 메시지 형식이며, 서비스 인터페이스를 정의하는 IDL(Interface Definition Language) 역할도 합니다.
syntax = "proto3";
package user.v1;
option go_package = "github.com/myapp/gen/user/v1;userv1";
import "google/protobuf/timestamp.proto";
// 사용자 메시지 정의
message User {
int64 id = 1;
string name = 2;
string email = 3;
Role role = 4;
google.protobuf.Timestamp created_at = 5;
}
enum Role {
ROLE_UNSPECIFIED = 0;
ROLE_USER = 1;
ROLE_ADMIN = 2;
}
// 서비스 정의
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}
message GetUserRequest {
int64 id = 1;
}
message GetUserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
message CreateUserRequest {
string name = 1;
string email = 2;
string password = 3;
}
message CreateUserResponse {
User user = 1;
}# 도구 설치
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Go 코드 생성
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/user/v1/user.protobuf 도구를 사용하면 protoc 대신 더 현대적인 방식으로 Protobuf를 관리할 수 있습니다. 린팅(Linting), 호환성 검사(Breaking Change Detection), 의존성 관리를 통합적으로 지원합니다.
유니어리(Unary) RPC는 클라이언트가 하나의 요청을 보내고 서버가 하나의 응답을 반환하는 가장 기본적인 통신 방식입니다.
import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
userv1 "github.com/myapp/gen/user/v1"
)
type userServer struct {
userv1.UnimplementedUserServiceServer
repo UserRepository
}
func (s *userServer) 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 {
if errors.Is(err, ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "사용자를 찾을 수 없습니다: ID %d", req.Id)
}
return nil, status.Errorf(codes.Internal, "사용자 조회 실패: %v", err)
}
return &userv1.GetUserResponse{
User: toProtoUser(user),
}, nil
}
func (s *userServer) CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error) {
if req.Name == "" || req.Email == "" {
return nil, status.Error(codes.InvalidArgument, "이름과 이메일은 필수입니다")
}
user := &User{
Name: req.Name,
Email: req.Email,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, status.Errorf(codes.Internal, "사용자 생성 실패: %v", err)
}
return &userv1.CreateUserResponse{
User: toProtoUser(user),
}, nil
}
// 서버 시작
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal("리슨 실패:", err)
}
grpcServer := grpc.NewServer()
userv1.RegisterUserServiceServer(grpcServer, &userServer{repo: repo})
slog.Info("gRPC 서버 시작", "addr", ":50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatal("서버 실행 실패:", err)
}
}gRPC는 자체적인 상태 코드 체계를 가지고 있습니다.
codes.OK -> 200 OK
codes.InvalidArgument -> 400 Bad Request
codes.Unauthenticated -> 401 Unauthorized
codes.PermissionDenied-> 403 Forbidden
codes.NotFound -> 404 Not Found
codes.AlreadyExists -> 409 Conflict
codes.Internal -> 500 Internal Server Error
codes.Unavailable -> 503 Service Unavailable
codes.DeadlineExceeded-> 504 Gateway Timeout서버가 여러 응답을 순차적으로 전송합니다.
service EventService {
rpc StreamEvents(StreamEventsRequest) returns (stream Event);
}func (s *eventServer) StreamEvents(
req *eventv1.StreamEventsRequest,
stream eventv1.EventService_StreamEventsServer,
) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-stream.Context().Done():
return nil
case t := <-ticker.C:
event := &eventv1.Event{
Id: uuid.New().String(),
Type: "heartbeat",
Timestamp: timestamppb.New(t),
}
if err := stream.Send(event); err != nil {
return status.Errorf(codes.Internal, "이벤트 전송 실패: %v", err)
}
}
}
}클라이언트와 서버가 동시에 메시지를 주고받습니다.
service ChatService {
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}func (s *chatServer) Chat(stream chatv1.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return status.Errorf(codes.Internal, "메시지 수신 실패: %v", err)
}
// 메시지 처리 후 응답
response := &chatv1.ChatMessage{
UserId: "system",
Content: fmt.Sprintf("수신 확인: %s", msg.Content),
}
if err := stream.Send(response); err != nil {
return status.Errorf(codes.Internal, "응답 전송 실패: %v", err)
}
}
}인터셉터(Interceptor)는 gRPC 버전의 미들웨어로, 요청 처리 전후에 횡단 관심사를 처리합니다.
// 로깅 인터셉터
func LoggingInterceptor(
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 := status.Code(err)
slog.Info("gRPC 요청 처리",
"method", info.FullMethod,
"status", statusCode.String(),
"duration_ms", duration.Milliseconds(),
)
return resp, err
}
// 인증 인터셉터
func AuthInterceptor(jwtSvc *JWTService) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// 인증이 필요 없는 메서드 건너뛰기
if isPublicMethod(info.FullMethod) {
return handler(ctx, req)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "메타데이터 없음")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "인증 토큰 없음")
}
claims, err := jwtSvc.ValidateToken(strings.TrimPrefix(tokens[0], "Bearer "))
if err != nil {
return nil, status.Error(codes.Unauthenticated, "유효하지 않은 토큰")
}
ctx = context.WithValue(ctx, userClaimsKey, claims)
return handler(ctx, req)
}
}
// 서버에 인터셉터 적용
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
LoggingInterceptor,
AuthInterceptor(jwtSvc),
RecoveryInterceptor,
),
)gRPC-Gateway는 gRPC 서비스를 RESTful HTTP API로 자동 변환합니다. 하나의 서비스 정의로 gRPC와 REST를 동시에 제공할 수 있습니다.
import "google/api/annotations.proto";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/api/v1/users/{id}"
};
}
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
option (google.api.http) = {
post: "/api/v1/users"
body: "*"
};
}
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {
option (google.api.http) = {
get: "/api/v1/users"
};
}
}func runGateway(ctx context.Context, grpcAddr, httpAddr string) error {
mux := runtime.NewServeMux(
runtime.WithMarshalerOption(
runtime.MIMEWildcard,
&runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
},
},
),
)
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err := userv1.RegisterUserServiceHandlerFromEndpoint(ctx, mux, grpcAddr, opts)
if err != nil {
return fmt.Errorf("게이트웨이 등록 실패: %w", err)
}
slog.Info("HTTP 게이트웨이 시작", "addr", httpAddr)
return http.ListenAndServe(httpAddr, mux)
}gRPC-Gateway를 사용하면 내부 서비스 간에는 gRPC의 성능 이점을 활용하고, 외부 클라이언트(웹 브라우저, 모바일 앱)에는 REST API를 제공하는 패턴을 쉽게 구현할 수 있습니다.
gRPC 헬스 체킹 프로토콜은 로드밸런서와 오케스트레이션 도구가 서비스 상태를 확인하는 표준 방법입니다.
import "google.golang.org/grpc/health"
import healthpb "google.golang.org/grpc/health/grpc_health_v1"
func main() {
grpcServer := grpc.NewServer()
// 헬스 체크 서비스 등록
healthServer := health.NewServer()
healthpb.RegisterHealthServer(grpcServer, healthServer)
// 서비스 상태 설정
healthServer.SetServingStatus("user.v1.UserService",
healthpb.HealthCheckResponse_SERVING)
// 서비스 중지 시
// healthServer.SetServingStatus("user.v1.UserService",
// healthpb.HealthCheckResponse_NOT_SERVING)
}마이크로서비스 환경에서 서비스 인스턴스를 동적으로 찾는 패턴입니다.
// gRPC 리졸버를 통한 서비스 디스커버리
conn, err := grpc.Dial(
"dns:///user-service.default.svc.cluster.local:50051",
grpc.WithDefaultServiceConfig(`{
"loadBalancingConfig": [{"round_robin": {}}]
}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)Kubernetes 환경에서는 DNS 기반 서비스 디스커버리가 가장 간단합니다. gRPC의 내장 리졸버(Resolver)가 DNS SRV 레코드를 조회하여 서비스 인스턴스를 자동으로 찾고, 라운드 로빈 등의 로드밸런싱 정책을 적용합니다.
이번 장에서 살펴본 핵심 내용을 정리합니다.
11장에서는 실전 프로젝트로 이 시리즈의 모든 내용을 종합합니다. Chi 기반 API 서버, PostgreSQL(pgx), JWT 인증, 미들웨어 스택, 구조화된 로깅, 테스트, Docker 빌드, CI/CD 파이프라인까지 완전한 백엔드 서비스를 구축합니다.
이 글이 도움이 되셨나요?
Go의 testing 패키지를 중심으로 테이블 기반 테스트, testify, httptest, 목(Mock) 생성, testcontainers-go 통합 테스트, 벤치마크, 퍼징 테스트 전략을 다룹니다.
시리즈의 모든 내용을 종합하여 Chi 기반 REST API 서버, PostgreSQL(pgx), JWT 인증, 미들웨어 스택, 구조화된 로깅, 테스트, Docker 빌드, CI/CD 파이프라인을 갖춘 완전한 백엔드를 구축합니다.
Go 백엔드의 미들웨어 패턴, JWT 인증 구현, CORS 설정, 레이트 리미팅, 요청 유효성 검사, OpenAPI/Swagger 문서 생성, log/slog를 활용한 구조화된 로깅을 다룹니다.