gRPC는 모든 상황에서 최적의 솔루션은 아닙니다. 특정 경우엔 전통적인 HTTP/JSON API를 제공해야 할 수 있습니다. 이는 호환성 유지, 특정 언어 지원 부족 또는 gRPC가 효과적으로 처리하지 못하는 클라이언트 지원 등 다양한 이유에서 발생할 수 있습니다. 하지만 HTTP/JSON API를 별도로 구현하는 일은 시간과 노력을 많이 요구합니다.
이러한 문제를 해결하기 위한 방법은 존재합니다. 단일 코드 작성으로 gRPC와 HTTP/JSON 두 형식의 API를 동시에 제공할 수 있는 도구가 있습니다.
그 답은 "예"입니다.
gRPC-Gateway는 Google Protocol Buffers 컴파일러인 protoc의 플러그인입니다. 이 도구는 protobuf 서비스 정의를 읽고 RESTful HTTP API를 gRPC로 변환하는 역할을 수행하는 반복 프록시 서버를 생성합니다. 이 서버는 서비스 정의 내의 google.api.http 어노테이션에 기반하여 생성됩니다.
이를 통해 gRPC와 HTTP/JSON 형식의 API를 동시에 제공할 수 있습니다.
사전 준비
코드 작성 전 필요한 도구를 설치해야 합니다.
이 예제에서는 Go 기반 gRPC 서버를 사용하므로 먼저 https://golang.org/dl/ 에서 Go를 설치합니다.
Go 설치 후 다음 패키지를 go get 명령어로 다운로드합니다:
$ go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
$ go get google.golang.org/protobuf/cmd/protoc-gen-go
$ go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
이를 통해 스탬프 생성에 필요한 프로토콜 컴파일러 플러그인을 설치합니다. $GOPATH/bin을 $PATH에 추가해 설치된 실행 파일을 사용할 수 있도록 설정합니다.
새로운 모듈에서 작업할 예정이므로 원하는 폴더에 해당 모듈을 생성합니다:
go.mod 파일 생성
go mod init 명령어로 모듈을 시작하여 go.mod 파일을 생성합니다.
다음 명령어를 실행해 모듈 경로를 설정합니다. 여기서는 github.com/myuser/myrepo를 모듈 경로로 사용합니다(생산 환경에서는 이 경로가 패키지를 다운로드할 수 있는 URL이 됩니다).
$ go mod init github.com/myuser/myrepo
go: creating new go.mod: module github.com/myuser/myrepo
go mod init 명령어는 코드가 다른 코드에서 사용 가능한 모듈임을 나타내는 go.mod 파일을 생성합니다. 생성된 파일은 모듈 이름과 지원하는 Go 버전만 포함합니다. 의존 항목을 추가하면 go.mod 파일에 특정 모듈 버전이 표시되며, 이는 재현 가능한 빌드와 버전 관리에 도움을 줍니다.
간단한 hello world gRPC 서비스 생성
gRPC-Gateway를 이해하기 위해 먼저 hello world gRPC 서비스를 제작해야 합니다.
Protocol Buffers로 gRPC 서비스 정의
gRPC 서비스를 생성하기 전에 proto 파일을 작성하여 필요한 내용을 정의합니다. 여기서는 proto/helloworld/ 디렉터리 아래에 hello_world.proto 파일을 생성합니다.
gRPC 서비스는 Google Protocol Buffers로 정의됩니다. 다음과 같이 정의합니다:
syntax = "proto3";
package helloworld;
// 인사 서비스 정의
service Greeter {
// 인사를 보냅니다
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 사용자의 이름을 포함하는 요청 메시지
message HelloRequest {
string name = 1;
}
// 인사말을 포함하는 응답 메시지
message HelloReply {
string message = 1;
}
Buf를 사용한 스탬프 생성
Buf는 linting, 변경사항 감지 및 생성 등의 다양한 protobuf 유틸리티를 제공합니다. 설치 지침은 https://docs.buf.build/installation/ 에서 확인할 수 있습니다.
buf.yaml 파일로 구성되며, 저장소 루트 디렉터리에 체크인해야 합니다. buf는 이 파일이 존재할 경우 자동으로 이를 읽습니다. 또한 --config 명령행 플래그를 사용해 .json 또는 .yaml 파일 경로를 지정하거나 직접 JSON/YAML 데이터를 제공할 수도 있습니다.
모든 로컬 .proto 파일을 입력으로 사용하는 Buf 작업은 유효한 빌드 구성에 의존합니다. 이 구성은 .proto 파일을 어디서 찾고 어떻게 처리할지를 설명합니다. protoc와 달리 buf는 구성 하위의 모든 .proto 파일을 재귀적으로 발견하고 빌드합니다.
다음은 .proto 파일 루트가 저장소 루트 대비 proto 폴더에 있다고 가정한 유효한 구성 예시입니다.
version: v1beta1
name: buf.build/myuser/myrepo
build:
roots:
- proto
Go로 타입 및 gRPC 스탬프 생성하려면 저장소 루트에서 buf.gen.yaml 파일을 생성합니다:
version: v1beta1
plugins:
- name: go
out: proto
opt: paths=source_relative
- name: go-grpc
out: proto
opt: paths=source_relative
우리는 go 및 go-grpc 플러그인을 사용하여 Go 타입 및 gRPC 서비스 정의를 생성합니다. 우리는 proto 폴더 대비 생성 파일을 출력하며, path=source_relative 옵션은 생성된 파일이 소스 .proto 파일과 동일한 디렉터리에 표시됨을 의미합니다.
그러면 다음 명령어를 실행합니다:
$ buf generate
이 명령어는 우리의 proto 파일 계층 구조 내 모든 protobuf 패키지에 대해 *.pb.go 및 *_grpc.pb.go 파일을 생성합니다.
protoc를 사용한 스탬프 생성
이것은 protoc 명령어가 Go 스탬프를 생성하는 예시입니다. 저장소 루트에 있으며, proto 파일이 proto라는 디렉터리에 있다고 가정합니다:
$ protoc -I ./proto \
--go_out ./proto --go_opt paths=source_relative \
--go-grpc_out ./proto --go-grpc_opt paths=source_relative \
./proto/helloworld/hello_world.proto
우리는 go 및 go-grpc 플러그인을 사용하여 Go 타입 및 gRPC 서비스 정의를 생성합니다. 우리는 proto 폴더 대비 생성 파일을 출력하며, path=source_relative 옵션은 생성된 파일이 소스 .proto 파일과 동일한 디렉터리에 표시됨을 의미합니다.
이 명령어는 proto/helloworld/hello_world.proto에 대해 *.pb.go 및 *_grpc.pb.go 파일을 생성합니다.
main.go 생성
main.go 파일을 생성하기 전에 사용자가 github.com/myuser/myrepo 모듈을 생성했음을 가정합니다. 여기서의 import는 저장소 루트 대비 proto/helloworld에 생성된 파일의 경로를 상대 경로로 사용합니다.
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
helloworldpb "github.com/myuser/myrepo/proto/helloworld"
)
type server struct{}
func NewServer() *server {
return &server{}
}
func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {
return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}
func main() {
// TCP 포트에서 리스너 생성
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalln("Failed to listen:", err)
}
// gRPC 서버 객체 생성
s := grpc.NewServer()
// Greeter 서비스를 서버에 첨부
helloworldpb.RegisterGreeterServer(s, &server{})
// gRPC 서버 실행
log.Println("Serving gRPC on 0.0.0.0:8080")
log.Fatal(s.Serve(lis))
}
기존 proto 파일에 gRPC-Gateway 어노테이션 추가
이제 Go gRPC 서버를 사용할 수 있으므로 gRPC-Gateway 어노테이션을 추가해야 합니다.
어노테이션은 gRPC 서비스가 JSON 요청 및 응답에 어떻게 매핑되는지를 정의합니다. protocol buffers를 사용할 때 각 RPC는 google.api.http 어노테이션을 사용해 HTTP 메서드 및 경로를 정의해야 합니다.
따라서 우리는 google/api/http.proto를 import하고 HTTP->gRPC 매핑을 추가해야 합니다. 이 경우 POST /v1/example/echo를 SayHello RPC에 매핑합니다.
syntax = "proto3";
package helloworld;
import "google/api/annotations.proto";
// 전체 인사 서비스 정의에서 모든 엔드포인트를 정의합니다
service Greeter {
// 인사를 보냅니다
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
post: "/v1/example/echo"
body: "*"
};
}
}
// 사용자의 이름을 포함하는 요청 메시지
message HelloRequest {
string name = 1;
}
// 인사말을 포함하는 응답 메시지
message HelloReply {
string message = 1;
}
gRPC-Gateway 스탬프 생성
이제 proto 파일에 gRPC-Gateway 어노테이션을 추가했으므로 gRPC-Gateway 생성기를 사용해 스탬프를 생성해야 합니다.
Buf 사용
우리는 gRPC-Gateway 생성기를 생성 구성에 추가해야 합니다:
version: v1beta1
plugins:
- name: go
out: proto
opt: paths=source_relative
- name: go-grpc
out: proto
opt: paths=source_relative,require_unimplemented_servers=false
- name: grpc-gateway
out: proto
opt: paths=source_relative
우리는 buf.yaml 파일에 googleapis 의존성을 추가해야 합니다:
version: v1beta1
name: buf.build/myuser/myrepo
deps:
- buf.build/beta/googleapis
build:
roots:
- proto
그러면 buf beta mod update 명령어로 사용할 의존 항목 버전을 선택해야 합니다.
완료! 이제 buf generate 명령어를 실행하면 *.gw.pb.go 파일이 생성됩니다.
protoc 사용
protoc를 사용해 스탬프를 생성하기 전에 일부 의존 항목을 로컬 파일 구조에 복사해야 합니다. googleapis의 일부를 공식 저장소에서 복사해 로컬 파일 구조에 추가해야 합니다. 이후 구조는 다음과 같아야 합니다:
proto
├── google
│ └── api
│ ├── annotations.proto
│ └── http.proto
└── helloworld
└── hello_world.proto
이제 protoc 호출에 gRPC-Gateway 생성기를 추가해야 합니다:
$ protoc -I ./proto \
--go_out ./proto --go_opt paths=source_relative \
--go-grpc_out ./proto --go-grpc_opt paths=source_relative \
--grpc-gateway_out ./proto --grpc-gateway_opt paths=source_relative \
./proto/helloworld/hello_world.proto
이 명령어는 *.gw.pb.go 파일을 생성합니다.
main.go 파일에 gRPC-Gateway 멀티플렉서(mux)를 추가하고 서비스를 제공해야 합니다.
package main
import (
"context"
"log"
"net"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
helloworldpb "github.com/myuser/myrepo/proto/helloworld"
)
type server struct{
helloworldpb.UnimplementedGreeterServer
}
func NewServer() *server {
return &server{}
}
func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {
return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}
func main() {
// TCP 포트에서 리스너 생성
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalln("Failed to listen:", err)
}
// gRPC 서버 객체 생성
s := grpc.NewServer()
// Greeter 서비스를 서버에 첨부
helloworldpb.RegisterGreeterServer(s, &server{})
// gRPC 서버 실행
log.Println("Serving gRPC on 0.0.0.0:8080")
go func() {
log.Fatalln(s.Serve(lis))
}()
// gRPC 서버에 연결하는 클라이언트 연결 생성
// 이는 gRPC-Gateway가 요청을 중계하는 곳입니다
conn, err := grpc.DialContext(
context.Background(),
"0.0.0.0:8080",
grpc.WithBlock(),
grpc.WithInsecure(),
)
if err != nil {
log.Fatalln("Failed to dial server:", err)
}
gwmux := runtime.NewServeMux()
// Greeter 등록
err = helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn)
if err != nil {
log.Fatalln("Failed to register gateway:", err)
}
gwServer := &http.Server{
Addr: ":8090",
Handler: gwmux,
}
log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
log.Fatalln(gwServer.ListenAndServe())
}
gRPC-Gateway 테스트
이제 서버를 실행할 수 있습니다:
$ go run main.go
그런 다음 cURL을 사용해 HTTP 요청을 보냅니다:
$ curl -X POST -k http://localhost:8090/v1/example/echo -d '{"name": " hello"}'
{"message":"hello world"}
Refs
- https://github.com/iamrajiv/helloworld-grpc-gateway
- https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/introduction/