gRPC와 자바

Intro

이 아티클은 사내 프로젝트에 gRPC 를 적용하기 위한 고민과 경험을 기록한 내용입니다. 아래에 서술 된 모든 소스는 https://github.com/glqdlt/ex-grpc 에서 확인 하실 수 있습니다.

gRPC는 http2 를 기반으로 사용하는 RPC 프레임워크입니다. gRPC 를 사용한다는 것은 http2 에 대해서도 알아간다는 것을 의미하며, http2의 이해가 없으면 gRPC를 이해하기에 난해합니다. 이런 이유로 이 아티클에서는 gRPC 외에도 http2 에 대한 이야기도 짤막하게 다룹니다.

gRPC

What is gRPC

gRPC는 Google 에서 만든 RPC(Remote Procedure Call) 프레임워크 입니다, 단순하게 google + RPC 란 의미를 가지고 있습니다.

gRPC는 stubby rpc 를 기반으로 개발되었습니다. gRPC가 개발 되기 이전 google 에서는 stubby 라는 RPC를 사용해서 10년 이상 Micro Service 를 google 인프라에 정착시키고 사용했었습니다. 그러나 stubby 는 특정한 표준화가 없었던 탓에 google 인프라에는 잘 정착되었지만, 다른 곳에서 사용하기에는 범용적이지 못한 부분이 있었습니다.

http2가 세상에 공개가 되면서, stubby를 표준화 할 수 있다는 영감을 얻은 Google은 stubby 를 재작업하였고 그 결실을 맺은 것이 gRPC 입니다. (자세한 개발 비하인드는 gRPC blog에서 원본을 찾아볼 수 있습니다.)

gRPC는 로드 밸런싱, 추적, 상태 확인 및 인증을위한 플러그 가능한 지원을 통해 데이터 센터에서 또는 서비스 전반에 효율적으로 연결할 수 있습니다. (또한 농단 반 섞인 이야기이지만 기존 Restful API 대비 효율이 3배 좋다고 -_-a ) 자세한 이야기는 gRpc 성능 차트Rest vs gRPC 에서 확인해 볼 수 있습니다.

RPC

RPC(Remote Procedure Call)는 이름 뜻 그대로, 원격지의 기능을 호출하는 것입니다. 기능을 사용하고자 하는 사용자(consumer) 가 원격지 상의 제공자(provider)의 기능을 단일 소스 상에서 직접 호출하고 결과를 응답 받는 자연스러운 로직 흐름을 가지자는 개념입니다.

RPC 는 오래 전 부터 있던 개념이었는 데, 매우 획기적인 컨셉에 비해 사용성이 어려워서 많이 알려지지 않았습니다. 원격지의 기능을 호출한다는 컨셉은 피어 간의 통신을 관리해야 함을 의미하고, 공통 된 인터페이스를 정의하고 공유되어야 한다는 전제 때문에 사용성과 관리가 어려웠습니다.

대표적인 RPC 구현체는 아래와 같습니다.

(개인적으로는 처음 접한 RPC는 신입 시절에 Adobe Flex <-> Java SOAP를 다루어 본 게 최초였습니다. 사용 할 원격지의 메소드를 xml 에 클래스와 패키지 주소까지 직접 작성 했어야 했던 끔찍한 기억이 있습니다. IDE에서 자동 완성도 없었던 건 덤.. -_-;)

gRPC Specification

gRPC 의 큰 특징으로는 protocol buffers의 사용으로 특정 언어 종속 없이 폴리그랏한(polyglot) 환경을 꾸릴 수 있다는 점과 http/2 프로토콜 기반의 스트리밍을 사용 할 수 있다는 점이 있습니다. (여기서 폴리그랏(polyglot) 이란 다양한 프로그래밍 언어란 의미를 가집니다.)

아래는 official gRPC about에서 얘기하는 gRPC의 특징입니다.

Core Features that make it awesome:

gRPC Layer

gRPC는 개념적으로 총 3개의 계층을 가집니다.

자세한 것은 gRPC Overview 확인할 수 있습니다.

Protocol buffers

