이번 글에서는 Go gRPC의 프로젝트 세팅과 Unary RPC call에 대해 다룹니다
gRPC는 구글에서 2015년에 공개한 오픈소스 리모트 프로시져 콜 시스템이다
HTTP/2의 stream을 지원하며 Protocol Buffer를 이용해 proto파일을 작성하고 제공하는 Go, C++, Java, Python, Ruby 등에서 사용할 수 있는 코드로의 변환을 제공하고 있다
gRPC는 다음과 같이 4가지의 서비스 메소드를 정의하고 있다
-
Unary RPC
클라이언트는 서버에 싱글 리퀘스트를 보내고 다시 싱글 리스폰스를 받는다. 일반적인 함수의 호출과 같다
-
Server streaming RPC
클라이언트가 서버에 리퀘스트를 보내고 스트림을 가져와 일련의 메세지를 읽는다
리턴되는 스트림이 더 이상 메세지가 없을 때까지 읽음
gRPC는 개별적인 RPC call에 대해 메세지 순서를 보증한다
-
Client streaming RPC
클라이언트는 일련의 메세지를 작성하고 제공되는 스트림을 통해 서버로 전송한다
클라이언트에서 메세지 작성을 끝내고 서버에 전송하면, 서버에서 읽고 응답을 반환할 때까지 기다린다.
Server streaming RPC와 마찬가지로 개별적인 RPC call에 대해 메세지의 순서를 보증한다
-
Bidirectional streaming RPC
read-write 스트림을 이용해 일련의 메세지를 서버, 클라이언트 양쪽에서 보낸다
두 개의 스트림은 독립적으로 작동하기에 클라이언트와 서버는 원하는 순서대로 읽고 쓸 수 있도록 해준다
이번 글에서는 Unary RPC 서비스 메소드에 대해 다뤄 볼 것이다
gRPC에 필요한 세팅
일단 protoc와 grpc 모듈을 설치해야 한다.
proto
파일은 protoc
을 통해 Go에서 사용가능한 코드로 변환시켜 사용한다
이를 위해선 protocol buffer가 필요하다
Protocol Buffers 다운로드
https://github.com/protocolbuffers/protobuf/releases
Ubuntu 환경
$ apt install -y protobuf-compiler
// Ubuntu 기준
MacOS 환경
$ brew install protobuf
윈도우 유저는 (또는 위 방식이 안된다면) Install pre-compiled binaries를 참고하면 된다
설치가 잘 되었는지 버전 확인을 해보자
$ protoc --version
libprotoc 3.0.0
Ubuntu 기준 : 만일 command not found: protoc
이 뜬다면 아래 커맨드를 입력해 환경변수에 $GOPATH/bin
를 추가한다
$ export PATH="$PATH:$(go env GOPATH)/bin"
이제 작업할 프로젝트를 생성해보자
my_project로 폴더를 만들고 아래와 같이 go Module 초기 설정을 하였다
my_project$ go mod init my_project
그리고 서버와 클라이언트 코드에서 사용할 grpc모듈을 설치한다
my_project$ go get -u google.golang.org/grpc
go: google.golang.org/grpc upgrade => v1.32.0
# ~~ 로그 길어서 생략
go: google.golang.org/protobuf upgrade => v1.25.0
proto파일을 go 코드로 변환시킬 것이기에 protoc-gen-go
플러그인도 설치한다
my_project$ go get github.com/golang/protobuf/protoc-gen-go
Unary gRPC 사용하기
grpc통신을 위해 api.proto
파일에 proto 형식을 정의해본다
api.proto
syntax = "proto3";
package api;
option go_package = "api/proto";
service Api {
rpc GetHello(Request) returns (Reply) {}
}
message Request {
string name = 1;
}
message Reply {
string message = 1;
}
GetHello
라는 이름의 rpc 서비스를 정의하였다. 클라이언트에서 name
데이터가 담긴 메세지 Request를 보내면 서버에서 message
데이터가 담긴 메세지 Reply를 받게 될 것이다
package
: 다른 프로젝트들과의 naming conflict 방지를 위해 선언go_package
: 생성될 코드가 포함되는 패키지의 import 경로를 정의한다
아래 명령어를 통해 api.proto
를 api.pb.go
로 변환해본다
my_project$ protoc --go_out=plugins=grpc:. ./api.proto
--go_out
에 plugins=grpc:
을 붙이지 않으면 GetApiServer
와 같은 grpc를 위한 함수가 생성되지 않으니 주의해야 한다
이제 server.go
를 작성해보자. server폴더를 만들고 파일을 생성하였다
my_project
├── api
│ └── proto
│ └── api.pb.go
├── api.proto
├── go.mod
├── go.sum
└── server
└── server.go
아래의 코드는 간단한 server 코드이다. 물론 api.proto
에 정의한 서비스는 아직 grpc서버에 연결되지 않았기에 gRPC와 관련해 우리가 의도하는 동작은 할 수 없다
server.go
package main
import (
"log"
"net"
"google.golang.org/grpc"
)
func main() {
lis, err := net.Listen("tcp", "localhost:50051")
if err != nil {
log.Fatalf("failed to listen %v", err)
}
grpcServer := grpc.NewServer()
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
이제 proto에 정의한 서비스에 대해 함수를 작성해야 한다. my_project/server/handler
에 파일 명은 handler.go로 두었다
handler.go
package handler
import (
"context"
"log"
pb "my_project/api/proto"
)
// APIServer is representation of protobuf ApiServer
type APIServer struct {
}
// GetHello implements api.proto.ApiServer.GetHello
func (s *APIServer) GetHello(ctx context.Context, in *pb.Request) (*pb.Reply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.Reply{Message: "Hello " + in.GetName()}, nil
}
GetHello
함수를 살펴보면 앞에 (s *APIServer)
가 적혀있다. 이는 pointer receiver이며 APIServer의
메소드가 된다
grpc 서비스로 등록하기 위해 APIServer
구조체를 server.go
에서 사용해야 한다
위에서 작성한 handler
를 import 하고 APIServer
를 RegisterApiServer
에 넣어 등록한다
이로써 APIServer
의 GetHello
메소드를 사용할 수 있게 된다
server.go
package main
import (
"log"
"net"
pb "my_project/api/proto"
handler "my_project/server/handler"
//추가
"google.golang.org/grpc"
)
func main() {
lis, err := net.Listen("tcp", "localhost:50051")
if err != nil {
log.Fatalf("failed to listen %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterApiServer(grpcServer, &handler.APIServer{})
//추가
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
서버 코드를 모두 작성하였으니 클라이언트의 코드도 작성해보자
my_project/client
에 client.go
파일을 생성하였다
client.go
package main
import (
"context"
"log"
"time"
pb "my_project/api/proto"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
name = "AaronRoh"
)
func main() {
// 서버 연결 셋업
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewApiClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
reply, err := c.GetHello(ctx, &pb.Request{Name: name})
// GetHello 호출
if err != nil {
log.Fatalf("GetHello error: %v", err)
}
log.Printf("Person: %v", reply)
}
지금까지 진행한 프로젝트의 구조는 아래와 같다
my_project
├── api
│ └── proto
│ └── api.pb.go
├── api.proto
├── client
│ └── client.go
├── go.mod
├── go.sum
└── server
├── handler
│ └── handler.go
└── server.go
이제 작성을 마친 gRPC서버와 클라이언트를 실행시켜보자
my_project/server$ go run server.go
my_project/client$ go run client.go
아래와 같이 로그가 나오며 정상적으로 실행된 것을 확인할 수 있다
2020/09/17 01:05:19 Person: message:"Hello AaronRoh"
// client log
2020/09/17 01:05:19 Received: AaronRoh
// server log
이것으로 Go의 gRPC를 위한 세팅, 간단한 Unary RPC 콜을 사용해보았다
다음 포스팅으로는 메시지 내에 메시지를 정의하는, 중첩된 메시지 유형을 만드는 방법에 대해 다뤄보고자 한다.
내용 속 오류 피드백은 언제나 환영합니다
참고자료
댓글