[DB] TestContainers 도입기

easyone·2026년 6월 15일

Spring

목록 보기
23/25

Testcontainers로 PostgreSQL 통합 테스트 환경 맞추기

이번에 테스트 환경을 보다가 찝찝한 지점이 있었다.

운영은 PostgreSQL을 쓰고 있는데, 테스트는 H2의 PostgreSQL mode로 돌고 있었다.

처음에는 PostgreSQL mode면 어느 정도 비슷하지 않나?라고 생각할 수 있다.
그런데 Repository 테스트나 JPA 매핑 테스트에서는 이 차이가 생각보다 크다.
H2, PostgreSQL 예약어를 둘다 피해야 하고, 같은 환경이 아니므로 테스트 시 예외가 생길 수도 있는 것이다.

왜 H2를 사용하지 않는지

H2 PostgreSQL mode는 PostgreSQL을 완전히 재현하지 않는다.
PostgreSQL처럼 보이게 일부 문법을 맞춰주는 것에 가깝다.

그래서 다음 같은 부분에서 차이가 생길 수 있다.

  • @SQLRestriction 기반 소프트 삭제 필터
  • native query
  • PostgreSQL 전용 타입이나 함수
  • @MapsId 공유 PK 매핑
  • FK, unique, not null 같은 제약 조건
  • cascade, orphan removal 동작

예를 들어 이런 native query가 있다고 하자.

SELECT COUNT(*)
FROM users
WHERE user_id = ?

H2에서는 통과했는데 PostgreSQL에서는 식별자, 타입, 문법 차이 때문에 실패할 수 있다.
이러면 테스트는 통과인데 운영에서 깨지는 상황이 생긴다.

그래서 테스트 DB도 운영과 같은 PostgreSQL 16으로 맞추기로 했다.

선택한 방식: jdbc:tc URL

이번에는 Testcontainers의 JDBC URL 방식을 썼다.

spring:
  datasource:
    url: jdbc:tc:postgresql:16:///testdb
    username: test
    password: test
  test:
    database:
      replace: none

jdbc:tc:postgresql:16:///testdb는 Testcontainers가 제공하는 특수한 JDBC URL이다.
애플리케이션이 이 datasource로 연결하려고 하면 Testcontainers가 PostgreSQL 컨테이너를 띄워준다.

이 방식의 장점은 설정이 가볍다는 것이다.

dependencies {
    testImplementation("org.testcontainers:junit-jupiter")
    testImplementation("org.testcontainers:postgresql")
}

컨테이너를 직접 생성하는 테스트 설정 클래스를 만들 필요가 없다.
지금처럼 PostgreSQL 하나만 필요한 상황에서는 가장 단순하다.

@ActiveProfiles("test") 추가하기

놓치기 쉬운 부분이 있다.

application-test.yml에 Testcontainers datasource를 잡아도, 테스트가 test profile로 실행되지 않으면 해당 설정을 읽지 않는다.

그래서 Repository 테스트나 통합 테스트에 profile을 명시했다.

@DataJpaTest
@ActiveProfiles("test")
class UserRepositoryTest {
    // ...
}

통합 테스트도 마찬가지다.

@SpringBootTest
@ActiveProfiles("test")
class HealthControllerTests {
    // ...
}

그리고 @DataJpaTest 같은 slice test에서는 Spring Boot가 datasource를 임베디드 DB로 바꾸려고 할 수 있다.
그래서 다음 설정도 같이 둔다.

spring:
  test:
    database:
      replace: none

이 설정이 없으면 열심히 Testcontainers datasource를 잡아놓고도 테스트에서는 다른 DB를 볼 수 있다.

@ServiceConnection

Spring Boot에서는 @ServiceConnection으로 Testcontainers 연결 정보를 자동 등록할 수도 있다.

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfig {

    @Bean
    @ServiceConnection
    fun postgresContainer(): PostgreSQLContainer<*> =
        PostgreSQLContainer("postgres:16")
}

