gRPC 구현 - Unary RPC #1

gRPC의 대표적인 4가지 서비스 예제를 직접 구현해보고 정리해 보기로 했다. 이번 글에서는 gRPC 의 가장 단순한 서비스 형태인 Unary RPC 를 작성 해보도록 한다.

Performance is not an Option - gRPC and Cassandra

gRPC에 대한 소개

gRPC 프레임웍에 대한 소개는 좋은 글을 소개하는 것으로 대신하며, 다음 링크(Microservices with gRPC - 디지털 세상을 만드는 아날로거 - Medium)에서는 gRPC를 써야 하는 이유에 대해서 아래와 같이 요약하고 있다.

  • 높은 생산성과 효율적인 유지보수
  • 다양한 언어와 플랫폼 지원
  • HTTP/2 기반의 양방향 스트리밍
  • 높은 메시지 압축률과 성능
  • 다양한 gRPC 생태계
  • 기타 참고 링크
  • What is gRPC?
  • gRPC Concepts

gRPC의 서비스 형태

아래는 gRPC의 대표적인 서비스 구현 방법을 요약 한 것이다.

Unary RPC
클라이언트에서 요청를 보내고 서버에서 응답을 던짐

Server Streaming RPC
클라이언트에서 요청을 보내고 서버에서 스트림을 던짐

Client Streaming RPC
클라이언트에서 스트림을 서버에 던지고 서버에서 응답 받음

Bidirectional Streaming RPC
클라이언트와 서버가 서로 독립적인 스트림을 주고 받음

Unary RPC의 구현

시작하기

Unary RPC 는 클라이언트에서 요청를 보내고 서버에서 응답을 던지는, gRPC의 가장 단순한 형태의 서비스 구현 방법이다.
이제 서버-클라이언트를 구현하는 예제를 작성 할 것이고, 서버는 Room 이라는 서비스를 하도록 하고 이 Room에 입장(Entry) 하는 기능과  조회(EntryList)하는 기능을 구현하기로 한다. 클라이언트는 두개를 작성 할 생각이고, 각각 입장과 조회를 확인할 수 있도록 구현한다. 위 기능을 구현하기 위해 아래와 같이 디렉토리 구조를 잡아봤다.

// 입장 클라이언트 path
$ mkdir -p cmd/unary/client-entry
// 조회 클라이언트 path
$ mkdir -p cmd/unary/client-list
// 서버 path
$ mkdir -p cmd/unary/server
// gRPC 라이브러리 path
$ mkdir -p core/unary

core/unary/unary.proto

기본적으로 gRPC는 서비스 인터페이스와 메시지의 구조를 모두 설명하기 위해 ::프로토콜 버퍼::를 IDL(Interface Definition Language) 로 사용하고 있으며, 이 인터페이스를 기준으로 다양한 환경의 클라이언트와 통신할 수 있도록 도와준다. 예를 들어 C++ 로 작성된 서버와 각각 Go나 Node.js 로 작성된 클라이언트가 동일한 인터페이스를 기준으로 통신 할 수 있다는 이야기다.
앞서 말한 Room 서비스를 만들기 위해 다음과 같이 .proto 파일을 생성 하였고 기능과 데이터 유형을 정의했다.

syntax = "proto3";

package unary;

// Room 서비스를 생성
service Room {
    // Entry RPC 를 생성하고 Guest 정보를 받아 Message 로 응답을 한다
    rpc Entry (Guest) returns (Message);
    // EntryList RPC 를 생성하고 Entry 로 진입한 Guest 목록을 Guests 로 응답한다.
    rpc EntryList (Void) returns (Guests);
}

message Void {}
message Message {
    string message = 1;
}
message Guest {
    string name = 1;
    string age = 2;
}

message Guests {
    // Map (Key-Value) 형태로 데이터를 정의
    map<string, Guest> guest = 1;
    // Array 형태로 데이터를 정의
    // repeated Guest guest = 1;
}

make go code

위에서 작성한 proto 파일을 protoc 로 grpc plugin과 함께 컴파일 하게 되면 프로토콜 버퍼 코드와 서버-클라이언트 코드가 생성되게 된다.  아래는 grpc plugin 으로 go 언어로 컴파일하는 예시이며 Java,C++,Python,Ruby,C,Go,Erlang,Javascript 등 다양한 언어로 변환 가능하다.

