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 를 위한 라이브러리를 설치하자
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 동작을 확인해보았다.
'Back-end' 카테고리의 다른 글
AWS 서버리스 단축URL 서비스 만들기 - 1 (0) | 2021.07.24 |
---|---|
Go gRPC 튜토리얼 #2 - Server Streaming RPC (0) | 2021.01.14 |
JPA User 테이블, Postgres reserved keyword 해결하기 (0) | 2020.08.12 |
JPA, Postgres earthdistance를 이용하여 사용자 근처 가맹점 조회 API 구현하기 (0) | 2020.07.24 |
WSL환경에서 Docker-compose 사용하기 (0) | 2020.04.26 |
댓글