프로토콜 버퍼는 Google 에서 만든 데이터(또는 객체)를 직렬화하기 위한 언어 중립적인 도구입니다. 직렬화를 한다는 점에서 JSON 과 자주 비교가 되는 데, JSON 은 메세징 데이터인 것에 비해 프로토콜 버퍼는 바이너리 데이터로 직렬화가 된다는 점에서 차이가 있습니다.

프로토콜 버퍼는 폴리그랏 프로그래밍에 아주 적절합니다. proto 파일을 정의하여 언어 불문하고 다양한 언어로 컴파일 할 수 있습니다. 현재 대략 10개의 언어를 지원하고 있으며, 이는 gRPC 에서도 10개의 언어를 지원한다는 것과 같습니다.

Protocol Buffers Syntax

gRPC를 통한 개발을 진행하면 가장 첫번째로 마주치는 것으로 프로토콜 버퍼를 정의하는 데에서 부터 시작합니다. 정의 된 proto 파일은 프로토콜 버퍼 컴파일러를 통해 메세징에 사용 될 Model 과 BASE 서비스 소스(gRPC 에서만 지원)로 컴파일 됩니다.

기본적인 프로토콜 버퍼 문법은 아래와 같습니다.

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

enum 과 같은 열거형 상수 타입도 지원하며, repeated 키워드를 통해 List 타입을 선언할 수 있습니다. 프로토콜 버퍼는 기본적으로 빌더 패턴을 사용하며, required 와 optional 과 같은 객체 초기화에 꼭 필요한 필드를 강제화 하거나 선택적 옵션 필드로 선언할 수 있습니다.

프로토콜 버퍼에서는 하위 호완성을 위해 메세지 필드의 순서를 정의합니다. 이 순서는 바이너리 데이터에서 어느 위치에서 해당 필드 데이터를 읽어야 하는지 알려주는 정보로 활용 됩니다.

이 기능은 피어와 피어가 proto 파일을 항상 최신 버전으로 갱신해야하는 제약의 부담스러운 부분을 해소해줄 수 있습니다.

예를 들어 클라이언트가 필드가 2개 밖에 없는 과거 버전의 proto 파일을 사용하고 있는데, 서버에서는 필드가 4개 더 추가 된 proto를 사용 하더라도 클라이언트, 서버에서 서로 호환성에 문제 없이 사용 할 수 있습니다. 클라이언트는 자기가 알고 있는 메세지의 필드만 접근할 것이고, 서버에서는 최신 버전에 추가된 메세지를 보내더라도 클라이언트는 추가 된 부분을 무시해버리게 될 테니깐요.

이를 통해 개별적으로 클라이언트와 서버는 자연스럽게 느슨한 결합을 유지할 수 있게 됩니다. 이는 위에서 말했듯 proto 가 업데이트 되었다고 해서 클라이언트가 필드 사용 유무에 따라 강제 업데이트를 하지 않아도 된다는 걸 의미합니다.

하지만 여기에도 제약 사항이 있습니다, 메세지의 필드 네이밍은 항상 동일해야 하는 점이 있습니다.

위 외에도 gRPC 에서 프로토콜 버퍼를 사용할 때에는 추가적으로 아래와 같이 서비스 인터페이스도 정의할 수 있습니다.

message UserRequest{
    ....
}

// gRPC 에서 사용 될 서비스와 메소드 정의
service UserService {
    rpc getUserDetail (UserRequest) returns (UserDetail) {};
}


message UserDetail {
        ....
    }

프로토콜 버퍼의 더 자세한 것은 Official Protocol Buffers Develop Guide 를 참고해주세요.

Streaming

gRPC 에서는 http2 프로토콜을 기반으로 동작합니다. http2 에는 기존 http1.1 과는 많이 진보된 구조를 가지기에 많은 차이가 있습니다.

대표적으로 기존 요청(reqeust)과 응답(response)을 확장한 ‘메세지(message)’가 있고, 통신 단위인 ‘프레임(frame)’과 실제 메세징이 올라가는 ‘스트림(stream)’이 있습니다. 쉽게 기차를 예로 들면, 프레임은 기차의 열칸(1호, 2호)과 유사하며, 스트림은 연결 된 기차에 해당합니다.