protoc --proto_path=core/unary/ --go_out=plugins=grpc:core/unary unary.proto

cmd/unary/server/main.go

protoc로 생성된 코드를 기준으로 Room 서비스를 제공하는 서버코드를 작성한다.
Entry 함수는 첫 방문시 Welcome 인사를 재 방문시 Welcome Back 을 리턴하고, Name 값을 키로 갖는 Map 에 Guest 정보를 저장 하도록 한다.

package main

import (
    "context"
    "fmt"
    "github.com/sirupsen/logrus"
    "google.golang.org/grpc"
    pb "learn-grpc/core/unary"
    "net"
)
// room 클래스 구현
type room struct {
    list *pb.Guests
}
// Entry (방문) 기능 구현
func (t *room) Entry(ctx context.Context, guest *pb.Guest) (*pb.Message, error) {
    var welcomeMsg pb.Message

    if _, ok := t.list.Guest[guest.Name]; !ok {
        t.list.Guest[guest.Name] = guest

        welcomeMsg = pb.Message{Message: "Welcome! " + guest.Name}
    } else {
        welcomeMsg = pb.Message{Message: "Welcome Back! " + guest.Name}
    }
    return &welcomeMsg, nil
}
// EntryList (방문자 목록 조회) 기능 구현
func (t *room) EntryList(ctx context.Context, void *pb.Void) (*pb.Guests, error) {
    return t.list, nil
}

func main() {
    l, e := net.Listen("tcp", ":8080")
    if e != nil {
        logrus.Error(e)
        return
    }

    srv := grpc.NewServer()

    wsrv := &room{
        list: &pb.Guests{
            Guest: map[string]*pb.Guest{},
        },
    }
    pb.RegisterRoomServer(srv, wsrv)

    logrus.Info(fmt.Sprintf("gRPC Server (%s)", l.Addr().String()))

    if e := srv.Serve(l); e != nil {
        logrus.Error(e)
    }
}

cmd/unary/client-entry/main.go

protoc로 생성된 코드를 기준으로 서버에서 작성한 Entry 를 호출하는 클라이언트를 작성한다.

package main

import (
    "context"
    "flag"
    "github.com/sirupsen/logrus"
    "google.golang.org/grpc"
    pb "learn-grpc/core/unary"
)

var (
    name string
)

func init() {
    flag.StringVar(&name, "name", "karl", "input name")
    flag.Parse()
}

func main() {
    conn, e := grpc.Dial("localhost:8080", grpc.WithInsecure())
    if e != nil {
        logrus.Error(e)
        return
    }
    defer conn.Close()

    c := pb.NewRoomClient(conn)

    member := pb.Guest{
        Name: name,
    }

    cb, e := c.Entry(context.Background(), &member)
    if e != nil {
        logrus.Error(e)
        return
    }

    logrus.Info(cb.Message)
}

cmd/unary/client-list/main.go

protoc로 생성된 코드를 기준으로 서버에서 작성한 EntryList 를 호출하는 클라이언트를 작성한다.

package main

import (
    "context"
    "flag"
    "fmt"
    "github.com/sirupsen/logrus"
    "google.golang.org/grpc"
    pb "learn-grpc/core/unary"
)

var (
    name string
)

func init() {
    flag.StringVar(&name, "name", "karl", "input name")
    flag.Parse()
}

func main() {
    conn, e := grpc.Dial("localhost:8080", grpc.WithInsecure())
    if e != nil {
        logrus.Error(e)
        return
    }
    defer conn.Close()

    c := pb.NewRoomClient(conn)

    list, e := c.EntryList(context.Background(), &pb.Void{})
    if e != nil {
        logrus.Error(e)
        return
    }
    for k, v := range list.Guest {
        logrus.Info(fmt.Sprintf("key : %s, value : %s", k, v.Name))
    }
}

구현 결과

실행 결과는 다음과 같다.
서버를 실행 시키고 동일한 사용자 이름으로 entry의 진입, 재진입을 테스트 하고, 방문자 조회를 해봤다.

Electron, React, gRPC로 Cross Platform Application 설계하기