지난 포스팅에서 gRPC에 대해서 설명했으며
이번에는 go를 이용해 사용하는 방법을 정리합니다
gRPC를 사용하기 위해서는 protobuf로 IDL을 정의하고 gRPC 방식으로 컴파일해야 합니다
단순한 protobuf는 go에서 사용하는 방법이 굉장히 단순합니다
하지만 Tree 구조를 가진 protobuf를 go에서 컴파일하는 데는 현재까지 버그가 있습니다
이번 포스팅에서는
- 하나의 protobuf를 정의하고 go에서 gRPC를 사용하는 간단한 예제
- Tree 구조의 protobuf 정의하고 go에서 gRPC를 사용하는 방법
를 정리해보겠습니다
Process
go에서 gRPC를 사용하기 위해서는 다음과 같은 과정을 거쳐야 합니다
1. protobuf를 이용해 IDL 파일 작성(.proto)
- service, rpc, message 정의
2. protoc 툴을 설치하고 이를 사용하여 protobuf 파일(.proto) 컴파일
- name.protobuf -> name.pb.go
3. go에서 protobuf go 파일(.pb.go) import하여 라이브러리처럼 사용
간단한 gRPC 예제(protobuf 파일 한 개)
위의 과정대로 간단한 예제를 작성합니다
1. protobuf를 이용해 IDL 파일 작성(.proto)
helloworld.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;
}
- proto3 버전 사용
- package 이름 정의
- Restaurant service 및 RPC 정의
- RPC에서 call, return 인자로 사용할 message 정의
2. protoc 툴을 설치하고 이를 사용하여 protobuf 파일(.proto) 컴파일
설치
$ go install google.golang.org/protobuf/cmd/protoc-gen-go
gRPC 용 protobuf 컴파일 명령
protoc -I . helloworld.proto --go_out=plugins=grpc:.
- gRPC 용도 말고 단순 protobuf 컴파일 명령 : protoc -I . helloworld.proto --go_out=.
3. go에서 protobuf go 파일(.pb.go) import하여 라이브러리처럼 사용
gRPC Server와 gRPC Client 서버에서 helloworld.pb.go를 import하여 구현 수행
server.go : Client에서 연결하여 RPC를 호출할 수 있도록 RPC 구현 및 Server Listen 수행
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)
const (
port = ":50051"
)
// server is used to implement helloworld.GreeterServer.
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
- pb를 import
- pb.UnimplementedGreeterServer를 가진 struct 생성
- RPC 구현
- main
- gRPC server 생성/등록/Serve 수행
client.go : Server에 연결하여 RPC 호출 수행
package main
import (
"context"
"log"
"os"
"time"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the server and print out its response.
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
- pb를 import
- main
- grpc 옵션과 함께 Dial 수행하여 Server와 연결
- pb에 정의된 RPC 호출
실행 결과
Server
Client
간단한 gRPC 예제였습니다
이러한 과정으로 어렵지 않게
작성한 protobuf 파일을 pb.go로 컴파일하고
pb.go를 기반으로 gRPC Server와 Client를 구현하면
Client에서 Server에 접속하여 gRPC Call을 수행할 수 있습니다
Tree 구조의 protobuf 파일
이번엔 protobuf 파일 한 개가 아닌
아래와 같은 구조를 가진 protobuf를 컴파일해보겠습니다
- common.proto는 package common
- as.proto는 package as
- gw.proto는 pacakge gw
로 되어 있으며
gw가 common을 import
as가 common을 import
하여 사용하는 구조입니다
일반적으로 사용되는 protobuf 구조이고 특이한 구조는 아니지만
이전과 같은 방식으로 protobuf를 compile한 경우,
작성한 방식에 따라 protobuf compile은 될 수 있으나 go compiler에서 에러가 발생합니다
protoc -I common common/common.proto --go_out=plugins=grpc:common
protoc -I as as/as.proto -I common --go_out=plugins=grpc:as
protoc -I gw gw/gw.proto -I common --go_out=plugins=grpc:gw
컴파일 결과로 생성된 gw.pb.go나 as.pb.go에서 import하는 common package의 위치가
적절히 설정되지 않기 때문입니다
다른 컴파일 방법을 찾아 수행해 보아도 파이썬 등에서는 문제없이 수행되나
protobuf 정의에 따라 golang에서만 이런 문제가 발생합니다
이러한 버그? 문제 때문에 찾아보면 아래와 같은 이슈가 있었고,
이를 해결하기 위해서는 grpc 컴파일로 생성된 코드를 직접 수정해야 할 부분이 많았습니다
하지만 아래의 2019년 1월 업데이트로 어느 정도 완화시킬 수 있었으며
결론적으로 여러 삽질 끝에 해당 과정을 아래와 같이 스크립트화할 수 있게 되었습니다
*macos에서는 sed 명령 사용 시 backup 옵션이 필요
[자세한 과정]
/*
위와 같은 디렉터리 구조지만
gw/gw.proto 파일의 import를
import "../common/common.proto";에서
import "common/common.proto";로(문법 에러 발생하지만 무시합니다)
변경해줍니다
as/as.proto 파일의 import도 마찬가지로
import "../common/common.proto";에서
import "common/common.proto";로(문법 에러 발생하지만 무시합니다)
변경해줍니다
protos 디렉터리에서 아래 명령 수행하여 protobuf 컴파일 정상 수행
for x in **/*.proto; do protoc --go_out=plugins=grpc,paths=source_relative:. $x; done
마지막으로
protobuf 컴파일 결과로 나온 gw/gw.pb.go와 as/as.pb.go 파일의
common import를 실제 패키지 경로에 맞게 직접 수정합니다
저의 경우엔 "uangel.com/skbib/protos/common" 입니다
이후, 전체 go build를 수행하여 정상적으로 컴파일되는 것을 확인할 수 있습니다
이와 같은 과정을 거쳐야 golang에서는 정상적으로 tree 구조의 protobuf를 사용할 수 있습니다
굉장히 번거로운데 한 번만 세팅해두면 이후 사용에 문제는 없습니다
*/
이후 생성된 pb.go 파일을 이용해
앞의 예제와 마찬가지로 방식으로 server와 client를 구현하면 문제없이 동작합니다
'Programming > Go' 카테고리의 다른 글
Go/Golang Test (2) | 2021.08.26 |
---|---|
Go/Golang Memory Management (16) | 2021.08.22 |
Go/Golang Configuration (0) | 2021.08.16 |
Go/Golang awesome-go (0) | 2021.08.11 |
Go/Golang Long Time Performance Test - Memory Leak 해결 과정 (0) | 2021.07.11 |