자바로 gRPC 입문하기

Jake Seo·2021년 1월 18일
1

네트워크

목록 보기
6/16

자바로 gRPC 입문하기

gRPC 소개

gRPC는 구글이 개발한 고성능의 오픈소스 RPC 프레임워크이다. 보일러 플레이트 코드를 줄여주면서 MSA 구조 안에서 여러 언어로 작성된 서비스들을 연결하는 것을 도와준다.

개요

이 프레임워크는 RPC의 클라이언트-서버 모델을 기반으로 한다. 클라이언트 어플리케이션이 서버 어플리케이션에 있는 메소드를 마치 자신의 로컬 오브젝트처럼 호출할 수 있게 한다.

다음과 같은 과정을 통해 gRPC를 통한 일반적인 클라이언트-서버 어플리케이션을 작성해볼 것이다.

  1. .proto 파일 내부에 서비스를 정의한다.
  2. protocol buffer compiler 를 이용하여 서버와 클라이언트 코드를 작성한다.
  3. 서버 어플리케이션을 만든다. 생성된 서버 인터페이스를 구현하고, gRPC 서버를 생성(spawn)한다.
  4. 클라이언트 어플리케이션을 만든다. 만들어진 stub을 통해 RPC 호출을 해본다.

간결한 HelloService 를 정의해보자. 이 서비스는 성과 이름을 교환하고 그에
따른 인사를 반환한다.

의존성 설치하기

여기서 필요한 의존성은 grpc-netty, grpc-protobuf, grpc-stub이다.

build.gradle 전체 내용

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
    }
}

plugins {
    id 'java'
}

apply plugin: 'com.google.protobuf'

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    /**
     * gRPC
     */
    compile group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.35.0'
    compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.35.0'
    compile group: 'io.grpc', name: 'grpc-stub', version: '1.35.0'

    implementation "com.google.protobuf:protobuf-java-util:3.8.0"
    compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.8.0'

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.8.0"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.35.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}

이 부분에서 많은 삽질을 했다..

서비스 정의하기

서비스를 정의하는 것부터 진짜 시작이다. 파라미터와 리턴 타입과 함께 원격에서 호출될 수 있는 메소드들을 명시적으로 작성하자.

이 과정은 protocol buffers를 이용하며 .proto 파일 안에서 끝난다. 또한 작성한 파일이 메세지의 구조와 페이로드를 기술하는데도 쓰일 것이다.

기본 설정

HelloService를 위한 HelloService.proto 파일을 만들어보자. 몇가지 기본 설정들을 추가해보자.

syntax = "proto3";
option java_multiple_files = true;
package com.demo.grpc;

첫번째 줄은 이 파일에서 컴파일러가 어떤 문법을 사용하는지에 대해 작성하는 것이다. 또한 proto에서는 기본값으로, 컴파일러는 모든 자바 코드를 하나의 자바 파일로 생성한다. 두번째 줄은 이 세팅을 오버라이드 하는 것이다. 이렇게 오버라이드를 해주면, 개개의 파일로 생성된다.

마지막으로, 생성된 자바 클래스들에서 사용하길 원하는 패키지를 명시해준다.

메세지 구조 잡기

message HelloRequest {
  string firstName = 1;
  string lastName = 2;
}

위 메세지가 요청 payload를 정의한다. 각 속성은 자체 타입에 따라서 메세지에 정의된다.

각 속성에 태그라 불리는 고유한 숫자가 할당되어야 한다. 이 태그는 protocol buffer가 각 속성을 표현할 때, 각 속성의 이름 대신에 사용된다.

그래서, JSON과 다르게, 우리는 매번 firstName이라는 속성에 값을 보내지만, protocol buffer는 1번이라는 숫자로 firstName을 표현할 것이다. 응답 payload 정의는 요청과 비슷하다.

여러 메세지 타입에 걸쳐서는 같은 태그를 사용할 수 있음을 명심해야 한다.

message HelloResponse {
  string greeting = 1;
}

서비스 협약(Contract) 정의하기

service HelloService {
  rpc hello(HelloRequest) returns (HelloResponse);
}

hello() 오퍼레이션은 단항 요청을 받아들이고 단항 응답을 한다. gRPC는 또한 접두사(prefix)인 stream 키워드를 통한 비동기 방식도 지원하니 확인해보자.

코드 만들기

HelloService.proto 파일을 protocol buffer compiler로 넘겨 자바 파일을 만들어보자. 여러가지 방법이 있다.

Protocol Buffer Compiler 그 자체를 이용하는 방법

먼저, 우리는 Protocol Buffer Compiler가 필요하다. 이 링크에서 우리는 미리 컴파일된 많은 바이너리들을 선택할 수 있다.