(http2 의 이야기는 별도의 아티클을 작성해서 다루어 보겠습니다. 자세한 것은 http2 Official Blog에서 참고하실 수 있습니다.)

gRPC 에서 말하는 스트리밍이란 쉽게 말해서 http2 스트림을 사용한다는 의미입니다. gRPC의 스트리밍이 좋은 점은 병렬 처리를 쉽게 적용 할 수 있다는 점에서 매우 매력적입니다.

예를 들어 10건의 작업을 해야하는 일감이 있다고 가정하고, 일을 처리하는 시간이 1초가 걸린다고 가정해봅니다. 단일 RPC 요청에서는 10번의 호출을 하기에 총 작업 시간이 10초가 걸리게됩니다. 반면, 스트리밍 RPC로 요청하게 되면 병렬 처리가 가능해짐으로 총 작업 시간이 1~2초 선에서 처리할 수 있는 성능적 이점을 가지게 됩니다.

http2 with gRPC

gRPC 에서 사용 하는 http2 프로토콜은 기존 http1.1 과 개념은 같지만, 매카니즘에서 많은 차이가 있습니다. http2 는 기존 http1.1 의 병렬 처리를 위해 다중 연결(최대 8개)을 사용하던 것과 다르게 단일 연결만 사용합니다. 단일 연결만을 사용하는 http2 에선 병렬 처리를 위해 스트림을 도입했습니다.

http2 stateless procotol?

http2 에서는 http1.1 과 달리 패킷 헤더 압축을 위해 hpack에서 기본적으로 클라이언트와 서버 간의 지속적인 옅은 state를 가지게 됩니다. (hPack RFC 4.3 )

> Header compression is stateful.  One compression context and one   decompression context are used for the entire connection.

(이런 부분이 부담스러워서 stateless 하게 만들어진 CASHPACK 라는 것도 있습니다.)

하지만 기존 http1.1과 유사하게 메세지(요청과 응답)이 필요할 때마다 단일 연결 내에서 새로운 스트림을 열어 통신 하는 구조를 가지고 있습니다.(메세지는 http1.1 에서는 1개의 요청과 1개의 응답이었던 반면, http2 에서는 N개의 요청과 N개의 응답을 보낼 수 있습니다.)

기존 http 1.1에서는 요청과 응답이 있을때에만 연결을 하고 연결을 끊는 반면, http2 에서는 hPack 에서 지속적으로 연결을 맺고 있고, 메세지(요청과 응답)가 필요할 때에만 리소스를 사용합니다. http2 스트림은 정상적인 송수신이 완료되었을 때 발생하는 END_STREAM 프레임을 통해 종료(CLOSEED) 상태에 들어섭니다. 하지만 연결 도중에 즉각 스트림을 해지하고 싶을 때에는 의도하여 RST_STREAM 프레임을 보내 연결을 즉시 해지할 수 있습니다.

기존 1.1의 연결 방식에서 http2의 스트림을 추가해서 병렬 처리하는 것은 기존 http1.1 의 다중 연결(4~8개 정도)에 비하면 매우 효율적으로 리소스를 사용합니다. (Http2 Why just one TCP connection?)

이런 의미에서 http2는 http1.1 에서 업그레이드(대체)가 아닌 새로운 규격입니다. 하지만 기존 http 처럼 요청과 응답이 있으며, 요청이 있을 때에만 리소스 비용이 발생 하게 되는 점에서 http 의 기존 사상을 그대로 유지하고 있습니다.

이 부분은 http2 rfc 의 개요 부분을 보면 명확하게 알 수 있습니다.

This specification is an alternative to, but does not obsolete, the HTTP/1.1 message syntax. HTTP’s existing semantics remain unchanged.

http2는 http1.1을 교체하지 않으며, 기존 http의 의미를 깨지 않는다.

이런 http2의 특징은 gRPC 에서도 그대로 똑같이 적용 되며, gRPC의 클라이언트 서버 간의 커넥션은 지속적으로 맺고 있으나, RPC 사용(메세지 요청)이 있을 때에만 리소스의 비용이 발생하게 됩니다.

Websocket vs Http2

