H2로 테스트하면 안 되는 이유
통합 테스트를 H2 인메모리 DB로 돌리고 있었다. 로컬에서는 전부 통과하는데, 운영(PostgreSQL)에서만 터지는 버그가 분기에 2-3번은 나왔다. '테스트가 무슨 의미가 있나'라는 회의감이 들 때쯤, Testcontainers를 알게 됐다. 실제 PostgreSQL 컨테이너로 테스트를 돌리니까, 운영 버그가 사라졌다.
저희 팀에서도 H2에서는 통과했던 테스트가 운영 PostgreSQL에서 실패하는 사고를 여러 번 경험한 후, Testcontainers로 전면 전환했습니다.
Testcontainers 설정
// build.gradle
dependencies {
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.testcontainers:kafka'
}
Spring Boot 3.1+ ServiceConnection 활용
Spring Boot 3.1부터 @ServiceConnection 어노테이션으로 컨테이너 연결 설정을 자동화할 수 있습니다.
@TestConfiguration(proxyBeanMethods = false)
public class TestContainerConfig {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withInitScript("schema.sql");
}
@Bean
@ServiceConnection
GenericContainer<?> redisContainer() {
return new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
}
}
통합 테스트 작성 패턴
Repository 테스트
@DataJpaTest
@Import(TestContainerConfig.class)
@AutoConfigureTestDatabase(replace = Replace.NONE)
class OrderRepositoryTest {
@Autowired
private OrderRepository orderRepository;
@Test
void PostgreSQL_네이티브쿼리가_정상_동작한다() {
// given
Order order = Order.builder()
.userId(1L)
.metadata(Map.of("coupon", "SUMMER2025"))
.build();
orderRepository.save(order);
// when - PostgreSQL JSON 연산자 사용
List<Order> results = orderRepository
.findByMetadataKey("coupon");
// then
assertThat(results).hasSize(1);
assertThat(results.get(0).getMetadata())
.containsEntry("coupon", "SUMMER2025");
}
@Test
void 대량_데이터_페이징이_정상_동작한다() {
// given
List<Order> orders = IntStream.range(0, 100)
.mapToObj(i -> Order.builder()
.userId((long) i % 10)
.amount(BigDecimal.valueOf(i * 1000))
.build())
.collect(Collectors.toList());
orderRepository.saveAll(orders);
// when
Page<Order> page = orderRepository.findByUserId(
1L, PageRequest.of(0, 5, Sort.by("amount").descending()));
// then
assertThat(page.getTotalElements()).isEqualTo(10);
assertThat(page.getContent()).hasSize(5);
assertThat(page.getContent().get(0).getAmount())
.isGreaterThan(page.getContent().get(1).getAmount());
}
}
API 통합 테스트
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Import(TestContainerConfig.class)
class OrderApiIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void 주문_생성부터_조회까지_전체_흐름() {
// 1. 주문 생성
var createRequest = new CreateOrderRequest(1L, 2, "PRODUCT_A");
var createResponse = restTemplate.postForEntity(
"/api/orders", createRequest, OrderResponse.class);
assertThat(createResponse.getStatusCode())
.isEqualTo(HttpStatus.CREATED);
Long orderId = createResponse.getBody().getId();
// 2. 주문 조회
var getResponse = restTemplate.getForEntity(
"/api/orders/" + orderId, OrderResponse.class);
assertThat(getResponse.getBody().getProductCode())
.isEqualTo("PRODUCT_A");
assertThat(getResponse.getBody().getQuantity())
.isEqualTo(2);
}
}
테스트 성능 최적화
컨테이너 재사용으로 속도 향상
테스트마다 컨테이너를 새로 시작하면 시간이 오래 걸립니다. Singleton 패턴을 활용하여 전체 테스트 스위트에서 컨테이너를 공유합니다.
public abstract class AbstractIntegrationTest {
static final PostgreSQLContainer<?> POSTGRES;
static final GenericContainer<?> REDIS;
static {
POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
.withReuse(true);
REDIS = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379)
.withReuse(true);
POSTGRES.start();
REDIS.start();
}
@DynamicPropertySource
static void configureProperties(
DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username",
POSTGRES::getUsername);
registry.add("spring.datasource.password",
POSTGRES::getPassword);
registry.add("spring.data.redis.host", REDIS::getHost);
registry.add("spring.data.redis.port",
() -> REDIS.getMappedPort(6379));
}
}
testcontainers.properties 설정
# ~/.testcontainers.properties
testcontainers.reuse.enable=true
이 설정으로 로컬에서 컨테이너를 재사용하면 테스트 실행 시간이 50% 이상 단축됩니다.
CI/CD 파이프라인에서의 활용
GitHub Actions에서 Testcontainers를 사용하려면 Docker-in-Docker 또는 서비스 컨테이너 설정이 필요합니다. GitHub Actions는 기본적으로 Docker를 지원하므로 별도의 설정 없이 바로 사용할 수 있습니다.
마무리
Testcontainers는 통합 테스트의 신뢰성을 극적으로 높여줍니다. 초기 설정 비용이 있지만, 운영 환경에서 발생할 수 있는 DB 호환성 이슈를 개발 단계에서 잡아낼 수 있어 충분히 투자할 가치가 있습니다. 특히 Spring Boot 3.1+의 ServiceConnection 기능으로 설정이 훨씬 간편해졌으니, 아직 도입하지 않았다면 적극 고려해보세요.
10년차 풀스택 개발자. Spring Boot, Flutter, AI 등 실무 경험을 기록합니다.
GitHub →
💬 댓글