본문 바로가기
카테고리 없음

Go gRPC 튜토리얼 #1 - 세팅부터 Unary RPC Call 사용까지

by 노아론 2020. 9. 17.
이번 글에서는 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.protoapi.pb.go로 변환해본다

my_project$ protoc --go_out=plugins=grpc:. ./api.proto

--go_outplugins=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 하고 APIServerRegisterApiServer에 넣어 등록한다

이로써 APIServerGetHello메소드를 사용할 수 있게 된다

 

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/clientclient.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 콜을 사용해보았다

다음 포스팅으로는 메시지 내에 메시지를 정의하는, 중첩된 메시지 유형을 만드는 방법에 대해 다뤄보고자 한다.

 

내용 속 오류 피드백은 언제나 환영합니다

 

참고자료

https://grpc.io/docs/what-is-grpc/core-concepts/

댓글