http2 의 서버 푸시 기술과 개선 된 병렬 처리 덕분에 많은 이들이 웹소켓과 비교를 하기도 합니다. http는 기본적으로 요청이 있어야 응답을 하는 매카니즘을 고수합니다. 반면 웹소켓은 요청이 없더라도 서버에서 일방적으로 push 를 할 수 있습니다. 이는 http2의 서버 푸시에서도 마찬가지 작용합니다.

서버 푸시는 클라이언트의 초기 불러와야할 http resources (예: css, js 등) 들의 목록을 전달 받았을 때에, 클라이언트가 추가 요청하기 전에 서버에서 사전으로 보내주는 기능입니다. (http server push wiki)

이런 제약사항이 있기에 일방적으로 서버에서 바이너리 데이터를 push 해줄 수 있는 websocket 과는 근본적으로 차이가 납니다.

gRPC with Servlet

gRPC는 http 2 기반이기에 http2 를 포함하는 구현체 중 하나인 tomcat 최신 버전에서 구현할 수 있지 않을까 생각을 했었습니다. 하지만 gRPC 에서 서블릿은 지원 대상이 아니라는 것을 알게 되었습니다. (netty 프레임워크 나 okHttp 는 지원)

궁금해서 구글링을 통해 찾아보다가 gRPC issue 1621에서 이유를 참고 하게 되었는 데, gRPC는 http에서 사용 되어지는 소켓을 완전히 제어할 수 있어야 한다는 전제가 필요 한것으로 보입니다.

기존 톰캣8.5x 에서도 http2 를 지원하지만, 구축을 하려면 많은 설정이 필요했던 걸 생각하면 어느정도 이해가 되는 부분이 있기도 하네요 (반면 undertow 는 그냥 되던 -_-a)

결국 웹 프로젝트를 진행함에 있어서는 서블릿 컨테이너의 http2을 이용해서 gRPC 를 동작시키는 것은 불가능하고, 서블릿 컨테이너 위에서 okHttp 나 netty 등의 http2 구현체들을 애드온 하는 형태로 개발 해야한다는 것을 의미하게 됩니다.

Getting start

공식 가이드에 있는 예제를 통해 gRPC 를 살짝 맛 봤습니다. 예제 소스는 자바와 메이븐 기반으로 작성했습니다. (추후 NodeJs 기반 Client를 추가적으로 작성 해 볼 생각은 있습니다.)

Work flow

gRPC 기반 프로젝트 작업 흐름을 나열하면 아래와 같습니다.

  1. proto 파일 정의

  2. proto compile

    컴파일 방법은 아래 2가지 중 택 1

    1. (자동) proto buffer generate plugin : 메이븐 빌드 커맨드에 애드온

      또는

    2. (수동) proto buffer compiler 를 직접 실행시켜 생성

  3. compile 된 base source 와 어플리케이션 로직과의 연결

  4. deploy

    1. Server : Server Run()

    2. Client : server remote method call()

gRPC type

gRPC 에서는 전통적인 RPC 인 단일 RPC 의 형태를 비롯해서, 스트림을 이용한 단방향 스트리밍 및 양방햔 스트리밍를 지원합니다. 타입의 결정은 (단방향인지, 스트림인지) proto 파일에서 stream 키워드가 어느 위치에 선언 되어있는 지를 통해 결정이 되어집니다.

gRPC 에서 RPC의 타입은 아래와 같습니다.

자세한 것은 gRPC Concepts 에서 참고할 수 있으며, 메소드 타입에 대한 내용은 JAVA 기준으로 MethodType Enum Java API Docs 에서도 자세한 설명을 얻을 수 있습니다.

부록 : 호출 RPC 타입 디버깅

클라이언트 소스 상에서 아래와 같이 호출 되는 시점을 인젝션하는 인터셉터를 등록해서 실제 해당 호출이 어떠한 Method Type 인지를 출력해 볼 수도 있습니다.


public class ClientApplication {
public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder

                ...

                .intercept(new SimpleHookClientInterceptor())
                .build();

    ...

}

public class SimpleHookClientInterceptor implements ClientInterceptor {
    private final static Logger logger = LoggerFactory.getLogger(SimpleHookClientInterceptor.class);

    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
        logger.info("method : {}, callOpt : {}, next : {}", method.getType(), callOptions.getCredentials(), next.authority());
        return next.newCall(method, callOptions);
    }
}


