Testcontainers는 테스트가 시작될 때 Docker 컨테이너로 실제 DB를 띄우고, 테스트가 끝나면 지우는 라이브러리다. Spring Boot는 @ServiceConnection으로 이 컨테이너의 접속 정보를 DB 설정에 자동으로 연결해 준다. 라이브러리 자체의 개념과 Node.js에서 쓰는 방법은 Testcontainers에 정리했고, 이 글은 Spring Boot의 @DataJpaTest와 함께 쓰는 구성을 다룬다.
내장 DB 대신 실제 DB를 쓰는 이유
@DataJpaTest는 기본으로 DB를 H2 같은 내장 메모리 DB로 바꿔 끼운다. 설치할 것이 없어 가볍지만, 운영 DB와 SQL 방언이 다르다. MySQL의 ENUM 컬럼이나 FOR UPDATE SKIP LOCKED 같은 문법을 쓰는 마이그레이션은 H2에서 실패하거나, 반대로 H2에서만 통과하는 테스트가 생긴다.
로컬에 띄워 둔 MySQL에 붙이는 방법도 있지만, 그러면 테스트를 돌리기 전에 사람이 먼저 DB를 켜 둬야 한다. CI나 새 컴퓨터에서는 이 전제를 따로 맞춰야 하고, 개발용 데이터와 테스트 데이터가 섞일 수도 있다. Testcontainers는 테스트가 스스로 DB를 띄우므로 이런 전제가 사라진다.
의존성
Spring Boot 4와 Testcontainers 2 기준이다. 버전은 Spring Boot의 의존성 관리가 정한다.
// build.gradle.kts
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:testcontainers-junit-jupiter")
testImplementation("org.testcontainers:testcontainers-mysql")Testcontainers 1.x에서는 모듈 이름이 org.testcontainers:mysql, org.testcontainers:junit-jupiter였고, 2.0부터 testcontainers- 접두어가 붙었다. MySQLContainer의 패키지도 org.testcontainers.containers에서 org.testcontainers.mysql로 바뀌었다.
테스트 구성
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.mysql.MySQLContainer
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryTest {
companion object {
@Container
@ServiceConnection
@JvmStatic
val mysql = MySQLContainer("mysql:8.4")
}
@Autowired
lateinit var orderRepository: OrderRepository
}어노테이션을 하나씩 보면 다음과 같다.
@Testcontainers: 이 테스트 클래스가 컨테이너를 쓰니 시작과 종료를 JUnit 확장이 관리하라는 표시다.MySQLContainer("mysql:8.4"): 띄울 컨테이너의 설계도다. 이 줄만으로는 컨테이너가 켜지지 않는다. 운영이나docker-compose.yml과 같은 이미지 태그를 쓰면 버전 차이로 생기는 문제를 줄일 수 있다.@Container: 이 필드의 컨테이너를 테스트 시작 때 켜고 끝나면 끈다.@ServiceConnection: 컨테이너는 켜질 때마다 호스트 포트가 임의로 정해져서application.yml에 주소를 적어 둘 수 없다. 이 어노테이션이 켜진 컨테이너의 주소, 포트, 계정을 Spring의DataSource설정에 자동으로 넣는다.@AutoConfigureTestDatabase(replace = Replace.NONE):@DataJpaTest가 DB를 내장 DB로 바꿔 끼우지 않게 한다. 설정된 DB, 즉 컨테이너 MySQL을 그대로 쓴다.
공식 문서: Spring Boot, Testcontainers
companion object와 @JvmStatic
@Container를 인스턴스 필드에 붙이면 테스트 메서드마다 컨테이너를 새로 켜고 끈다. static 필드에 붙이면 테스트 클래스 전체가 컨테이너 하나를 공유한다. 컨테이너 기동에는 몇 초가 걸리므로 보통 static으로 둔다.
Kotlin에는 static 키워드가 없다. 그래서 컨테이너를 companion object 안에 두고, @JvmStatic이나 @JvmField를 붙여 JVM에서 static 멤버로 다뤄지게 한다. 위 구성은 @JvmStatic을 붙인 형태다.
테스트가 돌 때 일어나는 일
- JUnit이 테스트 클래스를 시작하면 Testcontainers가 MySQL 컨테이너를 띄운다.
@ServiceConnection이 컨테이너 접속 정보를 Spring DB 설정에 넣는다.- Flyway 같은 마이그레이션 도구가 컨테이너 DB에 테이블을 만든다.
- 테스트 메서드가 실행된다.
@DataJpaTest가 각 테스트를 트랜잭션으로 감싸고 끝나면 롤백하므로, 테스트 사이에 데이터가 남지 않는다. - 클래스의 테스트가 모두 끝나면 컨테이너가 지워진다.
테스트가 끝난 뒤 testcontainers-ryuk 컨테이너가 잠시 남아 있는 것은 정상이다. 테스트 프로세스가 비정상 종료돼도 띄운 컨테이너를 치우는 관리용 컨테이너다.
롤백 때문에 테스트 안에서 커밋이 일어나지 않는 점은 JPA의 쓰기 시점과 얽혀 있다. 자세한 내용은 JPA 쓰기 시점과 영속성 컨텍스트에 정리했다.
치르는 비용
- 테스트를 돌리는 환경에 Docker 엔진(Docker Desktop, OrbStack 등)이 켜져 있어야 한다.
- 컨테이너 기동 시간만큼 테스트가 느려진다. 처음 실행할 때는 이미지 내려받는 시간도 더해진다.
- 테스트 라이브러리 버전과 Docker 엔진 버전이 어긋날 수 있다. 예를 들어 Spring Boot 3.4.1이 관리하던 Testcontainers 1.20.4는 오래된 Docker API 버전(1.32)으로 요청을 보냈고, 최소 API 버전을 1.40으로 올린 최신 Docker 엔진에서
client version 1.32 is too old오류로 컨테이너를 띄우지 못했다. 이때는 Testcontainers 버전만 올려서 해결한다.
// build.gradle.kts, Spring Boot가 관리하는 버전을 이 프로젝트에서만 덮어씀
extra["testcontainers.version"] = "1.21.4"Spring Boot를 올리면 관리 버전도 함께 올라가 이 덮어쓰기가 필요 없어질 수 있다. 버전을 올린 뒤에는 덮어쓰기를 지우고 테스트가 통과하는지 다시 확인한다.
테스트 단계 전체에서 이 구성이 어디에 해당하는지는 Spring 테스트 단계에 정리했다.