[Spring] JPA 환경에서 테스트와 @Transaction 를 같이 사용할 때 주의점

김효권·2023년 5월 13일
post-thumbnail

인프런에서 자바로 된 스프링 프로젝트를 코틀린으로 마이그레이션 하는 강의를 듣던 중 겪은 문제이다. 서비스 단에 있는 코드를 테스트 하는데, 잘못된 부분이 없음에도 계속해서 테스트 실패가 되고있었다.

원하는 결과와 실제 결과가 다르다고 나온다!!

원인은 테스트 코드에 @Transaction 를 사용하는 것이 문제였다.
이처럼 테스트 코드에 @Transaction를 사용하게되면 테스트 종료시점에 자동으로 롤백해주기 때문에 직접 DB에 저장한 데이터를 직접 정리하는 작업을 해주지 않아도 되서 자주 사용하는 기능이었는데 여기서 문제가 발생하였다.

문제가 되는 코드

먼저 테스트 하려는 Service단의 코드는 다음과 같다.

    @Transactional(readOnly = true)
    fun getUserLoanHistories(): List<UserLoanHistoriesResponse> {
        val users = userRepository.findAll()
        return users.map { user ->
            UserLoanHistoriesResponse(
                name = user.name,
                books = user.userLoanHistories.map { book ->
                    BookHistoryResponse(
                        name = book.bookName,
                        isReturn = book.status == UserLoanStatus.RETURNED
                    )
                }
            )
        }
    }

User 엔티티와 UserLoanHistory1:N로 연관관계를 맺고 있다.
아래는 테스트 코드이다.

	@Transactional
	@Test
    @DisplayName("대출 기록이 많은 유저도 정상 응답된다.")
    fun getUserLoanHistoriesTest2() {

        // given
        val request = UserCreateRequest("hyo", null)
        val saveUser = userService.saveUser(request)
        userLoanHistoryRepository.saveAll(mutableListOf(
            UserLoanHistory("book1", saveUser, UserLoanStatus.RETURNED),
            UserLoanHistory("book2", saveUser, UserLoanStatus.LOANED),
        ))

        // when
        val result = userService.getUserLoanHistories()

        // then
        assertThat(result).hasSize(1)
        assertThat(result[0].name).isEqualTo("hyo")
        assertThat(result[0].books).extracting("name").containsExactlyInAnyOrder("book1", "book2")
        assertThat(result[0].books).extracting("isReturn").containsExactlyInAnyOrder(true, false)
    }

테스트 코드에는 테스트 완료 후 자동 rollback을 위한 @Transactioal이 사용되고 있다.

테스트가 실패하는 원인

JPA는 트랜잭션이 종료되고, 커밋되는 시점에 flush()가 호출되어서 실제 DB에 쿼리를 실행하게 된다. 따라서 userService.saveUser()userLoanHistoryRepository.saveAll() 메서드에 @Transactioanl 이 선언되어 있지만 테스트 코드에 @Transactional이 선언되어 있기때문에 트랜잭션이 상위로 전파되어 저장을 하고 난 뒤에도 트랜잭션이 커밋되지 않아 DB에 저장된 상태가 아닌, 여전히 영속성 컨택스트에 있는 상태이다.

따라서 userService.getUserLoanHistories() 메서드를 통해 User를 가져와도 DB에서 가져오는 것이 아닌 영속성 컨택스트에 존재하는 캐싱된 User를 가져오기 때문에 테스트가 실패했던 것이다.

또 다른 문제

만약에 개발자의 실수로 서비스 단에 있는 코드에 있는 @Transacational를 제거하면 어떻게 될까? 1:N 연관관계에서 Lazy Loading으로 데이터를 가져오기 때문에 프로덕션 환경에서는 User엔티티에서 userLoanHistories에 접근하려고 하는 순간 LazyInitailizationException 가 발생하기때문에 프로덕션 환경에서는 런타임에 예외가 날 것이다. 테스트 코드에는 @Transactional이 선언되어 있고, 서비스 단에는 선언되지 않은 경우에는 실제로는 실패하지만, 테스트는 성공하는.. 테스트 코드의 가치를 잃어버리는 현상이 발생된다.

결론

테스트 코드에서는 @Transactioanl 을 통해 데이터를 정리하는 것보다는 @BeforeEach, @AfterEach 와 같은 방법을 사용해서 데이터를 정리하자. 상황에 맞게 @Transactional 를 사용하자.

추가

토비님이 쓰신 답변중에 이런 내용도 있었다. 하단 링크 참조!
https://www.inflearn.com/questions/792383/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-transactional-%EC%82%AC%EC%9A%A9%EC%97%90-%EB%8C%80%ED%95%B4-%EC%A7%88%EB%AC%B8%EC%9D%B4-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4
테스트 코드에 @Transactional를 선언하는 것이 단점보다 장점이 압도적으로 많으니 위에서 작성한 문제가 발생할 수도 있음을 인식하고 주의해서 잘 작성을 하는 것이 좋을 것 같다..!

참고

https://www.youtube.com/watch?time_continue=331&v=S_66BYHWT2A&embeds_referring_euri=https%3A%2F%2Fwww.inflearn.com%2F&source_ve_path=MTM5MTE3LDEzOTExNywxMzkxMTcsMTM5MTE3LDEzOTExNywxMzkxMTcsMjg2NjY&feature=emb_logo

0개의 댓글