[Spring R2DBC] Junit Test시 @Transactional 사용이 불가능한 문제 해결하기

1 minute read

Spring Webflux로 Geo Query를 수행하는 서비스를 만들다, Test를 작성하게 되었다.

응..? 근데 왜자꾸 에러가 나지..? 뭐야대체..

하면서 삽질했던 부분을 기록해 둔다.

다음 테스트를 보자

[PostgresqlGeoRepositoryTest.kt]

@SpringBootTest
@Transactional
internal class PostgresqlGeoRepositoryTest @Autowired constructor(
    private val postgresqlGeoRepository: GeoRepository<MemberCurrentPoint, Long>
){

    @Test
    fun noGeoInfo_findAroundPlaces_noResults(){

        var l = -1
        StepVerifier.create(
            Flux.fromIterable(
                getLngLatPoints().map {
                    l++
                    MemberCurrentPoint(l.toLong(),it.first, it.second)
                }
            ).flatMap {
                postgresqlGeoRepository.save(it)
            }.switchMap{
                postgresqlGeoRepository.findAroundOf(
                    MemberCurrentPoint(
                        0,
                        getLngLatPoints()[0].first,
                        getLngLatPoints()[0].second
                    )
                )
            }.map {
                it.memberId
            }
        ).verifyComplete()
    }
}

위 테스트를 실행하면 exception이 발생한다.

java.lang.IllegalStateException: Failed to retrieve PlatformTransactionManager for @Transactional test:...

찾아보니, paluch가 R2DBC의 경우 테스트에서 @Transactional을 통해 테스트 트랜잭션을 관리할 방법이 없음을 언급하고있다.

(https://github.com/spring-projects/spring-framework/issues/24226)

그리고 해당 이슈에서 힌트를 얻어, 불편하지만 매 단위 테스트마다 Transaction Operator를 달아 Rollback을 하는 수밖에 없었다.

Transaction Operator를 쉽게 관리하기 위해 Transaction Helper라는 클래스를 작성했다.

[TransactionHelper.kt]

..

@Component
class TransactionHelper (
    private val tx: TransactionalOperator
){
    fun getTransaction(): TransactionalOperator = tx

    fun <T> withRollback(publisher: Mono<T>) : Mono<T> {
        return tx.execute {
            it.setRollbackOnly()
            publisher
        }.next()
    }

    fun <T> withRollback(publisher: Flux<T>) : Flux<T> {
        return tx.execute {
            it.setRollbackOnly()
            publisher
        }
    }

    fun <T> executeMonoWithRollback(function: () -> Mono<T>) : Mono<T>{
        return tx.execute {
            it.setRollbackOnly()
            function.invoke()
        }.next()
    }

    fun <T> executeFluxWithRollback(function: () -> Flux<T>) : Flux<T>{
        return tx.execute {
            it.setRollbackOnly()
            function.invoke()
        }
    }

}

그런 다음 테스트 Operator들을 롤백구간으로 감싸준다.


    @Test
    fun noGeoInfo_findAroundPlaces_noResults(){

        var l = -1
        StepVerifier.create(
            transactionHelper.executeFluxWithRollback {
                Flux.fromIterable(
                    getLngLatPoints().map {
                        l++
                        MemberCurrentPoint(l.toLong(), it.first, it.second)
                    }
                ).flatMap {
                    postgresqlGeoRepository.save(it)
                }.switchMap {
                    postgresqlGeoRepository.findAroundOf(
                        MemberCurrentPoint(
                            0,
                            getLngLatPoints()[0].first,
                            getLngLatPoints()[0].second
                        )
                    )
                }.map {
                    it.memberId
                }
            }
        ).verifyComplete()
    }
    

이후 테스트를 진행하면 롤백이 잘 되는것을 볼 수 있다.

이유?

테스트가 아닌 Spring Context에서는 @Transactional이 잘 작동하지만, 왜 테스트에서는 사용이 불가능할까?

그 이유는 크게 두가지로 볼 수. 있다.

첫번째로, Reactive Opertator가 작동하는 방식이다.

기존 차단형 프로그래밍 방식에서 트랜잭션의 상태는 ThreadLocal을 이용해 관리되었다. 하지만 Reactive 프로그래밍에서는 비-차단 이며 단일 스레드로 동작해야하기에 ThreadLocal이라는 클래스는 사용할 수가 없다. (엄밀히 말하면 ThreadLocal의 존재 이유가 없다.)

이 문제를 해결하기 위해 R2DBC에서는 Context라는 객체를 이용하여 각 Flow에 대한 상태를 관리하는데,

문제는 현재 JUNIT에서 이러한 Context에 대한 지원이 완벽하지 않다는것이다.

두번째로, Assertion이다. Test Framework에서 트랜잭션 상태는 Assertion과 강한 커플링을 가진다. (Assertion 성공여부에 따라 트랜잭션이 롤백되거나 되지 않을수 있음을 생각해보면..) 하지만 이 Assertion은 차단을 유발하며, Reactive Flow중간에 이러한 차단 코드를 삽입할 수 없기 때문에, StepVerifier를 이용하여 테스트를 진행해야만 한다. 고로, 트랜잭션 상태를 관리할 수 없다.

..끝

(내가 잘못알고있다면 댓글 주세요..)

Leave a comment