gRPC-Gateway 시작하기

gRPC는 Protocol Buffer를 이용하여 RESTful HTTP API에 비하여 적은 데이터로 빠른 통신을 할 수 있습니다.

하지만 브라우저-서버 간의 gRPC 통신이 지원되지 않는 단점이 존재하는데요. 이는 gRPC-Gateway를 이용하면 해결할 수 있습니다.

이번 포스팅에서는 gRPC-Gateway를 간략하게 알아보고 golang을 이용하여 실습을 진행해보도록 하겠습니다.

gRPC-Gateway 란?

gRPC-Gateway는 프로토콜 버퍼 컴파일러(protoc)의 플러그인입니다.

gRPC 서비스 정의(proto file) 를 읽고 리버스 프록시 서버를 생성하는데, 이 서버는 RESTful HTTP API를 gRPC로 변환해주는 역할을 하게됩니다.

스크린샷 2021-01-04 오후 7 55 49

위 그림은 서비스 정의를 이용하여 gRPC 클라이언트(Stub)와 리버스 프록시 서버를 생성한 뒤 RESTful HTTP API 요청을 gRPC로 변환하는 것을 잘 나타내고 있습니다.

사전작업

golang 설치

gRPC-Gateway 는 golang 만 지원되므로 다른 언어로 리버스 프록시 서버를 생성할 수 없습니다.

$ brew install golang

# 2020. 12. 31 기준으로 go1.15.6
$ go version

protobuf 설치

gRPC-Gateway 를 사용하기 위해서는 protoc v3.0.0 이상을 설치하여야 합니다.

$ brew install protobuf

# 2020. 12. 31 기준으로 3.14.0
$ protoc --version

환경변수 설정

golang 관련 환경변수를 설정합니다.

$ vi ~/.zshrc

export GOROOT="$(brew --prefix golang)/libexec"
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin
export PATH=$PATH:$GOPATH/bin

$ source ~/.zshrc

GOROOT

  • golang SDK의 위치를 의미합니다. Homebrew를 이용하여 설치하였기에 이에 해당하는 실제 설치 경로를 입력하였습니다.

GOPATH

  • 3rd Party library가 설치되는 위치입니다.

프로젝트 생성

해당 글의 모든 소스코드는 macOS Big Sur + Visual Studio Code 환경에서 작성 및 실행하였습니다.

프로젝트 생성에 완료하면 아래와 같은 프로젝트 구조를 가지게 됩니다.

.
├── go.mod
├── go.sum
├── main.go
└── proto
    ├── google
    │   └── api
    │       ├── annotations.proto
    │       └── http.proto
    └── hello
        ├── hello.pb.go
        ├── hello.pb.gw.go
        ├── hello.proto
        └── hello_grpc.pb.go

extension 설치

golang 개발에 도움을 주는 extension 인 golang.go 을 설치합니다.

스크린샷 2020-12-31 오전 11 02 30

프로젝트 초기화

터미널을 열고 hello 디렉토리를 생성합니다.

$ mkdir hello
$ cd hello

go mod init 명령을 이용하여 모듈의 경로를 지정합니다. orgs 에 조직의 이름을 작성합니다. e.g.) musma

$ go mod init github.com/[orgs]/hello

go.mod 파일이 생성 된 것을 확인할 수 있습니다.

module github.com/[orgs]/hello

go 1.15

패키지 설치

프로토콜 버퍼 컴파일러가 gRPC 서비스 정의를 읽고 컴파일하기 위해서 필요한 종속 패키지들을 설치합니다.

$ go get \
    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

Proto 파일 생성

hello.proto 생성

proto 디렉토리와 하위에 hello 디렉토리를 생성 후 hello.proto 파일을 생성합니다.

$ mkdir -p proto/hello
$ cd proto/hello
$ touch hello.proto

간단하게 HelloRequest 받아 HelloResponse 를 반환하는 SayHello RPC 메서드를 작성합니다.

