이번에는 go 에서 gRPC-Gateway 를 사용하는 방법에 대해 알아보려 한다.

최종 코드는 다음에서 확인 가능 : https://github.com/jeremyko/grpc-gateway-sample

앞서 살펴본 go 에서 proto buffer 사용하기 와 거의 비슷한 절차이나 gRPC-Gateway 사용을 위해 추가되는 절차가 있다.
다음 내용을 기초로 작성되었다 (그대로 따라 했더니 에러가 발생되어, 최신 go 버전에 맞게 내용이 추가된 부분이 있다. go 1.16 버전 기준).

https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/introduction/

필요한 패키지 다운로드

go get google.golang.org/grpc
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

테스트 grpc 모듈 생성

임의의 위치에 my_grpc_module 디렉토리 생성. 테스트 편의를 위해 main package 를 포함한 모듈을 생성한다.
모듈명은 github.com/jeremyko/my_grpc_module 으로 한다.

root: ~/mydev# mkdir my_grpc_module
root: ~/mydev# cd my_grpc_module

go mod init 수행한다

root: ~/mydev/my_grpc_module# go mod init github.com/jeremyko/my_grpc_module

생성된 go.mod 파일 내용 확인

root: ~/mydev/my_grpc_module# cat go.mod
module github.com/jeremyko/my_grpc_module
go 1.16

간단한 테스트용 hello world 서비스를 작성한다.

protocol buffer를 사용하여 gRPC service 를 정의해야 한다. 현위치에서 proto/helloworld/hello_world.proto 파일을 생성한다.

mkdir -p proto/helloworld/
cd proto/helloworld/
touch hello_world.proto

hello_world.proto 파일을 다음처럼 작성한다

syntax = "proto3";
package helloworld;

// The greeting service definition
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name
message HelloRequest {
    string name = 1;
}
// The response message containing the greetings
message HelloReply {
    string message = 1;
}

protoc 혹은 buf 를 사용해서 컴파일 한다.

cd ~/mydev/my_grpc_module

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_package 를 정의해야 함

protoc-gen-go: unable to determine Go import path for "helloworld/hello_world.proto"
Please specify either:
• a "go_package" option in the .proto source file, or
• a "M" argument on the command line.
See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.
--go_out: protoc-gen-go: Plugin failed with status code 1.

다시 hello_world.proto 파일을 수정한다(package 경로를 모듈명에 덧붙인 형식으로)

syntax = "proto3";
package helloworld;
option go_package = "github.com/jeremyko/my_grpc_module/proto/helloworld" ;

// The greeting service definition
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name
message HelloRequest {
    string name = 1;
}
// The response message containing the greetings
message HelloReply {
    string message = 1;
}

다시 컴파일 하면 정상적으로 파일들이 생성된다.

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

root: ~/mydev/my_grpc_module/proto/helloworld# ll
total 16
-rw-r--r-- 1 root root 3445 Apr 23 16:51 hello_world_grpc.pb.go
-rw-r--r-- 1 root root 7260 Apr 23 16:51 hello_world.pb.go
-rw-r--r-- 1 root root 432 Apr 23 16:51 hello_world.proto

gRPC server 코드를 작성한다.

/root/mydev/my_grpc_module 에 main.go 를 생성한다

package main

import (
    "context"
    "log"
    "net"
    "google.golang.org/grpc"
    helloworldpb "github.com/jeremyko/my_grpc_module/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() {
    // Create a listener on TCP port
    lis, err := net.Listen("tcp", ":8080")
    if err != nil {
    log.Fatalln("Failed to listen:", err)
    }

    // Create a gRPC server object
    s := grpc.NewServer()
    // Attach the Greeter service to the server
    helloworldpb.RegisterGreeterServer(s, &server{})
    // Serve gRPC Server
    log.Println("Serving gRPC on 0.0.0.0:8080")
    log.Fatal(s.Serve(lis))
}

gRPC-Gateway 기능 추가

자..여기까지가 Go gRPC server 를 작업한것이고 ;-), 이제 추가로 gRPC-Gateway 를 위한 처리가 필요하다.

다시 proto 파일에 다음처럼 변경을 해줘야 한다.

import “google/api/annotations.proto”; 를 추가한다

HTTP->gRPC mapping 을 추가한다

