인프런 커뮤니티 질문&답변

yoongdoo0819님의 프로필 이미지
yoongdoo0819

작성한 질문수

실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기 (Java 프로젝트 리팩토링)

@AfterEach 대신 @Transactional 사용 시 오류

해결된 질문

작성

·

407

·

수정됨

1

안녕하세요 강사님, 좋은 강의 제공해주셔서 감사합니다 :)

 

"9강. 책 관련 기능 테스트 작성하기" 강의를 듣던 도중 궁금한 점이 생겨 이렇게 질문을 드립니다.

 

해당 강의에서 @AfterEach()를 사용하며, 아래와 같이 returnBookTest()를 테스트합니다.

@AfterEach를 사용했을 경우, 위와 같이 정상적으로 테스트를 통과합니다.

 

그러나, @AfterEach 대신 @Transacctional을 사용할 경우, 동일한 테스트에서 다음과 같이 실패합니다.

@Transactional은 테스트 케이스 종료 후 db를 롤백시킨다고 알고 있습니다. 그러므로 @AfterEach 대신 @Transactional을 사용해도 잘 돌아갈 것으로 예상하고 돌려봤으나, 테스트에 실패한다고 떴습니다.
(@Transactional을 사용 시, BookServiceTest class를 open class BookServiceTest로 수정했습니다)

 

왜 @AfterEach 대신 @Transactional을 사용했을 경우 해당 상황에서 실패하는지 알 수 있을까요?

답변 1

0

최태현님의 프로필 이미지
최태현
지식공유자

안녕하세요, yoongdoo0819님! 정말 좋은 질문 감사드립니다! 😊

결론부터 말씀드리면, @Transactional 을 사용하면, BookService.returnBook() 의 트랜잭션 경계가

  • 프로덕션 코드를 돌릴 때

  • 테스트 코드를 돌릴 때

달라지기 때문입니다!

 

보다 자세히 설명드리기 위해 @Transactional 어노테이션을 사용할 때 / 사용하지 않을 때 흐름도를 간략히 설명드리겠습니다.

 

[@Transactional 을 사용하지 않을 때]

  1. 테스트 코드 시작!

  2. 데이터를 준비하고~ (given 절) 이때 트랜잭션이 없으므로, SimpleJpaRepository.save() 에 있는 @Transactional 에 의해 바로바로 DB에 반영된다.

  3. BookService.returnBook() 호출! (when 절)

  4. 자 그럼 이제 returnBook() 에 트랜잭션이 있으니 트랜잭션이 시작됩니다.

  5. 트랜잭션이 새로 시작되었으니, 완전히 격리된 환경에서 User 를 가져온다. 이때 DB에 반영되어 있는 형태 그대로 데이터를 가져오므로 UserUserLoanHistory 가 잘 연결되어 있다!!

  6. returnBook() 로직 완료! DB 데이터 반영 후 트랜잭션 종료!

  7. 테스트 코드로 돌아와 then 절 호출

  8. DB에도 데이터가 반영되어 있으니 then절이 정상 통과합니다.

 

[@Transactional 을 사용할 때]

  1. 테스트 코드 시작! 그리고 동시에 트랜잭션 시작!

  2. 데이터를 준비하고~ (given 절)

  3. BookService.returnBook() 호출! (when 절)

  4. 이때 returnBook() 에 트랜잭션이 있지만, 트랜잭션이 중첩되어 있으므로 무시됩니다. 즉 여전히 1번에서 시작된 트랜잭션 입니다.

  5. returnBook() 로직에서 이제 User를 가져올 건데, 이때 1번과 같은 트랜잭션에서 User 를 가져오게 됩니다! 즉, 격리된 환경에서 User 를 가져오는게 아니기 때문에 아까 저장했던 User 객체 그대로 데이터를 가져오게 되고 이 User 객체는 UserLoanHistory와 연결되어 있지 않습니다!

  6. returnBook() 로직을 수행하려 하지만, UserLoanHistory가 없기 때문에 No Such Element 에러가 발생하게 됩니다.

 

실제로 [@Transactional 을 사용할 때] - 5번 과정 을 확인해보시려면,

@Transactional
fun returnBook(request: BookReturnRequest) {
  val user = userRepository.findByName(request.userName) ?: fail()
  println(user.userLoanHistories.size) // 연결 확인!!
  user.returnBook(request.bookName)
}

UserLoanHistory 의 개수를 출력해보실 수 있습니다!

 

그렇다면, @Transactional 과 테스트를 함께 사용하려면 어떻게 해야 할까요?!!

정답은, User 객체와 UserLoanHistory 를 직접 이어주는 것입니다!

// given
val savedUser = userRepository.save(User("최태현", null))
val history = userLoanHistoryRepository.save(UserLoanHistory.fixture(savedUser, "이상한 나라의 엘리스"))
savedUser.userLoanHistories.add(history) // 직접 연결해주기!

이렇게 두 객체를 연결해주시면, 같은 트랜잭션 내에서 savedUser 를 가져오더라도 UserUserLoanHistory 가 연결된 채 가져오게 될 거에요!!

 

또 궁금한 점 있으시면, 편하게 질문 올려주세요!! 감사합니다!! 😊

yoongdoo0819님의 프로필 이미지
yoongdoo0819
질문자

아하.. 정성스러운 답변 너무나도 감사합니다!

 

저는 같은 트랜잭션 내에서 User, UserLoanHistory를 저장하므로 영속성 컨텍스트 1차 캐시에 의해 User와 UserLoanHistory가 연결되어 관리된다고 생각했는데, 그게 아니었네요..!

 

연결해주는 로직이 없었는데, JPA가 많은 부분을 관리 해주다보니 잘못 생각했던 것 같습니다.

 

다음에도 궁금한 점이 생기면 또 질문 드리겠습니다 ㅎㅎ

 

앞으로도 좋은 강의 부탁 드립니다 :)

감사합니다.

yoongdoo0819님의 프로필 이미지
yoongdoo0819

작성한 질문수

질문하기