[테스트 격리] jpa repository deleteAll unable to find entity

nathan·2022년 12월 19일
3

TroubleShoot

목록 보기
1/1

Trouble Shoot


1. 배경 및 문제 상황

Card는 여러 개의 Content를 가질 수 있다.

  • Content@ManyToOne 연관관계를 사용하여, Card와 N:1 연관관계를 갖는다.
  • 현재 @ManyToOne에는 어떠한 옵션도 들어가 있지 않다. 따라서 default fetch 옵션인 FetchType.EAGER 상태이다.
  • Card 조회 테스트에서는 해당 카드의 아이디를 가진 Content들이 List 형태로 함께 조회된다.

현재 Card 조회 테스트에는 두 개의 테스트가 존재한다.

  • Card 단 건 조회
  • Card 전체 조회
  • 각 테스트를 실행하기 전 init() 메서드를 @BeforeEach를 통해 실행한다.
    • init() 메서드의 코드는 다음과 같다.
      @BeforeEach
      	void init() {
      		contentRepository.deleteAll();
      		createNotesAndCardsAndContents(NUMBER_OF_CARD); // test에 필요한 데이터 생성
      }
  • 또한, @sql(truncate.sql)를 클래스 레벨에서 사용함으로써 현재 테스트 클래스(=CardFindTest)에 존재하는 테스트를 수행할 시, 매 번 note tablecard table에 대한 truncate를 진행하고 있다.
    • truncate.sql은 다음과 같다.
      SET FOREIGN_KEY_CHECKS = 0; // 외래키 제약조건으로 인해 TRUNCATE가 제대로 되지 않을 수 있으므로 선언
      TRUNCATE TABLE note;
      TRUNCATE TABLE card;
      SET FOREIGN_KEY_CHECKS = 1; // 외래키로 제약조건을 다시 원상태로 되돌림
    • 이를 통해 알 수 있는 점은, 현재 content table에 대한 truncate는 진행되고 있지 않다는 점이다.
    • 참고 블로그 : @SpringBootTest의 테스트 격리시키기(TestExecutionListener)
  • 테스트는 각각 실행 했을 때 문제가 발생하지 않았다. 그러나, 두 개의 테스트를 동시에 돌릴 때 다음과 같은 에러가 발생했다.
    org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find dev.whatevernote.be.service.domain.Card with id 1; nested exception is javax.persistence.EntityNotFoundException: Unable to find dev.whatevernote.be.service.domain.Card with id 1




2. 문제 원인

TRUNCATE 말고 deleteAll()을 사용해서..?

  • 위에서의 truncate.sql을 보면 알겠지만, note tablecard table에 대해서만 truncate를 진행하고 있다.
  • ContentCard의 id 값을 FK로 가지고 있다.
  • ContentRepository에서 제공되는 deleteAll() 메서드를 수행해서 content table을 비우려고 했다.
  • 여기서 Spring Data JPA가 제공하는 deleteAll() 메서드는 cascade 관계(부모-자식 관계)를 파악하여 자식 객체들을 모두 조회 후에 제거하는 작업을 수행한다.
    • 실제로 deleteAll()을 수행하였더니 다음과 같은 select 쿼리문을 날리는 것이 확인되었다.
      select card0_.id as id1_0_0_, card0_.created_at as created_2_0_0_, card0_.deleted as deleted3_0_0_, card0_.updated_at as updated_4_0_0_, card0_.note_id as note_id7_0_0_, card0_.card_order as card_ord5_0_0_, card0_.title as title6_0_0_, note1_.id as id1_2_1_, note1_.created_at as created_2_2_1_, note1_.deleted as deleted3_2_1_, note1_.updated_at as updated_4_2_1_, note1_.note_order as note_ord5_2_1_, note1_.title as title6_2_1_ from card card0_ left outer join note note1_ on card0_.note_id=note1_.id where card0_.id=? and ( card0_.deleted = 0)
  • 따라서 ContentRepository에 대하여 deleteAll()을 수행하면, 이미 card table은 truncate가 되어 데이터가 존재하지 않는데, deleteAll()은 casecade 관계를 파악할 때 부모 Entity 데이터가 존재하지 않아 Unable to find dev.whatevernote.be.service.domain.Card with id 1와 같은 에러가 뜬 것이다.




3. 해결 방안

해결 방법 (1) : TRUNCATE TABLE

  • deleteAll() 메서드를 사용하지 않고, content table에 대해서도 truncate를 해주는 것으로 문제를 해결하였다.

해결 방법 (2) : @ManyToOne(fetch = Fetch.LAZY)

  • 현재 ContentCard@ManyToOne 어노테이션을 통해 연관관계를 유지한다.
  • deleteAll() 메서드가 해당 도메인에 대한 cascade 관계를 파악하는데, 문제의 핵심은 cascade를 확인하는 과정에서 연관관계에 대한 fetch를 진행하게 되는데, @ManyToOne, @OneToOne의 경우 default strategy가 EAGER이다.
  • 따라서 즉시 로딩이 되어 card가 있는지 없는지 여부를 판단하는데, 이 때, 이미 card table은 truncate 되어 데이터가 더 이상 존재하지 않으므로, Unable to find dev.whatevernote.be.service.domain.Card with id 1 가 떴던 것이다.
  • fetch strategy를 LAZY로 바꾸게 되면, 실제 Card가 사용되기 전까지 조회를 진행하지 않는다. 따라서 Content를 무사히 모두 삭제할 수 있게 된다.
  • 그러나 이런 방식으로 해결이 가능하다고 하더라도, 사실 fetch strategy와 상관없이 deleteAll() 메서드는 해당 도메인에 대한 cascade 관계 파악 후 테이블의 모든 데이터를 조회한다는 측면에서 성능상으로 불리하다는 단점이 있었다.


오랜만에 팀 프로젝트를 건들면서, truncate의 사실을 인지하지 못했기 때문에 발생한 일이라고 생각된다.

그래도 문제 원인을 분석하고, 해결하는 과정에서 개발의 또 다른 즐거움을 얻을 수 있었다.

profile
나는 날마다 모든 면에서 점점 더 나아지고 있다.

0개의 댓글