위와 같은 경로를 가졌을 때, 아래 명령어로 자바 클래스를 만들 수 있다.

protoc --proto_path=src\main\proto --java_out=src\main\java src\main\proto\HelloService.proto

위와 같은 자바 클래스들이 생긴다.

그런데 위와 같은 방법으로 하지말고

Gradle에 생기는 Task를 이용해서 하자.

GRPC 서버 만들기

이전 과정들을 거치며 다음과 같은 키 파일들이 생성되었다.

  • HelloRequest.java
  • HelloResponse.java
  • HelloServiceImplBase.java - 이건 HelloServiceGrpc 밑에 있다. HelloServiceImplBase 의 추상 클래스를 갖고 있다. 또한 서비스 인터페이스 우리가 정의해놓은 모든 인터페이스의 구현을 제공한다.

ServiceBase 클래스 오버라이딩하기

@Generated 어노테이션은 주석처리해도 된다.
generated에서 만들어진 클래스를 가져왔다면, buildscript 부분은 없애주자.

추상 클래스 HelloServiceImplBase의 기본 구현은 메소드가 구현되어 있지 않다며, io.grpc.StatusRuntimeException을 던질 것이다.

우리는 이 클래스를 확장하고 우리 서비스 정의에 있는 hello() 메소드를 오버라이딩해야 한다.

만일 HelloService.proto 파일에 작성한 hello()의 시그니처와 비교하면, HelloResponse를 반환하지 않는다는 것을 알 수 있을 것이다. 대신에, 두번째 인자를 반환을 관찰하는 StreamObserver<HelloResponse>로 받는다. 이게 서버의 응답과 함께 호출하기 위한 콜백이 된다.

이 방법으로 클라이언트가 블록킹 호출이나 논블록킹 호출을 할 수 있는 옵션을 얻게 된다.

gRPC는 오브젝트를 만들기 위해 builder를 사용한다. HelloResponse.newBuilder()를 사용하고 HelloResponse 객체를 빌드하기 위해서 setGreeting을 수행한다. 이 오브젝트를 클라이언트로 보내기 위해서 responseObserveronNext() 메소드로 세팅한다.

마지막으로, RPG를 통한 일을 끝마쳤다고 명시하기 위해서 onCompleted() 를 호출할 필요가 있다. 만일 이 메소드를 호출하지 않으면 커넥션이 계속 유지되고, 클라이언트는 이후의 정보가 올 것이라고 생각하고 계속 기다리게 될 것이다.

gRPC 서버 실행하기

이제 서버로 요청을 받기 위해 위와 같은 코드를 작성하고 실행하면 된다.

8080 포트에서 gRPC 서버를 구동할 것이다. 그리고 정의했던 HelloServiceImpl 서비스를 추가한다. start()가 서버를 실행시킬 것이다. 이후에 프롬프트를 블록킹하면서 포어그라운드에서 서버의 실행을 유지하기 위해서 awaitTermination()을 호출할 것이다.

서버 만들기

gRPC는 커넥션, 커넥션풀, 로드밸런싱 등을 추상화하고 있는 channel 이란 것을 제공한다.

ManagedChannelBuilder를 이용하여 채널을 만들고 서버 주소와 포트를 명시한다.

또한 이번 통신에는 어떤 암호화도 없이, 평문을 사용할 것이다.

위 소스에서 보이듯, 우리는 hello()라는 원격지 프로시저를 호출하기 위한 stub을 만들어야 한다. stub은 gRPC에서 클라이언트가 서버와 소통하는 방법이다. 자동 생성된 stub들을 사용할 때는, stub 클래스가 channel을 감싸주는 생성자들을 가질 것이다.

여기서 우리는 blocking/syncronous stub을 사용했기 때문에, RPC 호출이 서버가 응답할 때까지 기다렸다가 응답을 하거나 또는 exception을 내보낸다. gRPC에 의해 제공되는 stub의 종류는 두가지가 더 있는데, non-blocking/asynchronous 호출을 도와주는 것들이 있다.

마침내, hello() RPC 호출을 해볼 차례이다. 우리는 HelloRequest를 넘겨주면서, firstNamelastName 속성을 사용할 수 있다.

위와 같이 요청을 하면 서버로부터 반환된 HelloResponse를 받아볼 수 있다.

결론

두 서비스 사이에서 통신 개발을 쉽게하기 위해서 gRPC를 어떻게 사용할 수 있는지 살펴보았다. 서비스를 정의하고 gRPC가 모든 보일러 플레이트 코드를 처리할 수 있게함에 집중함으로써 통신을 쉽게 할 수 있었다.

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글