Go Protobuf와 GRPC-Gateway를 이용한 듀얼 퍼포즈 API 구축
James Reed
Infrastructure Engineer · Leapcell

소개
오늘날 상호 연결된 소프트웨어 환경에서 마이크로서비스 아키텍처와 다양한 클라이언트 애플리케이션이 일반적입니다. 이는 종종 문제를 야기합니다. 성능이 뛰어나고 타입 안전한 API를 내부 서비스 간 통신을 위해 구축하는 동시에 외부 웹 기반 클라이언트에서도 접근 가능하고 관용적인 API를 어떻게 구축할 수 있을까요? 기존의 RESTful API는 외부 소비자에겐 충분할 수 있지만, 서비스 간 통신에서 원하는 강력한 타이핑과 효율성이 부족한 경우가 많습니다. 반대로 순수 gRPC는 내부 시나리오에선 뛰어나지만 웹 브라우저에서 직접 소비하기는 어렵습니다. 이 글에서는 Go, Google의 Protocol Buffers (Protobuf), gRPC-Gateway를 사용하여 API 정의의 단일 진실 공급원을 구축하고 내부 gRPC 소비자와 외부 RESTful 클라이언트 모두를 원활하게 지원하는 강력한 솔루션을 자세히 살펴봅니다. 이 조합이 어떻게 개발을 간소화하고, 중복을 줄이며, 더 유지 관리 가능하고 확장 가능한 API 생태계를 육성하는지 알아봅니다.
핵심 개념
구현을 살펴보기 전에 관련된 핵심 기술에 대한 명확한 이해를 확립해 봅시다.
- Protocol Buffers (Protobuf): 구조화된 데이터를 직렬화하기 위한 언어 불가지론적, 플랫폼 불가지론적, 확장 가능한 메커니즘입니다. Protobuf는
.proto
파일을 사용하여 데이터 스키마를 정의하며, 인터페이스 정의 언어(IDL) 역할을 합니다. 이 스키마에서 컴파일러는 마샬링 및 언마샬링 데이터를 위해 다양한 언어(예: Go)로 코드를 생성합니다. 바이너리 직렬화는 매우 효율적이어서 고성능 통신에 이상적입니다. - gRPC: 고성능, 오픈 소스 범용 RPC 프레임워크입니다. Protobuf를 기반으로 구축된 gRPC는 직렬화, 네트워크 통신 및 메서드 호출을 자동으로 처리합니다. 스트리밍, 인증, 로드 밸런싱과 같은 기능을 지원하여 서비스 간 통신에 강력한 선택이 됩니다. Protobuf에서 파생된 강력한 타이핑은 컴파일 타임 검사와 신뢰할 수 있는 데이터 계약을 보장합니다.
- gRPC-Gateway: Protobuf 컴파일러를 위한 플러그인으로, 역방향 프록시 서버를 생성합니다. 이 프록시는 HTTP/JSON 요청을 gRPC 요청으로 변환한 다음 실제 gRPC 서비스로 전달합니다. 또한 gRPC 응답을 HTTP/JSON 응답으로 다시 변환합니다. 본질적으로 gRPC-Gateway를 사용하면 gRPC 서비스를 일반적인 RESTful API로 노출할 수 있으므로 특정 gRPC 클라이언트 라이브러리를 요구하지 않고도 웹 브라우저와 기타 HTTP 클라이언트에서 소비할 수 있습니다.
듀얼 퍼포즈 API 구축
이 접근 방식의 핵심 아이디어는 Protobuf를 사용하여 API를 한 번 정의하는 것입니다. 이 단일 정의는 gRPC 서비스와 gRPC-Gateway에서 생성된 RESTful 대응 모두에 대한 계약 역할을 합니다.
1. Protobuf로 API 정의
"Todo" 서비스에 대한 간단한 API를 정의하는 것부터 시작하겠습니다. proto/todo/todo.proto
라는 파일을 만듭니다.
syntax = "proto3"; package todo; option go_package = "github.com/your/repo/gen/proto/go/todo"; import "google/api/annotations.proto"; service TodoService { rpc CreateTodo(CreateTodoRequest) returns (Todo) { option (google.api.http) = { post: "/v1/todos" body: "*" }; } rpc GetTodo(GetTodoRequest) returns (Todo) { option (google.api.http) = { get: "/v1/todos/{id}" }; } rpc ListTodos(ListTodosRequest) returns (ListTodosResponse) { option (google.api.http) = { get: "/v1/todos" }; } } message Todo { string id = 1; string title = 2; string description = 3; bool completed = 4; } message CreateTodoRequest { string title = 1; string description = 2; } message GetTodoRequest { string id = 1; } message ListTodosRequest { } message ListTodosResponse { repeated Todo todos = 1; }
google/api/annotations.proto
import와 option (google.api.http)
주석을 주목하세요. 이것은 gRPC-Gateway에게 중요합니다. Protobuf 메서드가 HTTP 메서드, 경로 및 요청/응답 본문에 어떻게 매핑되는지 정의합니다.
2. Go 코드 생성
다음으로 Protobuf 정의를 Go 코드로 컴파일해야 합니다. 이를 위해서는 protoc
와 Go Protobuf 및 gRPC 플러그인, 그리고 gRPC-Gateway 플러그인이 필요합니다. 이것들이 설치되었다고 가정하면 Makefile
이나 스크립트를 컴파일에 사용할 수 있습니다.
# protoc-gen-go, protoc-gen-go-grpc, protoc-gen-grpc-gateway, protoc-gen-openapiv2 설치 # go install google.golang.org/protobuf/cmd/protoc-gen-go@latest # go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest # go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest # go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest mkdir -p gen/proto/go/todo protoc -I. -I/usr/local/include -I$(go env GOPATH)/src -I$(go env GOPATH)/pkg/mod --go_out=paths=source_relative:gen/proto/go --go-grpc_out=paths=source_relative:gen/proto/go --grpc-gateway_out=paths=source_relative:gen/proto/go --openapiv2_out=gen/proto/openapiv2 proto/todo/todo.proto
이 명령은 여러 파일을 생성합니다.
gen/proto/go/todo/todo.pb.go
: Protobuf 메시지 구조체와 헬퍼 함수를 포함합니다.gen/proto/go/todo/todo_grpc.pb.go
: gRPC 서비스 인터페이스와 클라이언트/서버 스텁을 포함합니다.gen/proto/go/todo/todo.pb.gw.go
: gRPC-Gateway 역방향 프록시 코드입니다.gen/proto/openapiv2/todo/todo.swagger.json
: OpenAPI/Swagger 문서 (필수적이지는 않지만 매우 유용합니다!)
3. gRPC 서비스 구현
이제 TodoService
에 대한 실제 비즈니스 로직을 구현합니다.
// main.go (설명을 위해 단순화) package main import ( "context" "log" "net" "net/http" "os" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "github.com/your/repo/gen/proto/go/todo" ) // todoService는 gRPC TodoService 인터페이스를 구현합니다 type todoService struct { pb.UnimplementedTodoServiceServer // 이전 호환성을 위해 포함 } func NewTodoService() *todoService { return &todoService{} } func (s *todoService) CreateTodo(ctx context.Context, req *pb.CreateTodoRequest) (*pb.Todo, error) { log.Printf("Received CreateTodo request: %v", req) // 실제 애플리케이션에서는 데이터베이스에 저장합니다 return &pb.Todo{ Id: "id-generated-123", // ID 생성 시뮬레이션 Title: req.GetTitle(), Description: req.GetDescription(), Completed: false, }, nil } func (s *todoService) GetTodo(ctx context.Context, req *pb.GetTodoRequest) (*pb.Todo, error) { log.Printf("Received GetTodo request: %v", req) // DB에서 가져오기 시뮬레이션 if req.GetId() == "id-generated-123" { return &pb.Todo{ Id: "id-generated-123", Title: "My First Todo", Description: "This is a detailed description.", Completed: false, }, nil } return nil, grpc.Errorf(codes.NotFound, "Todo with ID %s not found", req.GetId()) } func (s *todoService) ListTodos(ctx context.Context, req *pb.ListTodosRequest) (*pb.ListTodosResponse, error) { log.Printf("Received ListTodos request") // 모든 todo 가져오기 시뮬레이션 return &pb.ListTodosResponse{ Todos: []*pb.Todo{ {Id: "id-generated-123", Title: "My First Todo", Description: "Description 1", Completed: false}, {Id: "id-456", Title: "Buy groceries", Description: "Milk, eggs, bread", Completed: true}, }, }, nil } func main() { grpcPort := ":8080" httpPort := ":8081" // gRPC 서버 시작 lis, err := net.Listen("tcp", grpcPort) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterTodoServiceServer(s, NewTodoService()) log.Printf("gRPC server listening on %s", grpcPort) go func() { if err := s.Serve(lis); err != nil { log.Fatalf("Failed to serve gRPC: %v", err) } }() // gRPC-Gateway 프록시 시작 ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} err = pb.RegisterTodoServiceHandlerFromEndpoint(ctx, mux, "localhost"+grpcPort, opts) if err != nil { log.Fatalf("Failed to register gateway: %v", err) } log.Printf("gRPC-Gateway server listening on %s", httpPort) if err := http.ListenAndServe(httpPort, mux); err != nil { log.Fatalf("Failed to serve gRPC-Gateway: %v", err) } }
이 main.go
는 두 개의 서버를 설정합니다.
- 포트 8080에서 수신 대기하는 gRPC 서버로,
TodoService
를 구현합니다. - 포트 8081에서 수신 대기하는 gRPC-Gateway 프록시 서버로, HTTP/JSON 요청을 gRPC 서버로 전달합니다.
4. 애플리케이션 시나리오
이 설정을 통해 이제 다음을 수행할 수 있습니다.
-
내부 마이크로서비스: 다른 Go 마이크로서비스는
localhost:8080
에서 gRPC를 사용하여TodoService
와 직접 통신할 수 있습니다. 강력한 타이핑, 효율적인 바이너리 직렬화 및 gRPC의 고급 기능을 활용합니다.// 내부 gRPC 클라이언트 예제 package main import ( "context" "log" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "github.com/your/repo/gen/proto/go/todo" ) func main() { conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() client := pb.NewTodoServiceClient(conn) res, err := client.CreateTodo(context.Background(), &pb.CreateTodoRequest{ Title: "Internal Todo", Description: "Created via gRPC", }) if err != nil { log.Fatalf("could not create todo: %v", err) } log.Printf("Created Todo (gRPC): %v", res) }
-
외부 웹 클라이언트: 웹 브라우저, 모바일 앱 또는 기타 외부 시스템은
localhost:8081
에서 표준 HTTP/JSON 요청을 사용하여 서비스와 상호 작용할 수 있습니다.# HTTP/JSON에 대한 curl 사용 예제 curl -X POST -H "Content-Type: application/json" \ -d '{"title": "External Todo", "description": "Created via HTTP"}' \ http://localhost:8081/v1/todos # 출력: # {"id":"id-generated-123","title":"External Todo","description":"Created via HTTP","completed":false} curl http://localhost:8081/v1/todos/id-generated-123 # 출력: # {"id":"id-generated-123","title":"My First Todo","description":"This is a detailed description.","completed":false} curl http://localhost:8081/v1/todos # 출력: # {"todos":[{"id":"id-generated-123","title":"My First Todo","description":"Description 1","completed":false},{"id":"id-456","title":"Buy groceries","description":"Milk, eggs, bread","completed":true}]}
결론
Go, Protocol Buffers 및 gRPC-Gateway를 활용하면 내부 고성능 gRPC 서비스와 보편적으로 접근 가능한 RESTful HTTP/JSON 엔드포인트 모두에 대한 단일의 강력한 API 정의(.proto
파일)를 설정할 수 있습니다. 이 접근 방식은 중복을 크게 줄이고, 다양한 소비자 유형에 걸쳐 일관성을 개선하며, API 개발 및 유지 관리를 간소화합니다. 이 강력한 조합을 통해 내부 통신에는 매우 효율적이고 외부 클라이언트에는 쉽게 친근한 API를 구축하여 양쪽 세계의 최고를 진정으로 달성할 수 있습니다.