orgs 에 조직의 이름을 작성합니다. e.g.) musma

syntax = "proto3";

package hello;

option go_package = "github.com/[orgs]/hello/proto/hello";

import "google/api/annotations.proto";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse) {
    option (google.api.http) = {
      post: "/echo"
      body: "*"
    };
  }
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
}

annotations.proto 생성

gRPC 서비스가 JSON 요청 및 응답에 매핑되는 방식을 정의한 annotations.proto 파일을 생성하겠습니다.

우선 proto 디렉토리에 하위에 google/api 디렉토리를 생성합니다.

$ cd ../..
$ mkdir -p proto/google/api
$ cd proto/google/api
$ touch annotations.proto

이 후, 여기 의 소스코드를 복사하여 파일을 생성합니다.

http.proto 생성

서비스 정의(proto file)를 이용하여 리버스 프록시 서버를 생성하기 위해서는 gRPC 메서드를 HTTP 리소스에 매핑하여야 합니다. 따라서 http.proto 를 생성합니다.

여기 의 소스코드를 복사하여 google/api 디렉토리에 http.proto 파일을 생성합니다.

$ touch http.proto

gRPC 클라이언트(Stub) 생성

프로토콜 버퍼 컴파일러를 이용하여 golang 에 해당하는 스텁을 생성합니다.

$ cd ../../..
$ protoc -I ./proto \
   --go_out ./proto --go_opt paths=source_relative \
   --go-grpc_out ./proto --go-grpc_opt paths=source_relative \
   ./proto/hello/hello.proto

hello_grpc.pb.go 파일과 hello.ph.go 파일이 생성된 것을 볼 수 있습니다.

이후 스텁에 필요한 패키지를 다운받기 위하여 go mod tidy 명령어를 실행합니다.

$ go mod tidy

리버스 프록시 서버 생성

RESTful HTTP API 클라이언트 호출 지원용 리버스 프록시 서버를 생성하기 위하여 아래와 같이 입력합니다.

$ protoc -I ./proto \
		--grpc-gateway_out ./proto \
		--grpc-gateway_opt logtostderr=true \
		--grpc-gateway_opt paths=source_relative \
		--grpc-gateway_opt generate_unbound_methods=true \
		./proto/hello/hello.proto

동일한 위치에 hello.pb.gw.go 파일이 생성된 것을 볼 수 있습니다.

이후 리버스 프록시 서버에 필요한 패키지를 다운받기 위하여 go mod tidy 명령어를 실행합니다.

$ go mod tidy

main.go 생성

리버스 프록시 서버를 실행시키기 위한 main.go 를 루트 디렉토리에 작성합니다.

$ tocuh main.go
package main

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

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"

	hellopb "guthub.com/[orgs]/hello/proto/hello"
)

type server struct {
	hellopb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *hellopb.HelloRequest) (*hellopb.HelloResponse, error) {
	return &hellopb.HelloResponse{Message: in.Name + " world"}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalln("Failed to listen:", err)
	}

	s := grpc.NewServer()
	hellopb.RegisterGreeterServer(s, &server{})
	log.Println("Serving gRPC on 0.0.0.0:8080")
	go func() {
		log.Fatalln(s.Serve(lis))
	}()

	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()

	err = hellopb.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 서버와 HTTP 서버를 실행합니다.

$ go run main.go

이어서 curl 을 이용하여 HTTP 요청을 진행합니다.

$ curl -X POST http://localhost:8090/echo -d '{"name": " Hello"}'
{"message":" Hello world"}

마치며

여기 에서도 언급하였지만 gRPC-Gateway 는 현재 golang 에서만 지원되고있습니다. 어서 빨리 Node.js 를 포함한 다른 언어들을 지원하여 조금 더 익숙한 언어로 사용할 수 있으면 좋을 것 같습니다 :smile:

Reference