MSA 아키텍처 그리고 API 개발에서 주로 Rest API를 활용했지만 서버와 클라이언트간 더 작은 payload값을 가지고 더 빠른 성능을 보여줄 수 있는 gRPC를 적용해보고자 한다.
gRPC와 Rest API의 자세한 사항은 다음 페이지 참고
https://docs.microsoft.com/ko-kr/aspnet/core/grpc/comparison?view=aspnetcore-6.0
두가지를 모두 고려하였으나 내가 구현하고 싶은 범위는 단순히 gRPC를 활용하여 client에서 proto Service를 조회하는 기능 뿐이었고 Armeria 같은 경우에는 너무 많은 기능을 제공하여
2번 grpc-spring-boot-starter를 활용하였다.
의존성을 다음과 같이 설정을 진행하였고
implementation("net.devh:grpc-spring-boot-starter:2.13.1.RELEASE")
Application Build시 proto 파일에 대한 stub class 생성을 위한 gradle 설정까지 진행하였다.
(Maven Setting으로 하려고 하니 너무 복잡해서 gradle project로 변경하였다.)
build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.google.protobuf.gradle.generateProtoTasks
import com.google.protobuf.gradle.id
import com.google.protobuf.gradle.plugins
import com.google.protobuf.gradle.protobuf
import com.google.protobuf.gradle.protoc
/*
* This file was generated by the Gradle 'init' task.
*/
val grpcVersion = "3.19.3"
val grpcKotlinVersion = "1.2.1"
val grpcProtoVersion = "1.45.1"
group = "com.er"
version = "0.0.1-SNAPSHOT"
description = "kotlintoy"
plugins {
id("org.springframework.boot") version "2.6.7"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
id("com.google.protobuf") version "0.8.18"
}
apply(plugin = "java")
apply(plugin = "idea")
apply(plugin = "eclipse")
java.sourceCompatibility = JavaVersion.VERSION_11
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
repositories {
mavenCentral()
}
dependencies {
// SPRING BOOT 설정
implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.projectlombok:lombok:1.18.24")
implementation("org.mapstruct:mapstruct:1.4.2.Final")
// GRPC 설정
implementation("io.grpc:grpc-stub:$grpcProtoVersion")
implementation("io.grpc:grpc-protobuf:$grpcProtoVersion")
implementation("io.grpc:grpc-kotlin-stub:1.2.0")
implementation("io.grpc:grpc-netty-shaded:$grpcProtoVersion")
implementation("com.google.protobuf:protobuf-kotlin:$grpcVersion")
runtimeOnly("org.springframework.boot:spring-boot-devtools:2.6.7")
runtimeOnly("org.slf4j:log4j-over-slf4j:1.7.36")
testImplementation("org.springframework.boot:spring-boot-starter-test:2.6.7")
testImplementation("de.flapdoodle.embed:de.flapdoodle.embed.mongo:3.0.0")
testImplementation("io.projectreactor:reactor-test:3.4.17")
testImplementation("org.springframework.restdocs:spring-restdocs-webtestclient:2.0.6.RELEASE")
implementation("net.devh:grpc-spring-boot-starter:2.13.1.RELEASE")
}
// gradle build시 srcDirs에 파일 생성
// gradle build의 target으로 잡히기 위해서 scrDirs 안에 path를 설정한다.
// (Client가 호출하는) sutb 클래스가 생성될 때의 경로를 설정한다.
sourceSets {
getByName("main") {
java {
srcDirs(
"build/generated/source/proto/main/java",
"build/generated/source/proto/main/kotlin"
)
}
}
}
// protobuf 생성 및 stub 클래스(java, kotlin) 생성
protobuf {
// proto 및 stub 생성을 위한 artifact 설정 begin
protoc {
// protobuf compiler
artifact = "com.google.protobuf:protoc:$grpcVersion"
}
plugins {
// id값이 grpc일 경우 io.grpc:protoc-gen-grpc-java artifact를 활용하여 protobuf 생성 : JAVA
id("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:$grpcProtoVersion"
}
// id값이 grpckt일 경우 io.grpc:protoc-gen-grpc-kotlin artifact를 활용하여 protobuf 생성 : KOTLIN
id("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion:jdk7@jar"
}
}
// proto 및 stub 생성을 위한 artifact 설정 end
// protobuf 생성
generateProtoTasks {
all().forEach {
it.plugins {
id("grpc")
id("grpckt")
}
it.builtins {
id("kotlin")
}
}
}
}
특히 이번 toy Project에서는 kotlin을 활용하고 있기 때문에 stub class로 kotlin 생성까지 설정하였고
경로는 "build/generated/source/proto/main/kotlin"으로 설정하였다.
srcDirs(
"build/generated/source/proto/main/java",
"build/generated/source/proto/main/kotlin"
)
it.builtins {
id("kotlin")
}
프로젝트 언어로 kotlin을 활용한다고 하더라도 반드시 kotlin 경로 및 generateProtoTasks kotlin을 생성할 필요는 없다.
Java Class를 그대로활용할 수 있으며 kotlin 은 Java Class를 kotlin 형태로 래핑한 형태 이기 때문이다.
즉 Java Class를 활용하거나 kotlin 함수를 활용하거나 결과는 똑같다.
// JAVA CLASS 적용
class BaseGrpcServerService(var baseRepository: BaseRepository) : BaseProtoServiceGrpc.BaseProtoServiceImplBase() {
override suspend fun retrieveBaseOnDB(request: BaseRequest): BaseResponse {
val baseDTO = baseRepository.findByBaseId(request.baseId)
return BaseResponse.newBuilder()
.setBaseId(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseId)
.setBaseName(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseName)
.setBaseNumber(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseNumber)
.build()
}
}
// KOTLIN Coroutine 활용
@GrpcService
class BaseGrpcServerService(var baseRepository: BaseRepository) : BaseProtoServiceGrpcKt.BaseProtoServiceCoroutineImplBase() {
override suspend fun retrieveBaseOnDB(request: BaseRequest): BaseResponse {
val baseDTO = baseRepository.findByBaseId(request.baseId)
return BaseResponse.newBuilder()
.setBaseId(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseId)
.setBaseName(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseName)
.setBaseNumber(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseNumber)
.build()
}
}
// 결과는 동일하다.
Baseproto.proto
syntax = "proto3";
// com.er.kotlintoy.grpc.v1.BaseProtoService/retrieveBaseOnDB
package com.er.kotlintoy.grpc.v1;
option java_multiple_files = true; // message와 enum, service 각 java 파일로 생성
option java_outer_classname = "BaseProto"; // 생성될 자바코드의 클래스명 (camel case 변환)
// The BaseProtoService service definition.
// Client에서 stub 객체로 호출할 서비스
service BaseProtoService {
rpc retrieveBaseOnDB (BaseRequest) returns (BaseResponse);
}
// Request messagex
message BaseRequest {
string baseId = 1;
}
// Response message
message BaseResponse {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
string baseId = 4;
string baseName = 5;
string baseNumber = 6;
}
proto 파일에서 enum, repeated(List)를 활용할 수 있었지만 이번 프로젝트에서는 message만을 활용해도 되기 때문에 다음과 같이 설정하였다.
1. syntax = "proto3"
- proto3 문법으로 해당 파일이 작성 되었다.
2. package com.er.kotlintoy.grpc.v1
- stub 객체 생성 package 경로를 설정하였다. (밑의 사진 확인)
3. java_multiple_files = true (defalut 값은 false)
- false인 경우 BaseProto.class 파일만 생성
- true인 경우 message, enum, repeated 등 각각의 class파일이 독립적으로 생성
코드로 간단히 예시를 들면 BaseProto 객체 활용시 다음과 같은 차이가 있다.
// false 인 경우
// BaseProto.BaseRequest, BaseProto.BaseReponse와 같이 BaseProto 객체 안의 메소드 호출
@GrpcService
class BaseGrpcServerService() : BaseProtoServiceGrpcKt.BaseProtoServiceCoroutineImplBase() {
override suspend fun retrieveBaseOnDB(request: BaseProto.BaseRequest): BaseProto.BaseResponse {
return BaseProto.BaseResponse.newBuilder()
.setBaseId("baseId")
.setBaseName("baseName")
.setBaseNumber("BaseNumber")
.build()
}
}
// true 인 경우
// BaseRequest, BaseResponse와 같이 독립적인 객체 활용
@GrpcService
class BaseGrpcServerService(var baseRepository: BaseRepository) : BaseProtoServiceGrpcKt.BaseProtoServiceCoroutineImplBase() {
override suspend fun retrieveBaseOnDB(request: BaseRequest): BaseResponse {
return BaseResponse.newBuilder()
.setBaseId("baseId")
.setBaseName("baseName")
.setBaseNumber("BaseNumber")
.build()
}
}
4. java_outer_classname = "BaseProto"
- Java class명을 설정할 수 있으며 camel Case로 변환하여 파일이 생성된다.
5. (baseProto)service BaseProtoService {
rpc retrieveBaseOnDB (BaseRequest) returns (BaseResponse); }
- Stub 객체 (BaseProtoService.class 또는 BaseProtoService.kt)에서 파라미터 값으로 BaseRequest를 가진 retriveBaseOnDB 메소드 또는 함수 실행하게 된다면 BaseResponse를 리턴해줄 것이다.
추가적으로 gRPC 서버 port를 9090으로 지정해주자.
grpc:
server:
port: 9090
이제 설정은 모두 완료 되었다!
gRPC를 활용하여 API 호출을 해보자.
Client 또한 로직 개발 후 테스트 또는 터미널 grpcurl 명령어로 테스트를 진행 할 수 있지만 우리에게는 Rest API 개발 진행하면서 훌륭한 UI를 가진 test 프로그램 Postman이 있으므로 Postman으로 테스트를 진행 하였다.
Enter Server URL
- gRPC 서버와 Port 입력 (localhost:9090)
Choose a way to load Service and methods :
- 우리가 호출하려는 Stub Class의 패키지 경로 (com.er.kotlintoy.grpc.v1)
Postman에서 stub Class의 경로를 찾는 네가지가 있다.
1) Use server reflection : 서버 내 존재하고 있는 stub class 조회 하기
2) Import protobuf definition from local file : .proto 파일을 직접 등록하기
3) Import protobuf definition from local URL : url 내 .proto 파일을 직접 등록하기
4) Create a new protobuf defintion : 직접 proto 파일 형식을 정의 및 생성하기
select a method
- stub class내에서 호출하려고 하는 method 명 (retrieveBaseOnDB)
테스트 하려는 stub class와 그 안의 method까지 설정하고 빨간 테두리의 Generate Example Message를 클릭하면
다음과 같은 retrieveBaseOnDB의 파라미터값을 받는 baseId로 자동으로 설정해준다.
실행(Invoke)하면 우리가 base 값들을 설정한 것들이 리턴하는 것을 볼 수 있다.
그럼 더 나아가서 baseId값을 받아 MongoDB에서 해당 base값들을 조회하는 로직을 넣어보자.
@GrpcService
class BaseGrpcServerService(var baseRepository: BaseRepository) : BaseProtoServiceGrpcKt.BaseProtoServiceCoroutineImplBase() {
override suspend fun retrieveBaseOnDB(request: BaseRequest): BaseResponse {
val baseDTO = baseRepository.findByBaseId(request.baseId)
return BaseResponse.newBuilder()
.setBaseId(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseId)
.setBaseName(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseName)
.setBaseNumber(withContext(Dispatchers.IO) {
baseDTO.blockFirst()
}?.baseNumber)
.build()
}
}
baseId가 "erer"인 값을 조회 해보자.
"erer"에 해당하는 데이터가 조회되어 리턴되는 것을 확인 할 수 있다.
stub class의 패키지 경로 또는 service 객체명이 맞지 않는다면 다음과 같은 에러가 발생 할 수 있다.
gRPC 서버 상태 값 또한 확인 할 수 있는데
grpc.health.v1.Health/Check stub class를 호출 하면 정상적인 서버 상태에는 다음과 같은 응답값이 발생한다.
또한 gRPC 호출 관련한 grpc.health.v1.Health/Watch로 Trace 정보까지 확인 할 수 있다.
끝.
'개발 지식 > Backend' 카테고리의 다른 글
[Cache] Ehcache vs Caffeine Cache (4) | 2022.09.07 |
---|---|
[WebFlux] WebFlux Kotlin MapStruct 적용 (0) | 2022.05.10 |
[Log] Apache Log4j2 취약점 발견 및 원인 (0) | 2021.12.13 |
[JWT] JSON Web Token? (0) | 2021.10.14 |
[JWT] 서명을 위한 알고리즘 HS256과 RS256 (0) | 2021.10.11 |
댓글