백엔드 프로젝트를 스프링 웹플럭스로 구현하기로 한 김에..
Filter
, Servlet
) 또는 블로킹(getParameter
, getPart
) 등을 사용하기 어려움웹스택이란? OS, 프로그래밍 언어, 데이터베이스 소프트웨어, 웹 서버 등을 포함하는, 웹 개발에 쓰이는 소프트웨어의 집합
Servlet이란?
- 자바를 허용하는 웹/앱 서버에서 런하는 자바 프로그램
- 웹서버로부터 받아온 요청을 처리하고 응답을 만들어 전송하는 데 쓰임
- 웹서버와 데이터베이스의 중간다리
Reactive: 변화에 대한 반응을 중심으로 짜여진 프로그래밍 모델
Back pressure: 트래픽 통신 과부하에 대한 피드백 매커니즘
Spring Webflux는 리액티브 라이브러리 Reactor
를 채택한다
Reactor
의 모든 기능이 non-blocking back pressure을 지원함Mono
, Flux
API 타입을 제공한다 WebFlux API
는 Reactor
에 대한 의존성이 필수적임Reactive Streams
을 통해 다른 reactive 라이브러리와도 상호운용적Publisher
를 받아 내부적으로 Reactor
type으로 적용해서 처리하고, Flux
/Mono
를 리턴함
[이미지 출처]
Spring MVC와 WebFlux는 함께 사용해도 됨
결국 경우에 따라 고려하면 됨
zip(): 주어진 입력 소스를 병합하는 메서드
문제: 본사(seller)와 지점(shop)이 1:N으로 구성될 때, 본사에 연결된 모든 지점의 정보를 가져온 뒤, 본사 정보와 지점 정보를 조합해 사용자에게 보여준다
과정:
fun getSeller(sellerId: String): Mono<Seller> = sellerRepository.findById(sellerId)
fun getShops(shopIds: Collection<String>): Flux<Shop> = shopRepository.findAllByIds(shopIds)
getSeller
Mono
Mono
getShops
Flux
Flux
본사 정보(i.e., 삼성스토어)와 지점 정보(i.e., 송파점)을 합치고자 한다
getShops(shopIds)
.zipWith(getSeller(sellerId))
.map { (shop, seller) -> "${seller.name} ${shop.name}" }
위처럼 코드를 작성하면 지점이 딱 한 개만 나오는 버그가 발생한다
왜냐?
zip()
연산자에 3개의 Publisher
를 넘기면, 각 Publisher
가 한개씩은 값을 내보내야 zip()
연산자도 한 묶음의 값을 내보내기 때문에 여러 값을 내보내는 Publisher
와 하나의 값을 내보내는 Publisher
를 zip하면 한개의 값만 받게 된다
파이프라인을 구성할 때 Flux
, Mono
를 반환하는 일련의 로직을 함수로 분리하자
val seller = sellerRepository.findById(sellerId)
val shops = shopRepository.findAllByIds(shopIds)
Mono.zip(
seller,
shops.collectList(),
)
보다는.. 아래 코드처럼!
fun getSeller(sellerId: String): Mono<Seller> = sellerRepository.findById(sellerId)
fun getShops(shopIds: Collection<String>): Flux<Shop> = shopRepository.findAllByIds(shopIds)
Mono.zip(
getSeller(sellerId),
getShops(shopIds).collectList(),
)
왜냐?
하지만 Mono
, Flux
를 캐싱해서 파이프라인 하나에서 여러 번 구독하여 사용하는 경우 (cache()
등을 통해)에는 변수를 추천
flatMap
을 통해 여러 값을 조합하면 중첩 flatMap
이 생길 가능성이 높기 때문에 Mono.zipWhen()
을 활용하자
shopRepository.findById(shopId)
.zipWhen { shop -> sellerRepository.findById(shop.sellerId) }
.map { (shop, seller) -> mapper.toDto(shop, seller)
}
Flux
, Mono
에 값이 없을 때 switchIfEmpty()
보다
shopRepository.findById(shopId)
.map { shop -> shop.name }
.switchIfEmpty("알 수 없는 지점".toMono())
defaultIfEmpty()
가 더 간결하다
shopRepository.findById(shopId)
.map { shop -> shop.name }
.defaultIfEmpty("알 수 없는 지점")
하나의 이벤트로 여러 처리를 실행할 때 flatMap()
을 사용하면, 서로 의존할 필요 없는 로직끼리 의존하게 되는 문제가 생긴다.
fun saveToDB(event: ProductEvent): Mono<ProductEvent> =
productDBRepository.save(toEntity(event))
.thenReturn(event)
fun saveToCache(event: ProductEvent): Mono<ProductEvent> =
productCacheRepository.save(toCacheObject(event))
.thenReturn(event)
fun process(event: ProductEvent): Mono<Void> =
saveToDB(event)
.flatMap { saveToCache(event) }
.then()
위처럼 작성한 코드는 누군가 리턴값을 지운다면 함수간 의존성 때문에 어느 로직은 절대 실행되지 않는 경우가 생길 수가 있음.
then()
and()
saveToDB(event).then(saveToCache(event))
saveToDB(event).and(saveToCache(event))
fun getSeller(sellerId: String): Mono<Seller> =
sellerRepository.findById(sellerId)
.doFirst { log.debug("셀러 조회 발생: {}", sellerId) }
References