자바로 gRPC 입문하기
gRPC
는 구글이 개발한 고성능의 오픈소스 RPC 프레임워크이다. 보일러 플레이트 코드를 줄여주면서 MSA 구조 안에서 여러 언어로 작성된 서비스들을 연결하는 것을 도와준다.
이 프레임워크는 RPC의 클라이언트-서버 모델을 기반으로 한다. 클라이언트 어플리케이션이 서버 어플리케이션에 있는 메소드를 마치 자신의 로컬 오브젝트처럼 호출할 수 있게 한다.
다음과 같은 과정을 통해 gRPC를 통한 일반적인 클라이언트-서버 어플리케이션을 작성해볼 것이다.
.proto
파일 내부에 서비스를 정의한다.protocol buffer compiler
를 이용하여 서버와 클라이언트 코드를 작성한다.간결한 HelloService
를 정의해보자. 이 서비스는 성과 이름을 교환하고 그에
따른 인사를 반환한다.
여기서 필요한 의존성은 grpc-netty
, grpc-protobuf
, grpc-stub
이다.
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;
}
service HelloService {
rpc hello(HelloRequest) returns (HelloResponse);
}
hello()
오퍼레이션은 단항 요청을 받아들이고 단항 응답을 한다. gRPC는 또한 접두사(prefix)인 stream
키워드를 통한 비동기 방식도 지원하니 확인해보자.
HelloService.proto
파일을 protocol buffer compiler
로 넘겨 자바 파일을 만들어보자. 여러가지 방법이 있다.
먼저, 우리는 Protocol Buffer Compiler
가 필요하다. 이 링크에서 우리는 미리 컴파일된 많은 바이너리들을 선택할 수 있다.
위와 같은 경로를 가졌을 때, 아래 명령어로 자바 클래스를 만들 수 있다.
protoc --proto_path=src\main\proto --java_out=src\main\java src\main\proto\HelloService.proto
위와 같은 자바 클래스들이 생긴다.
그런데 위와 같은 방법으로 하지말고
Gradle
에 생기는 Task
를 이용해서 하자.
이전 과정들을 거치며 다음과 같은 키 파일들이 생성되었다.
HelloRequest.java
HelloResponse.java
HelloServiceImplBase.java
- 이건 HelloServiceGrpc
밑에 있다. HelloServiceImplBase
의 추상 클래스를 갖고 있다. 또한 서비스 인터페이스 우리가 정의해놓은 모든 인터페이스의 구현을 제공한다.
@Generated
어노테이션은 주석처리해도 된다.
generated
에서 만들어진 클래스를 가져왔다면,buildscript
부분은 없애주자.
추상 클래스 HelloServiceImplBase
의 기본 구현은 메소드가 구현되어 있지 않다며, io.grpc.StatusRuntimeException
을 던질 것이다.
우리는 이 클래스를 확장하고 우리 서비스 정의에 있는 hello()
메소드를 오버라이딩해야 한다.
만일 HelloService.proto
파일에 작성한 hello()
의 시그니처와 비교하면, HelloResponse
를 반환하지 않는다는 것을 알 수 있을 것이다. 대신에, 두번째 인자를 반환을 관찰하는 StreamObserver<HelloResponse>
로 받는다. 이게 서버의 응답과 함께 호출하기 위한 콜백이 된다.
이 방법으로 클라이언트가 블록킹 호출이나 논블록킹 호출을 할 수 있는 옵션을 얻게 된다.
gRPC는 오브젝트를 만들기 위해 builder
를 사용한다. HelloResponse.newBuilder()
를 사용하고 HelloResponse
객체를 빌드하기 위해서 setGreeting
을 수행한다. 이 오브젝트를 클라이언트로 보내기 위해서 responseObserver
의 onNext()
메소드로 세팅한다.
마지막으로, RPG를 통한 일을 끝마쳤다고 명시하기 위해서 onCompleted()
를 호출할 필요가 있다. 만일 이 메소드를 호출하지 않으면 커넥션이 계속 유지되고, 클라이언트는 이후의 정보가 올 것이라고 생각하고 계속 기다리게 될 것이다.
이제 서버로 요청을 받기 위해 위와 같은 코드를 작성하고 실행하면 된다.
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
를 넘겨주면서, firstName
과 lastName
속성을 사용할 수 있다.
위와 같이 요청을 하면 서버로부터 반환된 HelloResponse
를 받아볼 수 있다.
두 서비스 사이에서 통신 개발을 쉽게하기 위해서 gRPC를 어떻게 사용할 수 있는지 살펴보았다. 서비스를 정의하고 gRPC가 모든 보일러 플레이트 코드를 처리할 수 있게함에 집중함으로써 통신을 쉽게 할 수 있었다.