마이크로서비스 api 간 통신을 굳이 사용한 이유는 gRPC만의 장점이 서비스 성능에 있어 더 좋은 결과를 낼 수 있다고 생각했기 때문이다. Rest Api의 경우 클라이언트가 요청할 때마다 connection을 생성해야 한다. 그로 인한 병목현상이 생길 수도 있다. 그러나 gRPC의 경우는 처음 요청 시 생성된 connection을 통해 지속적인 통신을 할 수 있으며 요청에 대한 데이터를 바이너리로 보내기 때문에 훨씬 빠른 성능을 보여준다. 아직 많은 정보가 없어 시행착오가 많이 생기겠지만 GRC를 서비스에 적용해 좀 더 서비스에 도움이 되길 기대해 본다.
spring boot와 gRpc 에 대한 예제를 찾던 중 간단하게 gRPC를 적용할 수 있는 gRPC-spring-boot-starter를 찾았다. grpc-spring-boot-starter를 활용하여 간단하게 gRPC 서버를 만들어 보자. 우선 프로시져 실행을 요청할 client와 요청한 프로시져를 실행 후 결과를 반환할 server를 두 가지 서버를 구축해 볼 것이다.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.4.5")
}
}
plugins {
id "com.google.protobuf" version "0.8.16"
id 'org.springframework.boot' version '2.4.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.tandohak'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
// for gRpc'
implementation 'net.devh:grpc-client-spring-boot-starter:2.11.0.RELEASE'
// for spring boot
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
testCompileOnly 'org.projectlombok:lombok:1.18.20'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.20'
}
configurations.all {
resolutionStrategy.eachDependency { details ->
if ("io.grpc".equalsIgnoreCase(details.requested.group)) {
details.useVersion '1.37.0'
}
}
}
test {
useJUnitPlatform()
}
main/java/protoGen
패키지 아래에 protoBuf를 위한 자바 파일이 생성된다.sourceSets {
main {
java {
srcDir 'src/main/protoGen'
}
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.5.1'
}
clean {
delete "$generatedFilesBaseDir/main/protoGen"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:1.37.0"
}
}
generateProtoTasks {
ofSourceSet('main').each { task ->
task.builtins {
java {
outputSubDir = 'protoGen'
}
}
task.plugins {
grpc {
outputSubDir = 'protoGen'
}
}
}
}
generatedFilesBaseDir = "$projectDir/src/"
}
gpring:
application:
name: local-grpc-client
grpc:
client:
local-grpc-server:
address: 'static://127.0.0.1:6666'
enableKeepAlive: true
keepAliveWithoutCalls: true
negotiationType: plaintext
Greeter.proto
생성
syntax = "proto3";
option java_multiple_files = true;
option java_package = "net.devh.boot.grpc.examples.lib";
option java_outer_classname = "GreeterProto";
service Greeter {
rpc SayHello ( HelloRequest ) returns ( HelloReply ) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
message MapFieldEntry {
string name = 1;
bool result = 2;
}
// The response message containing the greetings
message HelloReply {
MapFieldEntry result = 1;
}
@Service
public class GreeterService {
@GrpcClient("local-grpc-server")
private GreeterGrpc.GreeterBlockingStub greeterStub;
public MapFieldEntry hello(final String name) {
try {
final HelloReply response =
this.greeterStub.sayHello(HelloRequest.newBuilder().setName(name).build());
return response.getResult();
} catch (final StatusRuntimeException e) {
return MapFieldEntry.newBuilder().setResult(false).build();
}
}
}
@RestController
@RequiredArgsConstructor
@SpringBootApplication
public class GrpcSpringBootClientApplication {
private final GreeterService greeterService;
public static void main(String[] args) {
SpringApplication.run(GrpcSpringBootClientApplication.class, args);
}
@GetMapping("/")
public ResponseEntity<?> greeter(String name) {
MapFieldEntry entry = greeterService.hello(name);
if (entry.getResult()) {
return ResponseEntity.ok(entry.getName());
} else {
return ResponseEntity.badRequest().build();
}
}
}
implementation 'net.devh:grpc-server-spring-boot-starter:2.11.0.RELEASE'
* client 서버와 같이 protoBuf를 생성해주는 task를 만들어 준다.
gpring:
application:
name: local-grpc-server
grpc:
server:
port: 9898
@Slf4j
@GrpcService
public class GreeterService extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String message = "SERVER 1 - Hello" + request.getName();
MapFieldEntry mapFieldEntry = MapFieldEntry.newBuilder()
.setName(request.getName())
.setResult(true)
.build();
final HelloReply.Builder replyBuilder = HelloReply.newBuilder().setResult(mapFieldEntry);
responseObserver.onNext(replyBuilder.build());
responseObserver.onCompleted();
log.info("SERVER 1 - Returning" + message);
}
}
client (포트:8081) 에 요청을 하면 결괏값을 반환한다. 여기까지 봤을 때는 평범한 Rest API와 다르지 않다.
Server의 로그를 보면 클라이언트에 요청한 파라미터 값이 전달 된 것을 볼 수 있다.
간단한 예제로 gRPC 통신을 만들어 보았다. 앞으로 갈 길이 멀지만 좀 더 알아보고 연구해 실제 서비스에 적용해 보아야겠다.