이 방식은 컨테이너를 코드로 직접 다룬다.
그래서 다음 상황에서는 JDBC URL보다 더 낫다.

  • PostgreSQL 외에 Redis, Kafka 같은 컨테이너가 추가된다.
  • 컨테이너 옵션을 코드로 제어해야 한다.
  • datasource 외의 연결 정보도 함께 등록해야 한다.
  • 테스트별 컨테이너 생명주기를 명확하게 관리해야 한다.

즉 이번 선택 기준은 이렇게 잡았다.

상황선택
PostgreSQL 하나만 필요하다jdbc:tc: URL
여러 컨테이너가 필요하다@ServiceConnection
Spring Boot 지원 범위를 벗어난 세부 설정이 필요하다@DynamicPropertySource

@ServiceConnection을 쓰기 어려운 경우에는 @DynamicPropertySource로 직접 datasource property를 넣을 수도 있다.

@Testcontainers
@SpringBootTest
class UserIntegrationTest {

    companion object {
        @Container
        val postgres = PostgreSQLContainer("postgres:16")

        @JvmStatic
        @DynamicPropertySource
        fun properties(registry: DynamicPropertyRegistry) {
            registry.add("spring.datasource.url", postgres::getJdbcUrl)
            registry.add("spring.datasource.username", postgres::getUsername)
            registry.add("spring.datasource.password", postgres::getPassword)
        }
    }
}

트레이드오프

Testcontainers를 쓰면 분명 H2보다 느리다.

첫 실행 때는 Docker 이미지 pull이 필요할 수 있고, 컨테이너 기동 시간도 있다.
또 로컬이나 CI에 Docker가 없으면 테스트가 돌지 않는다.

하지만 DB 통합 테스트의 목적이 운영 DB와의 정합성 검증이라면 이 비용은 낼 만하다.

정리하면 이렇다.

항목얻은 것감수한 것
정합성운영과 같은 PostgreSQL에서 테스트H2보다 무거움
속도JVM 내 컨테이너 공유로 비용 분산 가능첫 컨테이너 기동 수 초
환경로컬과 CI의 DB 조건 통일Docker 필수
설정JDBC URL은 코드 0줄DB 단독일 때 가장 적합
확장성지금 단계에서는 충분Redis 등 추가 시 방식 전환 필요

CI에서는?

GitHub Actions의 ubuntu-latest runner는 Docker를 사용할 수 있다.
그래서 일반적인 Testcontainers 기반 테스트는 별도의 PostgreSQL service를 workflow에 띄우지 않아도 동작한다.

물론 Docker가 필요한 테스트가 된다는 점은 명확히 해야 한다.
로컬 개발자 환경이나 CI runner가 Docker를 지원하지 않으면 테스트가 실패한다.

정리

이번 도입의 결론은 단순하다.

  • 운영이 PostgreSQL이면 DB 통합 테스트도 PostgreSQL에서 돌리는 편이 낫다.
  • H2 PostgreSQL mode는 PostgreSQL과 같지 않다.
  • jdbc:tc:postgresql:16:///testdb는 단일 DB 컨테이너 테스트에 가장 가볍다.
  • @DataJpaTest에서는 spring.test.database.replace: none을 확인해야 한다.
  • application-test.yml을 쓰려면 @ActiveProfiles("test")가 필요하다.
  • Redis, Kafka 등 컨테이너가 늘어나면 @ServiceConnection이나 @DynamicPropertySource로 넘어가는 게 낫다.

테스트 속도만 보면 H2가 더 좋다.
하지만 운영에서 깨질 가능성을 줄이는 테스트라면, 조금 느려도 실제 DB에서 검증하는 쪽이 더 낫다고 판단해서 도입하게 되었다.

출처

https://java.testcontainers.org/modules/databases/jdbc/
https://java.testcontainers.org/modules/databases/postgres/
https://docs.spring.io/spring-boot/reference/testing/testcontainers.html
https://docs.spring.io/spring-boot/api/java/org/springframework/boot/test/autoconfigure/jdbc/AutoConfigureTestDatabase.Replace.html

profile
백엔드 개발자 지망 대학생

0개의 댓글