Kotlin으로 Reactive Programming 개념 익혀보려다 엄한 짓 해본 후 사내 개발자들과 공유하려고 기록했던 것 옮겨 적어본다.
--- 시작 ---
- Rest API 구현 : Kotlin, Spring boot, Reactive Programming(R2DBC)
## 프로젝트 생성.
start.spring.io 에서 kotlin-gradle 프로젝트 선택해서 build.gradle.kts 파일이 생성.
- xml 제약을 넘어서기 위해 groovy 스크립트 활용 가능한 gradle 이 나왔는데 코트린 풀젝에서는 여기서 한 걸음 더 나아가 코틀린 스크립트(kts)를 사용할 수 있다.
아래는 java 17, jpa, reactive(r2dbc), postgresql 를 사용하기 위한 기본적인 구성 담긴 build.gradle.kts 파일이다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
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"
kotlin("plugin.jpa") version "1.6.21"
}
group = "com.test.rest-kt"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
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("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.github.microutils:kotlin-logging:2.1.21")
runtimeOnly("io.r2dbc:r2dbc-postgresql")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
## 애플리케이션 실행을 위한 메인 클래스.
@SpringBootApplication
class DpRestApplication
fun main(args: Array<String>) {
runApplication<DpRestApplication>(*args)
}
## application.yml 중 r2dbc 위한 주요 설정 부분만 발췌.
spring:
...
data:
r2dbc:
repositories: enabled=true
r2dbc:
url: r2dbc:postgresql://postgres@...:5432/db_name
username: XXX
password: XXX
pool:
...
...
server.port: 8099
## 모델 클래스
얼마 전 java 의 record 란거 소개할 때 잠깐 언급했던 data 클래스를 이용해보았다.
@Table("aht_job")
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
@JsonIgnoreProperties("id")
data class AhtJob(
@Id
var id: Long = 0,
val cheId: String,
val jobType: String,
@JsonProperty("status")
val jobSts: String,
@JsonProperty("from_location")
val fromLoc: String = "",
@JsonProperty("created")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss[.SSS]")
val creDt: LocalDateTime?,
@JsonProperty("updated")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss[.SSS]")
val updDt: LocalDateTime
)
## 레포지토리
import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
...
@Repository
interface AhtJobRepository: ReactiveCrudRepository<AhtJob, Long> {
@Query("select * from aht_job where id > 1")
fun getSomeAhtJobs(): Flux<AhtJob>
fun findByJobId(jobId: String): Mono<AhtJob>
}
## RestController
import mu.KLogging
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
...
@RestController
@RequestMapping("/api/v1")
class PortController(private val portService: PortService) {
companion object : KLogging()
@GetMapping("/ahtjobs")
fun getAhtJobs(): ResponseEntity<*> {
logger.info("logger test")
return ResponseEntity.ok(portService.getAhtJobs())
}
@GetMapping("/somejob")
fun findAhtJob(): ResponseEntity<*> {
logger.info("logger Native Query")
return ok(portService.getSomeAhtJobs())
}
@GetMapping("/ahtjob/{jobId}")
fun findAhtJobByJobId(@PathVariable("jobId") jobId: String): Mono<ResponseEntity<EvtAhtJob>> {
logger.info("find job id")
return portService.findAhtJobByJobId(jobId)
.map { ahtJob -> ResponseEntity.ok(ahtJob) }
.defaultIfEmpty(ResponseEntity.notFound().build())
}
}
## 서비스
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
...
interface PortService {
fun getAhtJobs(): Flux<AhtJob>
fun findAhtJobByJobId(jobId: String): Mono<AhtJob>
fun getSomeAhtJobs(): Flux<AhtJob>
}
@Service
class PortServiceImpl(val ahtJobRepository: AhtJobRepository): PortService {
override fun getAhtJobs(): Flux<AhtJob> = ahtJobRepository.findAll()
override fun findAhtJobByJobId(jobId: String): Mono<AhtJob> {
return ahtJobRepository.findByJobId(jobId)
}
override fun getSomeAhtJobs(): Flux<AhtJob> {
return ahtJobRepository.getSomeAhtJobs()
}
}
--- 샘플 소스 끝 ---
* 소스 전체를 보면 사소한 구문 차이, 타입 차이 외에는 Java, Spring boot, JPA, WebMVC와 거의 비슷해서 이해에는 큰 어려움 없을거라 몇 부분만 살짝 얘기해보자.
#### 엔티티 정의해놓은 거에서는
* jackson 애노테이션 쓰는 방법이 조금씩 다른 거 외에는 별다른 건 없었다(json 메시지에서의 null 값 처리나 jpa 활용 위한 ID 생성 규칙 적용 등등의 좀 더 상세 구현 필요한 부분 있기는 하지만 초간단 구현 목표라 그런 것들은 제외).
* 내 경우 자바 구현 시 Lombok 사용 하기에 형태는 자바 구현과 별 차이 없지만 애노테이션 형식이 조금 다른데 아래는 기존 자바에서 애노테이션 썼던 방식.
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@JsonIgnoreProperties({ "id" })
* val creDt: LocalDateTime?
테스트하다보니 creDt 속성이 널인 경우가 있어 이 속성에 한해 '?' 를 붙여주었다.
코틀린 특징 소개하는 문서에 빠지지 않는게 코틀린의 null 처리 방식이다. 많이 언급되다보니 아는 분들 역시 많겠지만코틀린은 NPE 를 명확하게 관리하기 위해 변수에 널을 허용하지 않는다.
널을 허용해야 하는 경우가 있다면 위 처럼 타입 뒤에 '?' 를 붙여주면된다.
좀 더 덧붙이자면 Null Safe Operator 라고 '?.' 도 제공되는데 가령,
val jobSts: String? = s?.toUpperCase()
은 자바에서 다음과 같이 구현한 것에 해당한다.
if (jobSts != null) jobSts.toUpperCase() else null
#### repository 에서는
* R2DBC 쓰기 위해 ReactiveCrudRepository 를 상속받았다.
* 함수의 리턴 타입이 좀 낯설텐데 Reactive Streams 의 Publisher 인터페이스 구현체는 기본적으로 다음 두 가지가 있다.
- Mono : 0~1 개의 데이터 전달
- Flux : 0~N 개의 데이터 전달
제대로 설명하려면 reactive programming(WebFlux) 에 대한 이해가 좀 필요한 부분이라 상세 설명은 스킵.
* @Query 애노테이션을 이용해서 JPA 에서 처럼 native query 도 사용 가능하다.
* r2dbc 는 ORM 같은거 아니고 그저 reactive 방식을 지원하기 위해 만들어진 Non-blocking 애플리케이션 스택 중 하나일 뿐고 아직은 JPA 에서 적용한 기술 중 일부만 가능하다. 대표적으로 OneToMany, ManyToOne 등의 연관관계는 아직 사용할 수 없다.
#### 컨트롤러 단에서는
* spring WebMVC 쓸 때랑 별 차이 없어 보인다. ResponseEntity 써보았고 기능 자체 구현과는 상관없지만 코틀린에서 logger 쓰는거 테스트해볼 겸 KLogging 이용해서 로그를 추가로 찍어보았다.
* 다른 두 개는 워낙 간단하니 넘어가고 findAhtJobByJobId 함수만 살짝 더 얘기해보자.
* Mono<> 타입으로 결과 리턴 받아서 response 하는데 이 때 데이터가 하나도 없을 경우 Rest API 에서 종종 사용하는 방식처럼 404 코드 응답하도록 map 과 defaultIfEmpty 이용해서 어렵지 않게 구현해보았다.
#### 서비스 단은
* 간단한 샘플에서 굳이 이렇게 분리할 필요까지는 없었지만 그래도 학습 차원에서 interface 를 추가해놓은 거외에는 따로 설명할게 거의 없다.
--- 소스 설명 끝 ---
이게 거의 전부고 API 호출하면 잘 동작.
- 설정에서 서버 포트를 8099 로 설정했으니 'http://...:8099/api/v1/ahtjob/1234' 과 같은 식으로 호출해볼 수 있다.
코딩한 후 간단히 리뷰하면서 메모하고 보니 글 처음에 적었듯 '엄한 짓'이었다 싶어요.
이건 Kotlin 공부도 아니고 Reactive Programming 이해에도 별 도움 안되고 ...
이번은 그냥 소위 '코프링'이란거 이런 식으로 쓸 수 있다 정도 실습한 셈치고 다음에 여유 생기면 애초 목표인 Reactive Programming 이 무엇인지 따로 정리해서 공유토록 해보겠습니다(r2dbc 는 또 뭔가 까지도).
'Lang' 카테고리의 다른 글
jdk 17 에서 spark 애플리케이션 실행 시 IllegalAccessError 해결법 (0) | 2022.07.05 |
---|---|
[python, julia]python 에서 julia 스크립트 파일 실행하기 (0) | 2022.05.20 |
[java]r2dbc 와 jpa/jdbc 같이 사용 시 에러 해결법 (0) | 2022.05.10 |
[go] postgresql 연동 테스트용 go 코딩 샘플 (0) | 2022.04.20 |
[python]엑셀 파일에서 데이터 추출해서 소스 생성하기 (0) | 2022.04.14 |