Spring gRPC
업데이트:
Spring gRPC1
- Spring gRPC는 gRPC2 프로젝트를 간소화하여 구성하기위한 프로젝트이다.
- gRPC란 고성능 오픈 소스 Remote Procedure Call(이하 RPC) Framework로, Client를 Server와 효율적으로 연결할 수 있다.
- Protocol buffer3는 언어와 플랫폼에 중립적인 구조화된 데이터를 직렬화하기 위한 데이터 형식으로, JSON과 비슷하지만 더 빠르고 다양한 언어에서 쉽게 읽고 쓸 수 있다. 현재는 Protocol buffer v3인 proto3로 더 다양한 확장성을 제공한다.
Spring gRPC Sample
- 간단한 Server-Client로 구성된 프로젝트를 기반으로 설명을 진행한다.
hello.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.gracefulsoul.grpc.lib.proto";
option java_outer_classname = "HelloProto";
service Hello {
rpc SayHello(Request) returns (Reply) {}
rpc StreamHello(Request) returns (stream Reply) {}
}
message Request {
string name = 1;
}
message Reply {
string message = 1;
}
- syntax는 Protocol buffer의 사용 버전을 명시하며, proto2 혹은 proto3를 반드시 명시한다.
- option은 세부 설정을 위한 기능으로, 아래 세 가지를 정의하였다.
- ‘java_package는’ Java 혹은 Kotlin 코드를 생성할 때, 패키지 이름을 정의한다.
- ‘java_multiple_files’은 Java 코드를 생성할 때, .proto 파일을 단일 .java 파일로 생성할지 각 Java Class, Enum 등에 대해서 각각 생성할지 결정하는 설정으로 기본값은 false인 단일 .java 파일로 생성한다.
- ‘java_outer_classname’는 Java 코드를 생성할 때, .proto 파일을 .java 파일로 생성할 때 이름을 결정하기 위한 설정으로 기본값은 .proto 파일의 이름을 카멜 표현식으로 변환한 값이다.
- service는 message 타입을 RPC과 함께 사용하기 위해 서비스 인터페이스를 정의하면 Protocol buffer 컴파일러가 선택한 언어로 서비스 인터페이스 코드와 Client와 Server 간의 통신을 추상화하여 복잡한 부분을 숨기고, 사용자가 편리하게 서비스를 이용할 수 있도록 돕기위한 코드인 stubs을 생성한다.
- rpc 키워드를 통해서 각 RPC를 정의하고, 반환되는 값에 stream 유무의 차이는 단일 응답인지 Streaming 응답인지를 구분하기 위한 키워드이다.
- message는 Client와 Server 간의 주고 받을 내용을 정의하는 데이터 구조로, 필드(field)와 값(value)으로 구성된 구조화된 데이터 형식을 사용한다.
- 위의 Request message를 예로 들면, string 타입의 name 필드 1개의 키-값 쌍을 가진다.
Request.java
// Generated by the protocol buffer compiler. DO NOT EDIT!
// NO CHECKED-IN PROTOBUF GENCODE
// source: hello.proto
// Protobuf Java Version: 4.30.2
package com.gracefulsoul.grpc.lib.proto;
/**
* Protobuf type {@code Request}
*/
public final class Request extends
com.google.protobuf.GeneratedMessage implements
// @@protoc_insertion_point(message_implements:Request)
RequestOrBuilder {
private static final long serialVersionUID = 0L;
static {
com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion(
com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC,
/* major= */ 4,
/* minor= */ 30,
/* patch= */ 2,
/* suffix= */ "",
Request.class.getName());
}
// Use Request.newBuilder() to construct.
private Request(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
super(builder);
}
private Request() {
name_ = "";
}
// 이하 생략
}
- 위의 ‘hello.proto’를 빌드하여 생성된 파일 중 Request message에 대한 파일로, 위에서 간단히 세 줄로 설정한 데이터 구조를 Java에 맞추어 객체 생성에 도움이 될 Builder를 포함하여 기본 객체 비교에 대한 Object의 메서드들과 데이터 송수신에 효율적인 구조를 정의한 GeneratedMessage의 각 메서드들을 상속받아 자동으로 구현한 객체이다.
HelloGrpc.java
package com.gracefulsoul.grpc.lib.proto;
import static io.grpc.MethodDescriptor.generateFullMethodName;
/**
*/
@io.grpc.stub.annotations.GrpcGenerated
public final class HelloGrpc {
// 중략
/**
* A stub to allow clients to do asynchronous rpc calls to service Hello.
*/
public static final class HelloStub
extends io.grpc.stub.AbstractAsyncStub<HelloStub> {
private HelloStub(
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
super(channel, callOptions);
}
@java.lang.Override
protected HelloStub build(
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
return new HelloStub(channel, callOptions);
}
/**
*/
public void sayHello(com.gracefulsoul.grpc.lib.proto.Request request,
io.grpc.stub.StreamObserver<com.gracefulsoul.grpc.lib.proto.Reply> responseObserver) {
io.grpc.stub.ClientCalls.asyncUnaryCall(
getChannel().newCall(getSayHelloMethod(), getCallOptions()), request, responseObserver);
}
/**
*/
public void streamHello(com.gracefulsoul.grpc.lib.proto.Request request,
io.grpc.stub.StreamObserver<com.gracefulsoul.grpc.lib.proto.Reply> responseObserver) {
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
getChannel().newCall(getStreamHelloMethod(), getCallOptions()), request, responseObserver);
}
}
/**
* A stub to allow clients to do limited synchronous rpc calls to service Hello.
*/
public static final class HelloBlockingStub
extends io.grpc.stub.AbstractBlockingStub<HelloBlockingStub> {
private HelloBlockingStub(
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
super(channel, callOptions);
}
@java.lang.Override
protected HelloBlockingStub build(
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
return new HelloBlockingStub(channel, callOptions);
}
/**
*/
public com.gracefulsoul.grpc.lib.proto.Reply sayHello(com.gracefulsoul.grpc.lib.proto.Request request) {
return io.grpc.stub.ClientCalls.blockingUnaryCall(
getChannel(), getSayHelloMethod(), getCallOptions(), request);
}
/**
*/
public java.util.Iterator<com.gracefulsoul.grpc.lib.proto.Reply> streamHello(
com.gracefulsoul.grpc.lib.proto.Request request) {
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
getChannel(), getStreamHelloMethod(), getCallOptions(), request);
}
}
// 이하 생략
}
- ‘HelloGrpc.java’는 ‘hello.proto’에서 정의한 Hello service의 각 rpc를 수행하기 위한 객체로, Asynchronous 방식의 연동을 제공하는 HelloStub과 Synchronous 방식의 연동을 제공하는 HelloBlockingStub 등을 포함하여 다양한 기능 수행에 필요한 Stub 객체들을 컴파일을 통해 자동으로 만들어준다.
GrpcServerService.java (Server)
package com.gracefulsoul.grpc.server.service;
import org.springframework.stereotype.Service;
import com.gracefulsoul.grpc.lib.proto.HelloGrpc;
import com.gracefulsoul.grpc.lib.proto.Reply;
import com.gracefulsoul.grpc.lib.proto.Request;
import io.grpc.stub.StreamObserver;
@Service
public class GrpcServerService extends HelloGrpc.HelloImplBase {
@Override
public void sayHello(Request request, StreamObserver<Reply> responseObserver) {
if (request.getName().startsWith("error")) {
throw new IllegalArgumentException("Bad name: " + request.getName());
}
if (request.getName().startsWith("internal")) {
throw new RuntimeException();
}
Reply reply = Reply.newBuilder().setMessage("Hello " + request.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void streamHello(Request request, StreamObserver<Reply> responseObserver) {
int count = 0;
while (count < 10) {
Reply reply = Reply.newBuilder().setMessage("Hello " + request.getName()).build();
responseObserver.onNext(reply);
count++;
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
responseObserver.onError(e);
return;
}
}
responseObserver.onCompleted();
}
}
- 서버에서 각 호출에 대해서 응답을 제공하기 위한 RPC를 Hello service를 구현한 가장 기본 객체인 HelloImplBase를 상속받아 구현하였다.
- 주요 차이점은 아래와 같다.
- sayHello(Request request, StreamObserver<Reply> responseObserver) 메서드는 request로 전달받은 요청을 responseObserver를 이용하여 reply를 한 번에 전달하고 전송을 종료한다.
- streamHello(Request request, StreamObserver<Reply> responseObserver) 메서드는 request로 전달받은 요청을 responseObserver를 이용하여 원하는만큼 분할하여 여러 번 전송하고 전송을 종료한다.
GrpcClientApplication.java (Client)
package com.gracefulsoul.grpc.client;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.grpc.client.GrpcChannelFactory;
import com.gracefulsoul.grpc.lib.proto.HelloGrpc;
import com.gracefulsoul.grpc.lib.proto.Request;
@SpringBootApplication
public class GrpcClientApplication {
private static final Log LOG = LogFactory.getLog(GrpcClientApplication.class);
public static void main(String[] args) {
SpringApplication.run(GrpcClientApplication.class, args);
}
@Bean
HelloGrpc.HelloBlockingStub stub(GrpcChannelFactory channels) {
return HelloGrpc.newBlockingStub(channels.createChannel("local"));
}
@Bean
CommandLineRunner runner(HelloGrpc.HelloBlockingStub stub) {
return args -> LOG.info(stub.sayHello(Request.newBuilder().setName("GracefulSoul").build()));
}
}
- stub(GrpcChannelFactory channels) 메서드를 통해서 Synchronous 방식의 HelloBlockingStub을 테스트를 위해 ‘local’로 채널을 생성한 객체를 만들어준다.
- runner(HelloGrpc.HelloBlockingStub stub) 메서드로 서버측의 sayHello RPC에 ‘GracefulSoul’을 전달해준다.
- 결과는 아래와 같다.
message: "Hello GracefulSoul"
Conclusion
- 현재 서비스의 아키텍쳐가 다양해짐에 따라 구성되는 서비스의 언어 또한 필요에 따라 다양한 언어들로 구성을 하여 서로의 장점을 이용한 최적의 서비스를 제공하고있다.
- 기존엔 REST API를 설계 후 Request와 Response에 대한 상세 문서로 연관 서비스 개발자들과 공유하고 관리하는 시간 또한 생산성의 저하의 주 요인이었다면, gRPC를 통해 자동화하고 정형화된 코드 기반으로 개발자들은 비즈니스 로직에 집중할 수 있다.
- 실 운영에서 사용해보면 단순한 REST API보다는 러닝 커브와 디버깅에 대한 다양한 이슈를 접하겠지만, 익숙해지면 더 나은 개발 환경을 여러분에게 제공하게 될 것이다.
Reference
※ Sample Code는 여기에서 확인 가능합니다.
댓글남기기