Repository 테스트를 할 때 서비스 로직은 신경 쓰고 싶지 않은데, 전체 애플리케이션을 띄워야 할까요?

@DataJpaTest는 JPA 관련 빈만 로드하는 슬라이스 테스트입니다. Repository 계층을 격리해서 테스트할 수 있고, 기본적으로 임베디드 DB를 사용하여 빠르게 실행됩니다.

@DataJpaTest란

@DataJpaTest는 JPA 관련 컴포넌트만 로드하는 슬라이스 테스트 어노테이션입니다.

로드되는 것:

  • @Entity 클래스
  • JpaRepository 인터페이스
  • EntityManager, DataSource
  • Flyway/Liquibase 마이그레이션 (설정에 따라)

로드되지 않는 것:

  • @Service, @Controller, @Component
  • Security, Web 관련 빈

내부적으로 @Transactional을 포함하고 있어 각 테스트 메서드 후 자동 롤백 됩니다.

기본 사용법

JAVA
@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private TestEntityManager em;  // 테스트용 EntityManager

em.clear()로 1차 캐시를 초기화한 후 조회해야 실제 DB 쿼리가 실행되는지 검증할 수 있습니다.

JAVA
    @Test
    void 상품_저장_및_조회() {
        // given
        Product product = Product.builder()
                .name("스프링 부트")
                .price(35000)
                .category("IT")
                .build();

        em.persistAndFlush(product);  // 저장 + flush
        em.clear();                    // 영속성 컨텍스트 초기화

        // when
        Product found = productRepository.findById(product.getId()).orElseThrow();

        // then
        assertThat(found.getName()).isEqualTo("스프링 부트");
        assertThat(found.getPrice()).isEqualTo(35000);
    }
}

TestEntityManager

TestEntityManager는 테스트에서 엔티티를 다루기 위한 편의 래퍼입니다.

JAVA
@Autowired
private TestEntityManager em;

// 저장
Product saved = em.persist(product);        // persist
em.flush();                                  // flush
em.persistAndFlush(product);                // persist + flush
em.persistFlushFind(product);              // persist + flush + find (새로 조회)

// 조회
Product found = em.find(Product.class, 1L);

// 영속성 컨텍스트 초기화 (1차 캐시 클리어)
em.clear();

em.clear()는 중요한 습관입니다. 클리어하지 않으면 1차 캐시에서 가져오기 때문에, 실제 DB 쿼리가 실행되는지 확인할 수 없습니다.

커스텀 쿼리 메서드 테스트

JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {

    List<Product> findByCategoryOrderByPriceDesc(String category);

    @Query("SELECT p FROM Product p WHERE p.price BETWEEN :min AND :max")
    List<Product> findByPriceRange(@Param("min") int min, @Param("max") int max);

    @Query(value = "SELECT * FROM products WHERE MATCH(name) AGAINST(:keyword)",
           nativeQuery = true)
    List<Product> fullTextSearch(@Param("keyword") String keyword);
}
JAVA
@Test
void 카테고리별_가격_내림차순_조회() {
    // given
    em.persistAndFlush(Product.builder().name("A").category("IT").price(10000).build());
    em.persistAndFlush(Product.builder().name("B").category("IT").price(30000).build());
    em.persistAndFlush(Product.builder().name("C").category("문학").price(20000).build());
    em.clear();

    // when
    List<Product> results = productRepository.findByCategoryOrderByPriceDesc("IT");

    // then
    assertThat(results).hasSize(2);
    assertThat(results.get(0).getPrice()).isEqualTo(30000);  // 비싼 것이 먼저
    assertThat(results.get(1).getPrice()).isEqualTo(10000);
}

@Query로 작성한 JPQL 쿼리도 같은 방식으로 테스트합니다.

JAVA
@Test
void 가격_범위_조회() {
    // given
    em.persistAndFlush(Product.builder().name("A").price(5000).build());
    em.persistAndFlush(Product.builder().name("B").price(15000).build());
    em.persistAndFlush(Product.builder().name("C").price(25000).build());
    em.clear();

    // when
    List<Product> results = productRepository.findByPriceRange(10000, 20000);

    // then
    assertThat(results).hasSize(1);
    assertThat(results.get(0).getName()).isEqualTo("B");
}

임베디드 DB vs 실제 DB

임베디드 DB (기본)

@DataJpaTest는 기본적으로 H2 같은 임베디드 DB를 사용합니다.

XML
<!-- 테스트용 H2 의존성 -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

장점:

  • 별도 DB 설치/설정 불필요
  • 테스트 속도가 빠름
  • CI 환경에서 바로 실행 가능

** 단점:**

  • MySQL/PostgreSQL과 SQL 문법 차이
  • DB 고유 함수, 인덱스 동작을 테스트할 수 없음
  • 네이티브 쿼리 호환성 문제

실제 DB 사용

