[Spring] ReactiveCrudRepository를 이용한 삭제 시 주의점
😟 문제 상황
찜을 삭제하고 별도의 작업을 위해 event publish 하는 로직을 작성하였다.
fun remove(favorite: Favorite): Mono<Void> =
favoriteRepository.deleteById(favorite.id)
.flatMap { productService.findById(favorite.productId) } // event publish할 때 필요한 데이터 조회
.flatMap { product ->
val favoriteRequest = toFavoriteEventRequest(favorite, product)
Mono.fromCallable { favoriteEventPublisher.publish(favoriteRequest) }
}.then()
참고로 FavoriteRepository는 아래와 같이 ReactiveCrudRepository를 구현하여 만들었다.
@Repository
interface FavoriteRepository : ReactiveCrudRepository<Favorite, Long> { ... }
헌데 문제가 발생했다. DB 삭제는 잘 동작하는데 event publish 하는 로직은 호출조차 안 됐다.
🤔 원인 파악
flatMap이 동작하지 않았다는 것은 여러 가지를 의심해 볼 수 있다.
- 스트림이 오류로 인해 중단되었다. (ex: 이전 단계에서 예외가 발생했다던가 등)
- 아이템을 받지 못했다.
일단 API가 정상적으로 응답했었고 로그에 어떠한 에러 로그도 남지 않았으므로 1의 상황은 아니라고 생각했다. 그러면 2의 상황일까? 혹시 favoriteRepository.deleteById()의 결과가 비어있나?
ReactiveCrudRepository의 deleteById 메서드를 살펴보자. 아래는 메서드의 주석을 그대로 가져왔다.
Deletes the entity with the given id.
If the entity is not found in the persistence store it is silently ignored.
Params: id – must not be null.
Returns: Mono signaling when operation has completed.
Throws: IllegalArgumentException – in case the given id is null.
여기서 Returns 부분을 주목해 보자. 연산이 완료되면 Mono가 signaling 한다고 표현되어 있다. 여기서 내가 영알못+리액터알못 이슈로 착각한 부분이 '연산적으로 완료되면 Mono에 방출될 수 있는 값이 담긴 상태로 반환한다'는 의미라고 생각했다. 저 주석은 연산이 성공적으로 완료되었을 때 Mono라는 신호가 발생한다는 의미일 뿐, 방출될 수 있는 값이 반드시 존재한다는 말이 아니다.
실제 구현을 살펴보자. ReactiveCrudRepository는 인터페이스일 뿐이라, deleteById의 로직을 보고 싶다면 구현체를 찾아야 한다. 현재 찜 데이터 저장에는 PostgreSQL을 사용하고 있으므로 기본적으로 SimpleR2dbcRepository라는 구현체를 사용한다. (MongoDB를 사용한다면 SimpleReactiveMongoRepository, Redis를 사용한다면 ReactiveRedisRepository 등의 구현체를 살펴보면 된다.)
아래는 SimpleR2dbRepository의 deleteById() 코드이다.
@Override
@Transactional
public Mono<Void> deleteById(ID id) {
Assert.notNull(id, "Id must not be null");
return this.entityOperations.delete(getIdQuery(id), this.entity.getJavaType()).then();
}
반환할 때 then()을 이용해 값을 반환한 것을 볼 수 있다. Mono의 then()에 대한 설명을 읽어보자.
Return a Mono<Void> which only replays complete and error signals from this Mono.
Mono<Void>를 반환한다. 이 Mono는 값을 방출하지 않고 오직 완료/오류 신호만 나타내는 Mono를 반환한다. favoriteRepository.deleteById()가 비어있는 Mono를 반환한다는 것을 확인한 셈이다.
😎 해결 방법
deleteById가 Mono.empty를 반환한다는 것을 감안하여 코드를 수정하면 된다. 해결 방법은 어떤 연산자를 사용하냐에 따라 정말 다양한 결과를 만들 수 있다. 가장 단순해 보이는 코드로 수정해 보았다.
방법 1. event publish 로직 먼저 호출
fun remove(favorite: Favorite): Mono<Void> =
productService.findById(favorite.productId)
.doOnNext { product ->
val favoriteRequest = toFavoriteEventRequest(favorite, product)
favoriteEventPublisher.publish(favoriteRequest)
}.flatMap { favoriteRepository.deleteById(favorite.id) }
방법 2. then(Mono<V> other) 사용
fun remove(favorite: Favorite): Mono<Void> =
favoriteRepository.deleteById(favorite.id)
.then(productService.findById(favorite.productId))
.doOnNext { product ->
val favoriteRequest = toFavoriteEventRequest(favorite, product)
favoriteEventPublisher.publish(favoriteRequest)
}.then()