본문 바로가기
Back-end

Go gRPC 서버에 REST API 요청 주고 받기 [grpc-gateway]

by 노아론 2020. 12. 31.

rpdly 프로젝트 를 진행하면서 단축 URL를 생성하는 부분을 Go와 grpc, redis를 이용해 구현하고 있다. (프로젝트 명은 rpdly-go-uri 라고 정함)

 

회원가입 등의 이외 비즈니스 로직들은 Java와 Spring boot를 사용하고 있는데 (rpdly-api), 이 곳에서 rpdly-go-uri 서비스와 연결할때 grpc를 이외에도 REST로 http 요청을 하면 어떨지 떠올랐다.

 

이 과정에서 grpc-gateway 플러그인과 twirp 프레임워크를 알게 되었다.

언뜻 보기엔 내가 원하는 기능을 둘다 지원해주는 것 같지만, 이 둘의 차이는 아래처럼 나눠볼 수 있겠다.

 

  • grpc-gateway

    protobuf를 REST HTTP API를 gRPC로 변환시켜주는 리버스 프록시 서버를 생성

    HTTP2 사용 (메세지와 프레임이 바이너리 형식으로 인코딩 됨)

    • 성능이나 속도면에서 더 우수하겠지만 궁극적으로 gRPC에서 Stream 사용이 가능해짐
  • twirp

    서버 인터페이스, 클라이언트 자동 생성

    endpoint path를 알아서 정해줌, serialization, deserialization도 지원함

    protobuf를 사용하고 gRPC 와 닮았지만 사실 net/http 기반임

    HTTP2 대신 HTTP 1.1로 돌아감

이렇게 보니 나에겐 grpc-gateway가 적합하였고 이를 사용해보기로 하였다

 

먼저 grpc-gateway의 역할과 사용을 알아보자면,

grpc-gateway는 API 클라이언트를 통해 REST API 값을 받아 이를 gRPC에서 사용가능한 형태로 넘겨주는 과정을 맡는다.

그렇기에 proto buffer 파일은 grpc-gateway에 해당하는 grpc코드와 일반적인 (순수. 플러그인 없는) grpc코드로 총 두개를 생성해야 한다.

grpc-gateway 구조

 

먼저 grpc-gateway 를 위한 라이브러리를 설치하자

go install \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
    google.golang.org/protobuf/cmd/protoc-gen-go \
    google.golang.org/grpc/cmd/protoc-gen-go-grpc

 

그리고 protoc (v3.0.0 이상)이 있으면 된다

문서에 적혀있는대로 uri_exchange.proto에 아래처럼 추가하였다.

REST API로 사용할 수 있게 API 엔드포인트 별 경로를 적으면 된다.

syntax = "proto3";

+ import "google/api/annotations.proto";

package v1;
option go_package = "protobuf/uri/v1";


service UriExchange {
  rpc GetUri(Request) returns (Response) {
+    option (google.api.http) = {
+      get: "/v1/uri/{uri}"
+    };
  }

  rpc SetUri(Request) returns (Response) {
+    option (google.api.http) = {
+      post: "/v1/uri",
+      body: "*"
+    };
  }
}

message Request {
  string uri = 1;
}

message Response {
  string uri = 1;
}

 

third_party/googleapis 폴더를 레포 아래에 추가해야 한다고 하여 google apis에서 apirpc폴더를 붙여넣기 하였다.

api, rpc 폴더에 대해 붙여넣기를 하지 않고 이후에 protoc으로 파일 생성을 하게되면 아래와 같은 문제가 일어난다

protobuf/uri/v1/uri_exchange.proto:3:1: Import "annotations.proto" was not found or had errors.

 

추가하였으니 프로젝트는 아래와 같은 구조가 되었다.

./google./protobuf/uri/v1 으로 관리하고 있다.

(프로젝트 루트에 ./google 이 있는게 상당히 거슬렸지만 일단 보류하였다)

