@DataJpaTest 주의사항

Park JeaHyun·2023년 4월 5일

정확하지 못한 정보는 댓글로 알려주시면 감사하겠습니다.

사전 지식

  • @DynamicInsert: 엔티티 인스턴스의 필드가 null일때 Hibernate insert 문에서 null값이 들어가지 않기 위해서는 @DynamicInsert 어노테이션이 필요하다
  • DB default 설정을 통해 insert 문에서 없는 컬럼은 기본 값으로 설정이 가능하다. 단, null값이 들어오면 null로 설정된다. (not null 제약이 없는 경우)

참고
[JPA] @DynamicInsert, @DynamicUpdate 실습

@DataJpaTest

JPA 관련 테스트를 진행하기 위해서 보통 @DataJpaTest을 많이들 사용한다. 이때 애플리케이션에서 DAO/DB 동작 환경과 @DataJpaTest 동작 환경이 약간 다르다. 이 차이점에 대해서 간략하게 정리해봤다.

애플리케이션에서는 되는데 테스트는 왜 안됨?

새로운 Task 데이터를 만들때 status라는 필드는 DB에 설정된 default를 가지도록 의도했다.

그러기 위해선 빌더로 새로운 엔티티를 만들때 status 필드는 값을 넣어주지 않는다. 따라서 status 필드는null 값이다. 그리고 이 엔티티를 DB에 저장할 때 dummyRepository.save(dummy); 코드를 실행하면 @DynamicInsert 덕분에 null 값을 가지는 필드는 insert 문에서 제외된다. insert 문에서 제외된 컬럼은 DB에서 설정된 default 값을 가지게 된다.

그렇게 스프링 애플리케이션을 실행하고 스웨거로 테스트 해보니 의도한대로 동작한다!

@DynamicInsert
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Dummy {

  @Id
  @GeneratedValue(
    strategy = GenerationType.SEQUENCE,
    generator = "dummy_seq")
  private Integer id;

  private String name;

  private String contents;

  @Enumerated(EnumType.STRING)
  @ColumnDefault("'READY'")
  private TaskStatus status;

  @Builder
  public Dummy(
    String name,
    String contents) {
    this.name = name;
    this.contents = contents;
  }
}

하지만 문제는 테스트에서 발생했다. dummySaved.getStatus()null이라서 테스트 실패

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class DummyRepositoryTest {

  @Autowired
  private TestEntityManager entityManager;
  @Autowired
  private DummyRepository dummyRepository;

  @Test
  @DisplayName("엔티티를 DB에 저장")
  public void create() throws Exception {
    // given
    Dummy dummy = Dummy.builder()
      .name("테스트")
      .contents("테스트 내용")
      .build();

    // when
    Dummy dummySaved = dummyRepository.save(dummy);

    // then
    assertEquals(dummy.getName(), dummySaved.getName());
    assertEquals(dummy.getContents(), dummySaved.getContents());
    assertEquals(TaskStatus.READY, dummySaved.getStatus());
  }
}

@DynamicInsert가 소용없는 건가...라고 생각했지만 @DataJpaTest의 동작 원리를 조금 이해해야 했다.

참고

위 글들을 통해 @DataJpaTest의 동작 원리가 내가 생각한 것과 다르다는 것을 알았다. @DataJpaTest에서 dummyRepository.save(dummy);는 단순히 Hibernate First Level Cache(EntityManager라고 생각하겠음)에만 저장되는 것이다. 그렇기 때문에 실제 DB에 insert 문이 날라가지 않는 것이다.

@DynamicInsertinsert문을 만들 때 null값을 제거하기 때문에 캐시에 저장하는 것은 @DynamicInsert가 애초에 동작하지 않는 것이다.

실제 디버그 콘솔에서도 insert문은 보이지 않는다.

그럼 실제 DB에 저장하려면 어떻게 해야하지?

@DataJpaTest을 사용하면 TestEntityManager를 주입받을 수 있다. TestEntityManager을 통해 위에서 언급한 Hibernate First Level Cache에 저장된 엔티티 인스턴스들을 실제 DB에 저장할 수 있다.

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class DummyRepositoryTest {

  @Autowired
  private TestEntityManager entityManager;
  @Autowired
  private DummyRepository dummyRepository;

  @Test
  @DisplayName("엔티티를 DB에 저장")
  public void create() throws Exception {
    // given
    Dummy dummy = Dummy.builder()
      .name("테스트")
      .contents("테스트 내용")
      .build();

    // when
    Dummy dummySaved = dummyRepository.save(dummy);

    entityManager.flush();

    // then
    assertEquals(dummy.getName(), dummySaved.getName());
    assertEquals(dummy.getContents(), dummySaved.getContents());
    assertEquals(TaskStatus.READY, dummySaved.getStatus());
  }
}

이제야 비로서 insert문이 날라간다. 즉, DB에 default status 값을 가진 dummy 인스턴스가 1개 만들어졌다. 그럼 이제 ID로 조회하면 status 값을 가진 dummy를 조회할 수 있을까?

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class DummyRepositoryTest {

  @Autowired
  private TestEntityManager entityManager;
  @Autowired
  private DummyRepository dummyRepository;

  @Test
  @DisplayName("엔티티를 DB에 저장")
  public void create() throws Exception {
    // given
    Dummy dummy = Dummy.builder()
      .name("테스트")
      .contents("테스트 내용")
      .build();

    // when
    Dummy dummySaved = dummyRepository.save(dummy);
    Integer id = dummySaved.getId();

    entityManager.flush();

    Dummy dummyFound = dummyRepository.findById(id).get();

    // then
    assertEquals(dummy.getName(), dummyFound.getName());
    assertEquals(dummy.getContents(), dummyFound.getContents());
    assertEquals(TaskStatus.READY, dummyFound.getStatus());
  }
}

여전히 null이다. dummyRepository.findById를 실행하게 되면 여전히 캐시에 있는 task를 반환하게 된다. (요청한 id 값을 가지는 엔티티 인스턴스가 캐시에 있다면 DB까지 가지 않고 캐시의 인스턴스를 반환함) 이때 캐시에 있는 dummy와 실제 DB에 저장된 dummy 사이에 불일치가 발생한다. (각 dummyid는 같음)

이를 해결하기 위해서 TestEntityManager를 비우고 다시 DB에서 조회해보면...

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class DummyRepositoryTest {

  @Autowired
  private TestEntityManager entityManager;
  @Autowired
  private DummyRepository dummyRepository;

  @Test
  @DisplayName("엔티티를 DB에 저장")
  public void create() throws Exception {
    // given
    Dummy dummy = Dummy.builder()
      .name("테스트")
      .contents("테스트 내용")
      .build();

    // when
    Dummy dummySaved = dummyRepository.save(dummy);
    Integer id = dummySaved.getId();

    entityManager.flush();
    entityManager.clear();

    Dummy dummyFound = dummyRepository.findById(id).get();

    // then
    assertEquals(dummy.getName(), dummyFound.getName());
    assertEquals(dummy.getContents(), dummyFound.getContents());
    assertEquals(TaskStatus.READY, dummyFound.getStatus());
  }
}

드디어 default status 값을 가진 dummy 인스턴스를 확인할 수 있다.

0개의 댓글