gRPC-Gateway를 사용하여 gRPC와 REST를 자동으로 연결
Ethan Miller
Product Engineer · Leapcell

현대의 마이크로서비스 아키텍처에서 gRPC와 REST 사이의 선택은 종종 딜레마를 제시합니다. 높은 성능, 강력한 타이핑, 효율적인 바이너리 직렬화(Protocol Buffers)를 갖춘 gRPC는 분산 시스템 내에서 서비스 간 통신의 사실상의 표준이 되었습니다. 그러나 웹 브라우저, 모바일 애플리케이션 또는 타사 통합업체와 같은 외부 클라이언트에 서비스를 노출하는 것과 관련해서는 RESTful API가 단순성, 광범위한 채택 및 뛰어난 도구 지원으로 인해 여전히 어디에나 존재합니다. 동일한 기본 서비스에 대해 두 개의 별도 API(하나는 gRPC, 하나는 REST)를 수동으로 개발하고 유지 관리하는 것은 복잡성, 중복 및 개발 오버헤드의 상당한 원천이 될 수 있습니다. 이것이 바로 gRPC-Gateway가 빛나는 부분입니다. gRPC 서비스를 관용적인 RESTful API로 자동 노출하는 우아한 솔루션을 제공하여 이중 구현 부담 없이 두 패러다임의 강점을 활용할 수 있습니다. 이 글은 gRPC-Gateway가 이러한 원활한 통합을 어떻게 달성하는지 자세히 살펴보고 개발자가 효율적이고 유지 관리 가능하며 접근성이 뛰어난 서비스를 구축할 수 있도록 지원할 것입니다.
<h2>핵심 개념 및 구현</h2>gRPC-Gateway의 구체적인 내용으로 들어가기 전에 작동의 기반이 되는 몇 가지 핵심 개념을 간략하게 정의해 보겠습니다.
<strong>gRPC:</strong> Google에서 개발한 고성능 오픈 소스 범용 RPC 프레임워크입니다. 인터페이스 정의 언어(IDL) 및 메시지 직렬화에는 Protocol Buffers를 사용하고 전송에는 HTTP/2를 사용합니다. 이를 통해 스트리밍, 양방향 스트리밍 및 강력한 유형 검사와 같은 기능으로 효율적인 통신이 가능합니다.
<strong>Protocol Buffers (Protobuf):</strong> 언어에 구애받지 않고 플랫폼에 구애받지 않는 확장 가능한 구조화된 데이터를 직렬화하는 메커니즘입니다. 데이터 구조를 한 번 정의한 후 생성된 소스 코드를 사용하여 구조화된 데이터를 다양한 데이터 스트림으로 쉽게 읽고 쓸 수 있습니다.
<strong>RESTful API:</strong> 네트워킹 애플리케이션을 설계하기 위한 아키텍처 스타일입니다. 상태 비저장 클라이언트-서버 통신 모델에 의존하며, 표준 HTTP 메서드(GET, POST, PUT, DELETE)를 사용하고 일반적으로 JSON 또는 XML 형식으로 리소스 표현을 반환합니다.
<strong>gRPC-Gateway:</strong> RESTful HTTP/JSON 요청을 gRPC 요청으로 변환하는 리버스 프록시입니다. gRPC 서비스 정의와 특별 주석(HTTP 규칙)을 읽고 해당 gRPC 서버로 들어오는 REST 요청을 전달하는 리버스 프록시 서버를 생성합니다. 여기서 핵심은 "자동 생성"이며, 수동 코딩을 크게 줄입니다.
<h3>gRPC-Gateway 작동 방식</h3>gRPC-Gateway의 마법은 Protobuf 서비스 정의를 분석하고 브리지 역할을 하는 Go 코드를 생성하는 능력에 있습니다. 이 프로세스에는 일반적으로 다음과 같은 단계가 포함됩니다.
-
<strong>HTTP 주석으로 GRPC 서비스 정의:</strong> Protocol Buffers를 사용하여 서비스 및 메시지 구조를 정의하는 것으로 시작합니다. RESTful 엔드포인트로 노출하려는 메서드의 경우 Protobuf 정의 내에 특별한
google.api.http
옵션을 추가합니다. 이러한 주석은 HTTP 메서드(GET, POST 등), URL 경로 및 요청 매개변수가 Protobuf 필드에 매핑되는 방식을 지정합니다. -
<strong>gRPC 및 gRPC-Gateway 코드 생성:</strong> Protocol Buffer 컴파일러인
protoc
과 특정 플러그인(gRPC의protoc-gen-go
및 게이트웨이의protoc-gen-grpc-gateway
)을 사용하여 필요한 Go 코드를 생성합니다.protoc-gen-grpc-gateway
플러그인은 각 서비스에 대해 HTTP 핸들러 논리를 포함하는*.pb.gw.go
파일을 생성합니다. -
<strong>gRPC 서버 실행:</strong> gRPC 서비스는
.proto
파일에 정의된 논리를 구현하고 일반적으로 특정 포트에서 gRPC 요청을 수신 대기합니다. -
<strong>gRPC-Gateway 프록시 실행:</strong> 생성된 gRPC-Gateway 핸들러를 인스턴스화하는 별도의 Go 애플리케이션을 만듭니다. 이 게이트웨이 애플리케이션은 HTTP 서버 역할을 하며 들어오는 RESTful HTTP/JSON 요청을 수신 대기합니다. 그러면 이러한 요청을 gRPC 호출로 변환하고 gRPC 서버로 전달합니다. gRPC 서버의 응답은 다시 JSON으로 변환되어 클라이언트로 전송됩니다.
간단한 "Greeter" 서비스를 사용하여 이를 설명해 보겠습니다.
<strong>1. greeter.proto
:</strong>
syntax = "proto3"; package greeter; import "google/api/annotations.proto"; option go_package = "./greeter"; service Greeter { rpc SayHello (HelloRequest) returns (HelloResponse) { option (google.api.http) = { get: "/v1/hello/{name}" }; } rpc SayHelloPost (HelloRequest) returns (HelloResponse) { option (google.api.http) = { post: "/v1/hello_post" body: "*" }; } } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; }
option (google.api.http)
줄에 주목하세요. 이러한 주석은 gRPC-Gateway에 중요합니다.
GET: "/v1/hello/{name}"
는John
을HelloRequest
의name
필드로 추출하여/v1/hello/John
에 대한 GET 요청을SayHello
메서드에 매핑합니다.body: "*"
가 있는POST: "/v1/hello_post"
는{"name": "Alice"}
와 같은 JSON 본문이 있는/v1/hello_post
에 대한 POST 요청을SayHelloPost
메서드에 매핑합니다.
<strong>2. Go 코드 생성:</strong>
protoc
컴파일러와 protoc-gen-go
및 protoc-gen-grpc-gateway
플러그인을 설치해야 합니다.
# protoc-gen-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 서비스 생성을 위해 # protoc-gen-grpc-gateway 설치 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 # 선택 사항, OpenAPI 사양 생성을 위해 # 주석에 대한 protobuf 정의를 사용할 수 있도록 합니다. # googleapis/google/api를 복제하거나 복사하여 proto 경로에 넣어야 할 수 있습니다. # 예를 들어, proto 루트에 'google' 폴더를 만들고 'api'를 안에 넣습니다. # 또는 'buf'와 같은 도구를 사용하여 종속성을 관리합니다. protoc -I. \ -I/usr/local/include \ -I$(go env GOPATH)/src \ -I$(go env GOPATH)/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ --grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative \ greeter.proto
이 명령은 greeter.pb.go
(Protobuf 메시지), greeter_grpc.pb.go
(gRPC 서비스 인터페이스 및 클라이언트), greeter.pb.gw.go
(gRPC-Gateway 핸들러)를 생성합니다.
<strong>3. gRPC 서버 구현 (main.go
서버용):</strong>
package main import ( "context" "fmt" "log" "net" "google.golang.org/grpc" pb "your_module_path/greeter" // your_module_path를 실제 Go 모듈 경로로 바꾸세요 ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloResponse{Message: "Hello " + in.GetName()}, } func (s *server) SayHelloPost(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { log.Printf("Received via POST: %v", in.GetName()) return &pb.HelloResponse{Message: "Hello from POST, " + in.GetName()}, } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) log.Printf("gRPC server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
<strong>4. gRPC-Gateway 프록시 실행 (main.go
게이트웨이용, 동일하거나 별도의 프로젝트일 수 있음):</strong>
package main import ( "context" "log" "net/http" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "your_module_path/greeter" // your_module_path 바꾸기 ) func main() { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} // 프로덕션 TLS 용으로 grpc.WithTransportCredentials와 함께 사용 // Greeter 서비스 등록 err := pb.RegisterGreeterHandlerFromEndpoint(ctx, mux, ":50051", opts) if err != nil { log.Fatalf("Failed to register gateway: %v", err) } log.Printf("gRPC-Gateway server listening on :8080") if err := http.ListenAndServe(":8080", mux); err != nil { log.Fatalf("Failed to serve gateway: %v", err) } }
이제 gRPC 서버와 게이트웨이 서버를 실행하십시오. 그러면 RESTful 요청을 할 수 있습니다.
<h3>애플리케이션 시나리오</h3># GET 요청 curl http://localhost:8080/v1/hello/World # 예상 출력: {"message":"Hello World"} # POST 요청 curl -X POST -H "Content-Type: application/json" -d '{"name": "Alice"}' http://localhost:8080/v1/hello_post # 예상 출력: {"message":"Hello from POST, Alice"}
gRPC-Gateway는 특히 다양한 시나리오에서 유익합니다.
<ul> <li><strong>하이브리드 환경:</strong> REST를 선호하거나 요구하는 외부 클라이언트(예: 프런트엔드 웹 애플리케이션, 모바일 앱, 타사 API)에 내부 gRPC 서비스를 노출해야 할 때입니다.</li> <li><strong>점진적 마이그레이션:</strong> 기존 RESTful API를 gRPC로 마이그레이션하는 경우 gRPC-Gateway를 사용하면 기존 외부 클라이언트와 호환성을 유지하면서 내부적으로 gRPC를 도입할 수 있습니다.</li> <li><strong>간소화된 클라이언트 개발:</strong> 네이티브 gRPC 클라이언트 라이브러리를 즉시 사용할 수 없거나 통합하기 복잡한 환경(예: 클라이언트 측 JavaScript, 테스트를 위한 간단한 curl 요청).</li> <li><strong>API 프로토타이핑 및 문서화:</strong> `protoc-gen-openapiv2`를 사용하면 gRPC-Gateway는 Protobuf 정의 및 HTTP 주석에서 직접 OpenAPI(Swagger) 사양을 생성하여 API 문서를 간소화할 수도 있습니다.</li> </ul> <h2>결론</h2>gRPC-Gateway는 내부 통신을 위한 gRPC의 성능 이점과 외부 클라이언트를 위한 REST의 광범위한 접근성을 어떻게 조화시킬 것인가라는 일반적인 아키텍처 문제에 대한 강력하고 우아한 솔루션을 제공합니다. Protocol Buffer 주석과 코드 생성을 활용하여 이중 API 수동 구현의 필요성을 완전히 제거하여 상당한 개발 노력을 절약하고 불일치 가능성을 줄입니다. 이 접근 방식은 개발자 생산성을 향상시킬 뿐만 아니라 시스템 전체에서 일관된 서비스 정의를 촉진하여 gRPC와 REST의 이점을 진정으로 극대화할 수 있도록 지원합니다.