rpdly-go-uri
├── ...생략...
├── google
│   ├── api
│   │   ├── annotations.proto
│   │   ├── field_behavior.proto
│   │   ├── http.proto
│   │   └── httpbody.proto
│   └── rpc
│       ├── code.proto
│       ├── error_details.proto
│       └── status.proto
├── handler
│   └── uri_gen.go
├── main.go
├── protobuf
│   └── uri
│       └── v1
│           └── uri_exchange.proto
├── ...생략...

 

이제 uri_exchange.pb.gw.go 를 생성해야 한다.

이는 grpc-gateway 서버를 위해 필요하다.

uri_exchange.pb.gw.go는 아래 옵션을 담아서 생성하였다.

protoc -I . -I /usr/local/include --grpc-gateway_out . --grpc-gateway_opt logtostderr=true --grpc-gateway_opt paths=source_relative --grpc-gateway_opt generate_unbound_methods=true ./protobuf/uri/v1/uri_exchange.proto

 

grpc서버를 위한 코드인 uri_exchange.pb.go는 이와 같은 옵션을 담아 생성하면 된다.

protoc -I . -I /usr/local/include --go_out=plugins=grpc:. ./protobuf/uri/v1/uri_exchange.proto

 

-I /usr/local/include 를 포함하지 않으면 이렇게 오류가 발생한다. (물론 환경에 따라 다를 수 있다)

google/protobuf/descriptor.proto: File not found.
google/api/annotations.proto:20:1: Import "google/protobuf/descriptor.proto" was not found or had errors.
google/api/annotations.proto:28:8: "google.protobuf.MethodOptions" is not defined.
protobuf/uri/v1/uri_exchange.proto:3:1: Import "google/api/annotations.proto" was not found or had errors.

 

아래와 같이 protoc으로 생성된 파일을 확인할 수 있게 된다.

rpdly-go-uri
├── protobuf
│   └── uri
│       └── v1
│           ├── uri_exchange.pb.go
│           ├── uri_exchange.pb.gw.go
│           └── uri_exchange.proto
├── ...생략...

 

proxy를 위한 grpc 코드까지 생성되었으니 프록시 서버 코드를 작성하자

도큐먼트에 나온 HTTP reverse-proxy server 코드를 참고했다

./gateway/gateway.go

package main

import (
    "context"
    "flag"
    "log"
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "github.com/roharon/rpdly-go-url/config"
    pb "github.com/roharon/rpdly-go-url/protobuf/uri/v1"
    "google.golang.org/grpc"
)

func Run() error {
    conf := config.GetConfig()
    // 내가 만들어 둔 환경변수 라이브러리
    // conf.SERVER_ADDRESS 대신에 하드코딩을 해도 무방하다
    // 예) localhost:3000

    grpcServerEndpoint := flag.String("grpc_endpoint", conf.SERVER_ADDRESS, "gRPC server endpoint")
    // gRPC 서버 엔드포인트를 적는다.
    // flag를 사용했지만 grpcServerEndpoint에 string으로 적어도 된다

    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}
    err := pb.RegisterUriExchangeHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)

    if err != nil {
        return err
    }

    return http.ListenAndServe(conf.PROXY_PORT, mux)
}

func main() {
    flag.Parse()

    if err := Run(); err != nil {
        log.Fatal(err)
    }
}

 

main.go 가 아닌 ./gateway/gateway.go 에서 새로 만든 이유는 위의 그림에서 보았듯이

gRPC server와 reverse-proxy server를 따로 실행해야 하기 때문이다.

 

지금까지 grpc reverse-proxy에 대한 작업을 마쳤고, 확인을 해보려 한다.

 

좌측엔 지금까지 작업한 reverse proxy 서버인 gateway.go를 실행하며

우측엔 기존 gRPC 서버(main.go)를 실행하였다.

 

지금까지 grpc-gateway를 이용하여 gRPC의 서버의 HTTP REST-API call 동작을 확인해보았다.

댓글