Getting Started

간단하게 단일 비동기 기반 메세지(요청과 응답)를 다루는 샘플 프로젝트를 시작해봅니다. 모든 소스는 https://github.com/glqdlt/ex-grpc 에 업로드 되어 있습니다.

(또한 본문에서는 내용이 길어지기에 양방향 스트리밍을 다루지 않지만, 이에 대한 예제도 (https://github.com/glqdlt/ex-grpc/blob/master/model/src/main/proto/Simple.proto) 업로드 하였으며, 필요하신 분은 참고해주세요.)

Project Structure

간단한 샘플 프로젝트의 구조는 아래와 같습니다.

grpc-exam
ㄴmodel : proto 가 정의 된 모듈. client 와 server 에서는 이 model 을 의존하게 됩니다.
ㄴserver : host method 를 제공 할 server 에 해당합니다.
ㄴclient : host method 를 호출 할 client 에 해당합니다.

1. Model

model 모듈은 proto 파일에 대한 정의와 컴파일을 담당합니다. 어떻게 보면 다른 모듈에 비해 핵심이라고도 볼 수 있습니다.

proto 파일 정의

proto 파일은 proto buffer 의 문법으로 작성됩니다. 일반적인 자바의 문법과 비슷합니다.

// proto 파일의 버전을 서술합니다.
syntax = "proto3";

// proto 가 빌드될 target package. 생략되면 classpath:/  빌드됩니다.
package com.glqdlt.ex.grpc.client.model;

// java 의 enum 과 같습니다.
enum Gender{
    MAN = 0;
    WOMAN = 1;
}

// 실제 호출 될 서비스를 정의합니다.
service UserService {
    rpc getUserDetail (UserRequest) returns (UserDetail) {};
}

// message 키워드가 붙는 것은 실제 메세지에 실릴 Model 객체입니다.
message UserRequest {
    string id = 1;
}

message UserDetail {
    string id = 1;
    string name = 2;
    int32 age = 6;
    Gender gender = 4;
    string password = 3;
    string address = 5;
    repeated string hobbies = 7;
    map<string, string> auth = 8;
}

proto definition

proto 파일을 컴파일 하는 방법은 직접 컴파일러를 사용해서 컴파일 할 수도 있지만, 프로젝트 소스 툴에 플러그인을 추가하여 자동으로 빌드 시에 컴파일 될 수 있게 할 수 있습니다.

저는 주로 프로젝트 도구로 Maven 을 사용합니다, Maven 에는 protobuf-maven-plugin 이 있습니다.

아래는 proto 파일을 컴파일 하기 위한 메이븐 설정.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.glqdlt.ex.grpc</groupId>
        <artifactId>grpc-exam</artifactId>
        <version>1.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.glqdlt.ex.grpc</groupId>
    <artifactId>model</artifactId>
    <version>1.0-SNAPSHOT</version>


    <properties>
        <java.version>1.8</java.version>
        <grpc.version>1.17.1</grpc.version>
        <protobuf.version>3.5.1</protobuf.version>
        <protoc.version>3.5.1-1</protoc.version>
        <netty.tcnative.version>2.0.7.Final</netty.tcnative.version>
    </properties>


    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>20.0</version>
        </dependency>

        <dependency>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.5.0.Final</version>
        </dependency>

        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-testing</artifactId>
            <version>${grpc.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java-util</artifactId>
            <version>${protobuf.version}</version>
        </dependency>

        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty-shaded</artifactId>
            <version>${grpc.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>${grpc.version}</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>${grpc.version}</version>
        </dependency>
    </dependencies>


    <build>
    <!-- 아래 build plugin 에 ${os.detected.classifier} OS 정보를 넣어주기 위한 확장 플러그인 -->
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.5.0.Final</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!-- maven 의 compile 단계에서 .proto 를 generate 플러그인 -->
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.5.1</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>


        </plugins>
    </build>


</project>

model 모듈을 빌드 하게 되면 아래와 같이 User.proto 파일이 자바 class 로 컴파일 된 것을 알 수가 있습니다.

물론, class 외에도 java 소스 원본도 존재합니다. generated sources 폴더에 가면 원본 .java 파일이 생성 되어 있음을 알 수 있습니다.

2. Server

이 모듈은 실제 원격 호출 될 로직이 존재하는 곳으로서 Model 모듈 에서 생성 된 BASE 소스와 비지니스 로직을 연결 하는 것이 핵심입니다.

Server 모듈의 메이븐 설정

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.glqdlt.ex.grpc</groupId>
	<artifactId>server</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>server</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
		<grpc.version>1.17.1</grpc.version><!-- CURRENT_GRPC_VERSION -->
		<protobuf.version>3.5.1</protobuf.version>
		<protoc.version>3.5.1-1</protoc.version>
		<netty.tcnative.version>2.0.7.Final</netty.tcnative.version>
	</properties>

	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
        <!-- model module 을 의존 -->
		<dependency>
			<groupId>com.glqdlt.ex.grpc</groupId>
			<artifactId>model</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>

		</plugins>
	</build>

</project>


proto 에서 생성 된 소스를 BASE로 하는 UserService 구현체

package com.glqdlt.ex.grpcexam;

// model 모듈의 .proto 에서 생성된 소스들을 import
import com.glqdlt.ex.grpcexam.model.User;
import com.glqdlt.ex.grpcexam.model.UserServiceGrpc;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class UserServiceGrpcImplement extends UserServiceGrpc.UserServiceImplBase {
    private final Logger logger = LoggerFactory.getLogger(UserServiceGrpcImplement.class);

    private static final String SOME_USER_ID = "glqdlt";

    private User.UserDetail generateUserDetail() {
        return User.UserDetail.newBuilder()
                .setId(SOME_USER_ID)
                .setAddress("Seoul")
                .setAge(20)
                .setName("Jhun")
                .setPassword("12345")
                .setGender(User.Gender.MAN)
                // hobbies 는 repeated 으로 선언되었던 것을 참고
                .addHobbies("Coding")
                .addHobbies("Walking")
                .putAuth("role", "admin")
                .build();
    }
    

    @Override
    public void getUserDetail(User.UserRequest request, StreamObserver<User.UserDetail> responseObserver) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Optional<String> req = Optional.ofNullable(request.getId());
        logger.info("Get Request : {}",req.orElse("Null?"));
        if (req.isPresent()) {
            if (req.get().toUpperCase().equals(SOME_USER_ID.toUpperCase())) {
                responseObserver.onNext(generateUserDetail());
                responseObserver.onCompleted();
            }
        } else {
            responseObserver.onError(new RuntimeException("Bad Wrong Request..!"));
        }
    }
}

서비스 구현체들이 실제 동작할 gRPC 서버 환경설정.

package com.glqdlt.ex.grpcexam;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.IOException;


@Component
public class GrpcServer {

    private final Logger logger = LoggerFactory.getLogger(GrpcServer.class);

    private static final String prop = "grpc.server.port";

    @Value("${grpc.server.port}")
    private Integer port;

    @Autowired
    private UserServiceGrpcImplement userServiceGrpcImplement;

    private Server server;

    public void start() throws IOException, InterruptedException {
        if (port == null || port == 0) {
            logger.error("gRpc Server Port is Not SetUp..! '{}' need check!", prop);
            System.exit(-1);
        }
        server = ServerBuilder.forPort(port)
                .addService(userServiceGrpcImplement)
                .build();
        server.start();
        logger.info("gRPC Server  Started! Port : {} ", server.getPort());
        Runtime.getRuntime().addShutdownHook(new Thread(GrpcServer.this::stop));
        server.awaitTermination();
    }

    private void stop() {
        server.shutdown();
    }

}

Client

Client 에서는 stub 이라는 개념이 등장 합니다. 이 stub 은 사전적인 의미에서는 ‘껍데기’ 의미를 갖는 데, 이 의미처럼 gRPC 에서도 ‘어떠한 것이 존재할 것이다’ 라고 추측하는 의미를 갖습니다. (JAVA RMI(Remote Method Invokation) 에서는 stub / Skeleton 이라 불리웁니다.) stub 은 서버에 사전에 약속한(proto 로 정의한) 메소드가 존재할 것이라 가정하는 클라이언트의 호출 인터페이스인 셈 입니다.

자바에서 gRPC는 총 3가지의 stub을 제공합니다.(언어마다 차이가 있습니다.) stub은 크게 동기/비동기로 구분되어 지며, 동기 객체인 BlockingStub 을 제공하는 Grpc.newBrockingStub() 과 비동기 Stub을 다루는 Grpc.newStub()이 있습니다. 또한 Java 의 Futre를 return 받을 수 있는 Grpc.newFutureStub()도 있습니다.

부록 : TDD 의 Stub

TDD 에서도 stub 이 등장합니다. TDD 에서 stub 은 gRPC 와는 조금 다르게 canned answer 라는 정해진 대답이란 의미를 갖습니다.

예를 들어보면 단어 외우기 시험과 비슷합니다. 출제 될 특정 단어들을 기재하두고 문제에서 기재한 단어를 단순히 작성해서 맞추는 시험과 비슷합니다. 어떠한 것을 사전에 약속하고 질의응답을 하는 개념입니다.

gRPC 와 TDD 에서의 stub 은 의미적으로 조금 차이가 있지만, 사용 맥락은 비슷한 것을 알 수 있습니다.


package com.glqdlt.ex.grpc.client;

import com.glqdlt.ex.grpcexam.model.User;
import com.glqdlt.ex.grpcexam.model.UserServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

@SpringBootApplication
public class ClientApplication implements CommandLineRunner {

    private static final String REQUEST_ID = "glqdlt";

    @Value("${grpc.server.port}")
    private Integer port;

    private final Logger logger = LoggerFactory.getLogger(ClientApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }

    private void callBack(User.UserDetail userDetail) {
        logger.info("Received! Response : {}", userDetail);
    }

    @Override
    public void run(String... args) throws Exception {
        ManagedChannel channel = ManagedChannelBuilder
                .forAddress("localhost", port)
                // 간단한 예제이기에 plaintText 모드로 동작시킴
                .usePlaintext()
                .build();
        User.UserRequest req = User.UserRequest.newBuilder().setId(REQUEST_ID).build();

        UserServiceGrpc.UserServiceStub serverResponse = UserServiceGrpc.newStub(channel);
        serverResponse.getUserDetail(req, new StreamObserver<User.UserDetail>() {
            // 서버에서의 응답이 있을 때 호출.
            @Override
            public void onNext(User.UserDetail userDetail) {
                callBack(userDetail);
            }

            @Override
            public void onError(Throwable throwable) {
                logger.error(throwable.getMessage(), throwable);
            }

            // 서버에서 완료 응답이 왔을 때 호출되는 메소드.
            @Override
            public void onCompleted() {
                logger.info("Done!");
            }
        });


        channel.awaitTermination(5, TimeUnit.SECONDS);
        logger.info("Channel Terminated");
    }

}

클라이언트의 메이븐 설정, Server 모듈과 큰 차이가 없습니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.glqdlt.ex.grpc</groupId>
    <artifactId>client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>client</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency><dependency>
        <!-- server module 과 마찬가지로 model module 을 의존 -->
        <groupId>com.glqdlt.ex.grpc</groupId>
        <artifactId>model</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

부록 : grpc-spring-boot-starter

위에서는 환경설정을 포함한 부수적인 것들을 직접 설정했다면, 어노테이션 기반으로 쉽게 사용 할 수 있는grpc-spring-boot-starter와 같은 스프링 부트 기반의 프로젝트가 있습니다. 이를 사용하면 보다 쉽게 gRPC 를 사용할 수 있습니다.

예를 들면 아래와 같습니다.

아래와 같은 proto 파일을 정의 했다고 가정하고..

service Greeter {
    rpc SayHello ( HelloRequest) returns (  HelloReply) {}
}

이를 상속 받은 구현체를 gRPC 코어에 등록하는 것은 아래와 같습니다.

@GRpcService(interceptors = { LogInterceptor.class })
public  class GreeterService extends  GreeterGrpc.GreeterImplBase{
    // ommited
}

어노테이션 기반으로 간단하게 등록할 수 있고, 인터셉터도 어노테이션의 옵션으로 등록하여 처리할 수 있습니다.

next

reference