JAVA
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
class ProductRepositoryTest {
    // 실제 DB를 사용하여 테스트
}
YAML
# application-test.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test_db
    username: test
    password: test
  jpa:
    hibernate:
      ddl-auto: create-drop  # 테스트 시작 시 테이블 생성, 종료 시 삭제

실무에서는 Testcontainers와 함께 사용하는 것이 더 좋습니다 (별도 포스트 참고).

트랜잭션 롤백 동작

@DataJpaTest에는 @Transactional이 포함되어 있어 각 테스트 메서드 후 자동 롤백됩니다.

JAVA
@Test
void 저장_후_다른_테스트에_영향_없음() {
    productRepository.save(Product.builder().name("테스트").price(1000).build());
    // 이 테스트 끝나면 자동 롤백 → 다른 테스트에 영향 없음
}

롤백이 문제가 되는 경우

JAVA
@Service
public class ProductService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createWithNewTransaction(Product product) {
        productRepository.save(product);
        // REQUIRES_NEW: 새 트랜잭션이 열리고 커밋됨
    }
}

테스트의 @Transactional 롤백이 REQUIRES_NEW로 열린 트랜잭션에는 영향을 주지 않습니다. 이런 경우 테스트 간 격리를 위해 @AfterEach에서 직접 데이터를 정리해야 합니다.

롤백 비활성화

실제로 커밋되는 것을 확인하고 싶다면 롤백을 비활성화할 수 있습니다.

JAVA
@Test
@Rollback(false)  // 또는 @Commit
void 커밋_확인용_테스트() {
    productRepository.save(Product.builder().name("커밋됨").build());
    // 실제 DB에 커밋됨 — 주의: 테스트 후 수동 정리 필요
}

Auditing 테스트

@CreatedDate, @LastModifiedDate 같은 JPA Auditing을 테스트하려면 @EnableJpaAuditing이 필요합니다.

JAVA
@DataJpaTest
@EnableJpaAuditing
class AuditingTest {

    @Autowired
    private TestEntityManager em;

    @Autowired
    private ArticleRepository articleRepository;

    @Test
    void 생성일자_자동_설정() {
        Article article = Article.builder().title("테스트").build();
        em.persistAndFlush(article);

        assertThat(article.getCreatedAt()).isNotNull();
        assertThat(article.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now());
    }
}

실무 팁

  • em.clear()를 적극적으로 사용하여 1차 캐시 영향을 제거하세요
  • 테스트 데이터는 ** 각 테스트 메서드 안에서** 생성하세요 (@BeforeEach보다 가독성이 좋음)
  • 네이티브 쿼리나 DB 고유 기능은 ** 실제 DB나 Testcontainers**로 테스트하세요
  • @DataJpaTest에서는 ** 쿼리 정확성 **에 집중하고, 비즈니스 로직은 서비스 테스트에서 검증하세요

주의할 점

1. H2와 실제 DB의 SQL 호환성 차이로 운영에서 쿼리가 실패할 수 있다

H2의 MySQL 호환 모드가 완벽하지 않아, H2에서 통과한 네이티브 쿼리가 실제 MySQL에서 문법 오류를 일으키는 경우가 있습니다. DB 고유 함수(JSON_EXTRACT, MATCH AGAINST 등)는 H2에서 지원하지 않으므로, 네이티브 쿼리가 많다면 Testcontainers로 실제 DB를 사용하는 것이 안전합니다.

2. @Transactional 자동 롤백 때문에 실제 커밋 시에만 발생하는 문제를 놓칠 수 있다

@DataJpaTest는 각 테스트 후 자동 롤백합니다. 유니크 제약 조건 위반, Auto Increment 동작, @GeneratedValue 전략 등은 커밋 시점에 확인되는데, 롤백되면 이런 문제가 드러나지 않습니다. 필요한 경우 @Rollback(false)로 실제 커밋을 확인하세요.

3. em.clear() 없이 테스트하면 1차 캐시 때문에 쿼리 검증이 무의미해진다

em.persistAndFlush() 후 같은 엔티티를 조회하면 1차 캐시에서 가져오기 때문에 실제 SELECT 쿼리가 실행되지 않습니다. Repository의 쿼리가 올바른지 검증하려면 반드시 em.clear()로 영속성 컨텍스트를 초기화한 뒤 조회해야 합니다.

정리

  • @DataJpaTest는 JPA 관련 빈만 로드하는 슬라이스 테스트로, Repository를 빠르게 격리 테스트합니다
  • 기본적으로 ** 임베디드 DB + 트랜잭션 롤백 **으로 동작합니다
  • TestEntityManager로 테스트 데이터를 관리하고, em.clear()로 1차 캐시를 초기화하세요
  • DB 고유 기능을 테스트하려면 @AutoConfigureTestDatabase(replace = NONE)으로 실제 DB를 사용합니다
댓글 로딩 중...