syntax = "proto3";
package helloworld;
import "google/api/annotations.proto";
option go_package = "github.com/jeremyko/my_grpc_module/proto/helloworld" ;

// The greeting service definition
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {
        option (google.api.http) = {
            post: "/v1/example/echo"
            body: "*"
        };
    }
}
// The request message containing the user's name
message HelloRequest {
    string name = 1;
}
// The response message containing the greetings
message HelloReply {
    string message = 1;
}

이제 stub 코드를 생성한다.

protoc 혹은 buf 를 사용한다. protoc 를 사용하는 경우에는 googleapis 의존 파일을 다음 폴더 구조를 만들어서 복사해야 한다.

proto
├── google
│ └── api
│   ├── annotations.proto
│   └── http.proto
└── helloworld
  └── hello_world.proto

my_grpc_module/proto/ 에 google/api 폴더 생성 mkdir -p google/api

사용할 googleapis 파일을 가져와야 한다. 임의의 위치에서 git clone 을 수행한다.

git clone https://github.com/googleapis/googleapis.git

그리고 파일을 복사한다.

cd googleapis/google/api
cp annotations.proto /root/mydev/my_grpc_module/proto/google/api/.
cp http.proto /root/mydev/my_grpc_module/proto/google/api/.

gRPC-Gateway generator 추가를 위해 protoc 를 실행한다.

cd ~/mydev/my_grpc_module/

root: ~/mydev/my_grpc_module# 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

실행하면 hello_world.pb.gw.go 파일이 생성된다.

root: ~/mydev/my_grpc_module/proto/helloworld# ll
total 24
-rw-r--r-- 1 root root 3445 Apr 23 17:20 hello_world_grpc.pb.go
-rw-r--r-- 1 root root 7664 Apr 23 17:20 hello_world.pb.go
-rw-r--r-- 1 root root 6377 Apr 23 17:20 hello_world.pb.gw.go
-rw-r--r-- 1 root root 586 Apr 23 17:10 hello_world.proto

main.go 를 수정한다

package main

import (
    "context"
    "log"
    "net"
    "net/http" //추가
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" //추가
     "google.golang.org/grpc"
     helloworldpb "github.com/jeremyko/my_grpc_module/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() {
    // Create a listener on TCP port
    lis, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatalln("Failed to listen:", err)
    }

    // Create a gRPC server object
    s := grpc.NewServer()
    // Attach the Greeter service to the server
    helloworldpb.RegisterGreeterServer(s, &server{})
    // Serve gRPC Server
    log.Println("Serving gRPC on 0.0.0.0:8080")
    //log.Fatal(s.Serve(lis)) //막고 다음을 추가
    //------------------------------------------ START
    go func() {
        log.Fatalln(s.Serve(lis))
    }()

    // Create a client connection to the gRPC server we just started
    // This is where the gRPC-Gateway proxies the requests
    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()
    // Register 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())
    //------------------------------------------ END
}

go mod tidy 실행

jeremyko: ~/mydev/go_dev/my_grpc_module# go mod tidy
go: finding module for package google.golang.org/grpc/codes
go: finding module for package google.golang.org/grpc/grpclog
go: finding module for package google.golang.org/genproto/googleapis/api/annotations
go: finding module for package github.com/grpc-ecosystem/grpc-gateway/v2/utilities
go: finding module for package github.com/grpc-ecosystem/grpc-gateway/v2/runtime
go: finding module for package google.golang.org/grpc/metadata
go: finding module for package google.golang.org/grpc
... 중략 ...
go: downloading github.com/golang/protobuf v1.5.2
go: downloading golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4
go: downloading golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4

서버 실행

자체에서 main package 로 테스트 하는 경우이므로 이제 바로 서버를 실행할수 있다.

jeremyko: ~/mydev/go_dev/my_grpc_module# go run main.go
2021/04/24 11:28:47 Serving gRPC on 0.0.0.0:8080
2021/04/24 11:28:47 Serving gRPC-Gateway on http://0.0.0.0:8090

서버로 http post 요청을 보내본다

curl -X POST -k http://localhost:8090/v1/example/echo -d '{"name": " hello"}'

jeremyko: ~# curl -X POST -k http://localhost:8090/v1/example/echo -d '{"name": " hello"}'
{"message":" hello world"}jeremyko: ~#

서버 응답이 오는것을 확인할수 있다.

느낀점

해줘야 할게 많고 복잡 ㅠ