Spring Data JPA를 쓰고 있는 Repository 계층의 단위 테스트를 작성할 때 겪은 오류이다.
1. 오류 발생 상황
전체 코드
@DataJpaTest
public class ResponseExampleRepositoryTest {
@Autowired
ResponseExampleRepository responseExampleRepository;
private List<ResponseExample> mockResponseExamples;
@BeforeEach
void setUp() {
mockResponseExamples = List.of(
createExample("피드뷰 - 예시 프롬프트1", PictureRatio.RATIO_SERO, null, FALSE),
createExample("피드뷰 - 예시 프롬프트2", PictureRatio.RATIO_SERO, null, FALSE),
createExample("피드뷰 - 예시 프롬프트3", PictureRatio.RATIO_SERO, null, FALSE),
// ..(중략)
);
responseExampleRepository.saveAll(mockResponseExamples);
}
@Test
@DisplayName("피드뷰 - 조건에 맞는 예시가 없을 때 빈 리스트가 반환되는지 검증")
void findAllFeedViewWhenNoMatchingData_Then_Return_emptyList() {
//given
responseExampleRepository.deleteAll();
responseExampleRepository.saveAll(mockResponseExamples.subList(10, mockResponseExamples.size()));
//when
List<ResponseExample> result = responseExampleRepository.findAllByPromptOnlyIsFalse();
//then
assertThat(result).isEmpty();
}
}
오류 메세지
- org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.ObjectDeletedException: deleted instance passed to merge
2. 오류 분석
코드 오류 부분
@Test
@DisplayName("피드뷰 - 조건에 맞는 예시가 없을 때 빈 리스트가 반환되는지 검증")
void findAllFeedViewWhenNoMatchingData_Then_Return_emptyList() {
responseExampleRepository.deleteAll();
responseExampleRepository.saveAll(mockResponseExamples.subList(10, mockResponseExamples.size()));
// ..(중략)
}
오류 분석 요약
- Hibernate의 merge()는 삭제 상태 객체를 처리할 수 없다.
- merge()하려고 할 때 삭제된 객체가 포함되어 있다면 오류(ObjectDeletedException)를 던진다.
- merge()하려고 할 때 삭제된 객체가 포함되어 있다면 오류(ObjectDeletedException)를 던진다.
- deleteAll()을 호출한 이후에 삭제된 객체를 saveAll()로 다시 저장하려고 해서 오류가 발생한 것이다.
- 테스트 메서드의 saveAll()에서 인자로 넣어준 객체들이 setUp 메서드의 saveAll()에서 인자로 넣어준 객체들과 같은 객체이기 때문이다.
- saveAll()은 내부적으로 merge()를 사용한다.
오류 분석 상세
-
@DataJpaTest 어노테이션이 테스트 클래스에 붙어 있기 때문에, 각 테스트 메서드는 트랜잭션 내에서 실행된다. 또한, 각 테스트 메서드는 테스트 클래스가 실행되는 동안 하나의 트랜잭션 범위 내에서 진행된다.
- @Transactional 어노테이션이 기본적으로 사용하는 전파 방식은 Propagation.REQUIRED이다. REQUEST 옵션은 현재 트랜잭션이 없으면 새로 시작하고, 이미 트랜잭션이 있으면 해당 트랜잭션에 참여하는 동작을 한다.
- 테스트 메서드는 @Transcational을 사용하고 있으며, deleteAll()과 saveAll()를 호출하고 있다. 이때, deleteAll()과 saveAll()은 모두 내부적으로 @Transactional 어노테이션을 사용하고 있는데, 이 메서드들을 호출하는 테스트 메서드에서 트랜잭션이 존재하기 때문에 두 메서드 모두 새로운 트랜잭션을 시작하지 않고 이미 존재하는 트랜잭션(테스트 메서드 것)에 참여한다.
- 테스트 메서드는 @Transcational을 사용하고 있으며, deleteAll()과 saveAll()를 호출하고 있다. 이때, deleteAll()과 saveAll()은 모두 내부적으로 @Transactional 어노테이션을 사용하고 있는데, 이 메서드들을 호출하는 테스트 메서드에서 트랜잭션이 존재하기 때문에 두 메서드 모두 새로운 트랜잭션을 시작하지 않고 이미 존재하는 트랜잭션(테스트 메서드 것)에 참여한다.
- 스프링은 기본으로 트랜잭션 범위의 영속성 컨텍스트 전략을 사용한다. 이는 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료한다는 의미이다. 또한, 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.
- 테스트 메서드 내의 deleteAll()과 saveAll()은 같은 트랜잭션(테스트 메서드 것)에 참여하고 있으므로 같은 영속성 컨텍스트를 사용한다.
- 테스트 메서드 내의 deleteAll()과 saveAll()은 같은 트랜잭션(테스트 메서드 것)에 참여하고 있으므로 같은 영속성 컨텍스트를 사용한다.
- 결론 - 오류의 원인
- deleteAll()과 saveAll()이 같은 영속성 컨텍스트를 사용하고 있다.
- 1번의 이유만으로는 오류가 발생하진 않는다. 그러나 나는 1번의 상황 속에서 deleteAll()에서 삭제된 객체를 saveAll()에서 사용하기 때문에 오류가 발생한 것이다.
3. 오류 해결
방법 1 : 삭제된 객체를 다시 저장하지 않고, 새로운 객체를 생성하여 저장
void findAllFeedViewWhenNoMatchingData_Then_Return_emptyList() {
// given
responseExampleRepository.deleteAll();
List<ResponseExample> newObjects = mockResponseExamples.subList(10, mockResponseExamples.size())
.stream()
.map(example ->
createExample(example.getExamplePrompt(),
example.getPictureRatio(),
example.getType(),
example.getPromptOnly())) // 새 객체 생성
.collect(Collectors.toList());
responseExampleRepository.saveAll(newObjects);
// ...(동일)
}
방법 2 : 영속성 컨텍스트 초기화
@PersistenceContext
private EntityManager entityManager;
void findAllFeedViewWhenNoMatchingData_Then_Return_emptyList() {
// given
responseExampleRepository.deleteAll();
entityManager.flush(); // DB와 동기화
// flush()로 데이터베이스에 반영된 변경 사항도 테스트가 끝난 후 롤백되므로
// 최종적으로 데이터는 삭제되지 않은 상태로 유지된다!
entityManager.clear(); // 영속성 컨텍스트 초기화
responseExampleRepository.saveAll(mockResponseExamples.subList(10, mockResponseExamples.size()));
// ...(동일)
}
- 참고 자료
https://stackoverflow.com/questions/18358407/org-hibernate-objectdeletedexception-deleted-object-would-be-re-saved-by-cascad
https://stackoverflow.com/questions/31335211/autowired-vs-persistencecontext-for-entitymanager-bean
https://www.inflearn.com/community/questions/527211/%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EC%99%80-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-propagation?srsltid=AfmBOordQwcCFu6HJkovFm-_NBBIR6DOSjiUAWJYAvy0H_xmAGEZMWnG
https://milenote.tistory.com/107
https://insanelysimple.tistory.com/314
ChatGPT