테스트에서는 통과했는데 프로덕션에서 쿼리 에러가 납니다. H2로 테스트하면 안 되는 걸까요?

개념 정의

@DataJpaTest 는 JPA 관련 빈(Repository, EntityManager)만 로드하는 슬라이스 테스트 어노테이션입니다. 전체 ApplicationContext를 띄우지 않아 빠릅니다.

3가지 테스트 전략 비교

전략DB속도프로덕션 정합성설정 복잡도
H2 인메모리H2매우 빠름낮음없음
TestcontainersMySQL/PostgreSQL 컨테이너중간** 높음**중간
별도 테스트 DB실제 DB 서버느림높음높음

전략 1: @DataJpaTest + H2 (빠르지만 위험)

JAVA
@DataJpaTest
class OrderRepositoryTest {
    @Autowired
    private OrderRepository orderRepository;

    @Test
    void 주문_저장_조회() {
        Order order = Order.create("user1", 10000);
        orderRepository.save(order);

        Order found = orderRepository.findById(order.getId()).orElseThrow();
        assertThat(found.getUserId()).isEqualTo("user1");
    }
}

@DataJpaTest는 기본적으로 H2 인메모리 DB를 사용합니다. 장점은 ** 설정 없이 바로 실행 **된다는 것. 단점은 프로덕션 DB(MySQL)와 ** 동작이 다를 수 있다 **는 것.

H2에서 통과하고 MySQL에서 터지는 예시

SQL
-- MySQL에서는 동작하지만 H2에서는 에러
SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM users GROUP BY team_id;

-- H2에서는 동작하지만 MySQL에서는 다르게 동작
SELECT * FROM orders WHERE created_at > DATEADD('DAY', -7, NOW());

전략 2: @DataJpaTest + Testcontainers (권장)

Testcontainers는 테스트할 때 **도커 컨테이너로 실제 DB를 띄웁니다 **.

테스트 클래스에 Testcontainers 어노테이션을 추가하고, H2 자동 교체를 비활성화합니다.

JAVA
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderRepositoryTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("testdb");

    @DynamicPropertySource
    static void setProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
    }

@DynamicPropertySource로 컨테이너의 동적 포트/URL을 주입합니다. 이제 실제 MySQL에서 테스트가 실행됩니다.

JAVA
    @Autowired
    private OrderRepository orderRepository;

    @Test
    void 주문_저장_조회() {
        Order order = Order.create("user1", 10000);
        orderRepository.save(order);

        Order found = orderRepository.findById(order.getId()).orElseThrow();
        assertThat(found.getUserId()).isEqualTo("user1");
    }
}

** 핵심 설정 3가지:**

  1. @Testcontainers — 컨테이너 라이프사이클 관리
  2. @AutoConfigureTestDatabase(replace = NONE) — H2 자동 교체 방지
  3. @DynamicPropertySource — 컨테이너의 동적 포트/URL을 주입

테스트 격리

@Transactional 자동 롤백

@DataJpaTest는 기본적으로 @Transactional이 적용되어, 각 테스트가 끝나면 ** 자동 롤백 **됩니다. 테스트 간 데이터가 격리됩니다.

JAVA
@DataJpaTest
class UserRepositoryTest {
    @Test
    void 테스트_A() {
        userRepository.save(new User("alice"));
        // 테스트 끝나면 자동 롤백
    }

    @Test
    void 테스트_B() {
        // 테스트 A의 alice는 롤백되어 없음
        assertThat(userRepository.count()).isEqualTo(0);
    }
}

@Sql로 테스트 데이터 준비

JAVA
@Test
@Sql("/test-data/orders.sql")
void 월별_주문_통계() {
    List<OrderStats> stats = orderRepository.findMonthlyStats(2026);
    assertThat(stats).hasSize(12);
}

함정 — 이걸 모르면 터진다

@Transactional 롤백 때문에 flush가 안 된다

@DataJpaTest의 자동 롤백은 편리하지만, JPA가 flush를 건너뛸 수 있습니다. 트랜잭션이 커밋되지 않으면 SQL이 실제로 실행되지 않아서, 제약 조건 위반(유니크 키 등)을 테스트에서 놓칩니다.

JAVA
@Test
void 유니크_제약_조건_테스트() {
    userRepository.save(new User("alice"));
    userRepository.save(new User("alice"));  // 유니크 위반
    // ❌ 롤백되어서 실제 INSERT가 안 나감 → 테스트 통과
    // ✅ flush 강제로 에러 확인
    assertThatThrownBy(() -> entityManager.flush())
        .isInstanceOf(PersistenceException.class);
}

Testcontainers가 느리다면 싱글톤 컨테이너 패턴

테스트마다 컨테이너를 새로 띄우면 30초씩 걸립니다. ** 싱글톤 패턴 **으로 한 번만 띄우고 재사용합니다.

JAVA
// 추상 클래스로 컨테이너 공유
abstract class IntegrationTestBase {
    static final MySQLContainer<?> MYSQL;

    static {
        MYSQL = new MySQLContainer<>("mysql:8.0");
        MYSQL.start();  // 한 번만 시작
    }

    @DynamicPropertySource
    static void setProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", MYSQL::getJdbcUrl);
        registry.add("spring.datasource.username", MYSQL::getUsername);
        registry.add("spring.datasource.password", MYSQL::getPassword);
    }
}

정리

상황선택이유
빠른 단위 테스트H2 + @DataJpaTestDB 방언 무관한 기본 CRUD
프로덕션 정합성 필요Testcontainers실제 DB와 동일 환경
네이티브 쿼리 테스트Testcontainers (필수)H2에서 동작 안 할 수 있음
CI 파이프라인Testcontainers 싱글톤속도 + 정합성 균형

기본 CRUD는 H2로 빠르게, 네이티브 쿼리나 DB 특화 기능은 Testcontainers로 확실하게.

댓글 로딩 중...