@DataJpaTest — Repository 계층을 격리해서 테스트하는 방법
Repository 테스트를 할 때 서비스 로직은 신경 쓰고 싶지 않은데, 전체 애플리케이션을 띄워야 할까요?
@DataJpaTest는 JPA 관련 빈만 로드하는 슬라이스 테스트입니다. Repository 계층을 격리해서 테스트할 수 있고, 기본적으로 임베디드 DB를 사용하여 빠르게 실행됩니다.
@DataJpaTest란
@DataJpaTest는 JPA 관련 컴포넌트만 로드하는 슬라이스 테스트 어노테이션입니다.
로드되는 것:
@Entity클래스JpaRepository인터페이스EntityManager,DataSource- Flyway/Liquibase 마이그레이션 (설정에 따라)
로드되지 않는 것:
@Service,@Controller,@Component- Security, Web 관련 빈
내부적으로 @Transactional을 포함하고 있어 각 테스트 메서드 후 자동 롤백 됩니다.
기본 사용법
@DataJpaTest
class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private TestEntityManager em; // 테스트용 EntityManager
em.clear()로 1차 캐시를 초기화한 후 조회해야 실제 DB 쿼리가 실행되는지 검증할 수 있습니다.
@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는 테스트에서 엔티티를 다루기 위한 편의 래퍼입니다.
@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 쿼리가 실행되는지 확인할 수 없습니다.
커스텀 쿼리 메서드 테스트
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);
}
@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 쿼리도 같은 방식으로 테스트합니다.
@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를 사용합니다.
<!-- 테스트용 H2 의존성 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
장점:
- 별도 DB 설치/설정 불필요
- 테스트 속도가 빠름
- CI 환경에서 바로 실행 가능
** 단점:**
- MySQL/PostgreSQL과 SQL 문법 차이
- DB 고유 함수, 인덱스 동작을 테스트할 수 없음
- 네이티브 쿼리 호환성 문제
실제 DB 사용
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
class ProductRepositoryTest {
// 실제 DB를 사용하여 테스트
}
# 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이 포함되어 있어 각 테스트 메서드 후 자동 롤백됩니다.
@Test
void 저장_후_다른_테스트에_영향_없음() {
productRepository.save(Product.builder().name("테스트").price(1000).build());
// 이 테스트 끝나면 자동 롤백 → 다른 테스트에 영향 없음
}
롤백이 문제가 되는 경우
@Service
public class ProductService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createWithNewTransaction(Product product) {
productRepository.save(product);
// REQUIRES_NEW: 새 트랜잭션이 열리고 커밋됨
}
}
테스트의 @Transactional 롤백이 REQUIRES_NEW로 열린 트랜잭션에는 영향을 주지 않습니다. 이런 경우 테스트 간 격리를 위해 @AfterEach에서 직접 데이터를 정리해야 합니다.
롤백 비활성화
실제로 커밋되는 것을 확인하고 싶다면 롤백을 비활성화할 수 있습니다.
@Test
@Rollback(false) // 또는 @Commit
void 커밋_확인용_테스트() {
productRepository.save(Product.builder().name("커밋됨").build());
// 실제 DB에 커밋됨 — 주의: 테스트 후 수동 정리 필요
}
Auditing 테스트
@CreatedDate, @LastModifiedDate 같은 JPA Auditing을 테스트하려면 @EnableJpaAuditing이 필요합니다.
@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를 사용합니다