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 사용 (메세지와 프레임이 바이너리 형식으로 인코딩 됨)
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 를 위한 라이브러리를 설치하자
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에서 api
와 rpc
폴더를 붙여넣기 하였다.
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 동작을 확인해보았다.
AWS 서버리스 단축URL 서비스 만들기 - 1 (0) | 2021.07.24 |
---|---|
Go gRPC 튜토리얼 #2 - Server Streaming RPC (0) | 2021.01.14 |
Go gRPC 서버에 REST API 요청 주고 받기 [grpc-gateway] (0) | 2020.12.31 |
JPA User 테이블, Postgres reserved keyword 해결하기 (0) | 2020.08.12 |
JPA, Postgres earthdistance를 이용하여 사용자 근처 가맹점 조회 API 구현하기 (0) | 2020.07.24 |
WSL환경에서 Docker-compose 사용하기 (0) | 2020.04.26 |
댓글 영역