채용 과제를 하면서 급하게 웹플럭스를 익혔다
이전에는 머릿속으로만 희미하게 존재하던 그 녀석을 집적 손으로 다루니 여러모로 느끼게 되었다
거의 2.5일에 걸쳐서 짧은 강의를 완강하고 500page의 책을 75% 정도를 속독한 것 같다
이게 가능한건 토비의 봄 리액티브 편을 종종 누워서 보아왔기 떄문이다
리액티브는 평소 관심은 있었지만 이걸 먼저 공부하면 성장 순서에 악영향을 미칠 것 같았기 때문에,
본격적인 공부는 미루어 왔었다
자, 이제 느낀 걸 적겠다 :)
얼마 전에 알게 된 사실들이라 틀린 내용이 많을 수 있다
느낀 것들
반드시 Mono<Void>를 반환해야 한다
무슨 얘기나면 컨트롤러의 Return type이 없거나, Unit이어선 안됀다
즉, Mono.empty() 라도 반환해야 한다
처음에는 이걸로 정말 당황했다 (시간은 없지... 컴파일도 아닌 런타임 오류 나지...)
플럭스는 내부적으로 반드시 비어있는 Sequence를 반환받아야 하는 것 같다
Operator 를 활용하는 것이 만만치 않다, 어!렵!다!
스프링 리액티브에선 map, doOnNext 등을 연산자라고 한다
나는 각각 다른 데이터를 combine하는 것에 관심을 두었고 zip을 한 번 활용해보기로 했다
아래는 내가 GPT에게 질문한 내용이다
There are two APIs
Each returns a List, so you can use
GET /x
GET /y
# Example response
X: [{a: 1, x:100}, {a: 2, x: 300}]
Y: [{a:1, y:500}]
At this point, I want to create new data using WebFlux's operator.
# Result List
Z: [{a: 1, xy: "100:500"}, {a:2, xy: "300:none"}]
The important thing is to think about cases where X is present but Y is not.
In other words, the result List should be equal to the number of X's.
How can I create data like this?
저기서 X와 Y를 이용해서 새로운 데이터 집합 형태로 만들고자 하는 게 목표다
X, Y는 데이터의 갯수가 다르다
이때 X에만 있는 데이터도 처리를 하고 싶을 때 zip을 이용할 수 없다
ChatGPT, Phind가 여러 방안을 주었지만 해결은 되지 않았다
그래서 flatMap을 이용해서 2시간 가량 어찌 어찌 짠 코드가 아래다
fun query() = runBlocking {
val userDeferred = async { userRepository.findAll() }
val itemDeferred = async { itemRepository.findAll() }
val userIds: Flux<Long> = userDeferred.await().map { it.id!! }.onBackpressureDrop()
val items: Flux<Item> = itemDeferred.await().onBackpressureDrop()
val itemMonoList = items.collectList()
val responseDto = userIds.flatMap { userId: Long ->
val itemCntMono = getItemCntMono(userId, itemMonoList)
convertResponseDto(userId, itemCntMono)
}
responseDto.subscribe { println(it) }
return@runBlocking responseDto
}
private fun getItemCntMono(userId: Long, itemMonoList: Mono<MutableList<Item>>) =
itemMonoList.map { it: MutableList<Item> ->
it.filter { it.userId == userId }.size
}
private fun convertResponseDto(userId: Long, itemCntMono: Mono<Int>) =
itemCntMono.flatMap { it: Int ->
Mono.just(QueryResponse(userId, it))
}
실제로 구하고자 하는 데이터 셋은 "유저가 등록한 상품의 갯수"이다 (등록하지 않은 경우도 포함)
사실 저 문장을 SQL문으로 표현하는 몇 글자 되지 않는다
select u.id, count(i.id)
from users u
left join item i
on u.id = i.user_id
group by u.id;
즉, 단순한 데이터 가공임에도 코드 량에서 매우 차이가 난다
대충 글자수를 새어 보면
대략 10배 정도 ..
(사실 웹플럭스 코드는 내가 사용 용례에 따라 잘 짠 코드가 아닐 수 있다 !! 주의 요망)
단순 테스트 유틸인 WebClientTest 조차도 ...
단순한 RestTemplate같은 Tool이라고 접근했지만 비동기로 호출하다 보니 테스트가 깨지는 현상이 있었다
webClientTest.호출_1() // 1
webClientTest.호출_2() // 2
1이 되어야 2가 success할 수 있는데, 1과 2가 병렬적으로 수행되는 것으로 보인다
1의 결과를 받은 후에 (block 필요) 2를 실행해야 했다
WebFlux로 이득보는 상황을 특정하는 게 쉽지 않아 보인다
우형 세미나를 보면서도 든 생각이지만, 현재의 JVM 진영에는 정말 많은 비동기 기술들이 있다
Thread, Future(Completable 가족들), ExecutorService, @Asnyc(내부적으로 Future사용),
WebFlux, Coroutine, JDK21 Virtual Thread
이 중에서 WebFlux가 고유하게 해주는 것은 비동기와 더불어 배압(Back pressure)로 보인다
배압이란 수신자의 이벤트 처리 량에 맞추어서 발행처가 이벤트를 주는 것이다 (request)
큐에 이벤트가 많이 싸이기 전에 속도를 조절할 수 있다
참고한 서적 및 강의
서적: 스프링으로 시작하는 리액티브 프로그래밍: Spring WebFlux를 이용한 Non-Blocking 애플리케이션 구현
메타 코딩: 웹플럭스 강의
우형 기술블로그: https://techblog.woowahan.com/12903/
'Back-end > Async, NIO (WebFlux, Coroutine)' 카테고리의 다른 글
[Reactive-X, WebFlux] 간단 정리 (0) | 2024.01.03 |
---|
hi hello... World >< 가장 아름다운 하나의 해답이 존재한다
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!