어떤 개념일까?
Testcontainers는
Docker 컨테이너로 실제 서비스 (DB, 메시지 브로커, 브라우저 등)의 일회용 인스턴스를 테스트 코드에서 직접 띄우고, 끝나면 자동으로 정리해주는 라이브러리이다.
데이터베이스, 메시지 브로커, 웹 브라우저 등 Docker 컨테이너에서 돌릴 수 있는 거의 모든 것의 가볍고 일회용일 인스턴스를 제공하는 오픈소스 라이브러리이다.
- 실제 서비스를 테스트 의존성으로 사용한다. Mock이나 인메모리 대체물 없이, 프로덕션에서 쓰는 것과 동일한 서비스로 테스트한다.
- 테스트 의존성을 코드로 정의한다.
어떤 문제를 해결하려고 나왔을까? 왜 사용 할까?
테스트 환경과 프로덕션 환경의 불일치 때문에 등장하게 되었다.
기존의 통합 테스트의 선택지는 둘중의 하나였다.
Mock / 인메모리 대체물 (H2)
- 빠르지만 진짜가 아니고, 실제 DB의 방언, 락 동작, MVCC, 제약 조건 처리 방식이 다르다.
- 테스트는 통과했는데, 실제 운영환경에서 깨지는 상황이 발생한다.
로컬에 직접 설치하거나, Docker/Docker Compose로 수정 운영
- 개발자 머신마다 환경이 다르고, 시뢰성 있게 완전히 초기화된 의존성을 만들려면 Docker 내부 동작에 대한 깊은 지식이 필요하다.
- 테스트가 깨끗한 상태에서 시작한다는 보장이 없다.
그래서 Testcontainers 의 장점
- 프로덕션과 동일한 서비스로 테스트가 진행된다.
- 항상 알려진 깨끗한 상태에서 테스트가 시작 된다.
- 복잡한 로컬 환경 설정이 필요없고, Docker로 바로 실행이 가능하다.
어떻게 동작하나?
- 테스트 코드에서 컨테이너 객체를 선언한다.
GenericContainer를 통해 Docker 컨테이너를 표현한다. (MySQLContainer,PostgreSQLContainer같은 전용 모듈은 이것을 구체화 한것이다.) - 기존 Docker 이미지 / Dockerfile / Docker Compose 파일 어디서든 컨테이너를 띄울 수 있다.
- 시작된 컨테이너로부터
hostname, 매핑된 포트, JDBC URL 등 접속 정보를 동적으로 얻어와 테스트에서 사용한다.
자동 정리 Ryuk (Resource Reaper)
- TestContainers는 컨테이너/볼륨/네트워크 등 생성한 리소스에 고유한 라벨을 붙인다.
Ryuk(Reaper)라는 별도의 사이드카 컨테이너를 띄워서, 그 라벨을 기준으로 리소스를 자동 정리한다.Ryuk는 Testcontainers와의 연결이 끊긴 뒤(대략 10초) 자동으로 리소스를 제거한다.- 덕분에 테스트 프로세스가 비정상 종료되어도 컨테이너가 좀비로 남지 않고 정리된다.
생명주기 관리 패턴 (JUnit 5)
JUnit 5 Extension, @Testcontainers + @Container로 컨테이너가 테스트 클래스 단위로 시작/종료 된다.Singleton Container추상 베이스 클래스의 static 필드 + static {container.start();} 베이스 클래스 로드 시 한번만 시작한다. 여러 테스트 클래스가 공유되고 종료는 Ryuk가 처리해준다.- Manual (try-with-resources) try (GenericContainer c = ….) {c.start();} 블록 종료 시 정리한다.
언제 쓰고, 언제 안 쓰나?
쓸 때:
- 데이터 접근 계층의 통합 테스트일 때, 실제 MySQL/PostgreSQL/Oracle 컨테이너로 Repository/DAO를 완전히 호환하여 검증할 때
- DB 벤터 고유 동작을 검증해야할 때 SQL 방언, 제약조건, 시퀀스 등 H2로 재현이 안되는 부분일 때
- 락 / 동시성 / 트랜잭션 격리 수준 / MVCC 테스트 할 때 비관적란, 격리 수준별 동작은 실제 DB 엔진에서만 정확히 검증된다.
- 애플리케이션 통합 테스트 DB, 메시지 큐, 웹 서버 같은 의존성과 함께 앱을 짧게 띄워서 검증할 때
- UI / 인수테스트 일 때
안 쓸 때:
- 순수 단위 테스트일 때
- Docker를 못 쓰는 환경
- 빠른 피드백 루프가 중요한 경우 컨테이너 시작 비용이 부담이 되고 표준 SQL만 쓰는 경우 Repository 테스트라면 @JdbcTest + H2가 더 빠르고 적절하다.
- DB 엔진 고유 동작과 무관한 표준 SQL 검증일 때 H2로 충분한 영역까지 굳이 TestContainers를 사용할 필요가 없다.
테스트하려는 게 H2로 재현 가능한가?
재현 가능하면H2,
엔진 고유 동작(락/MVCC/방언)을 검증해야 하면Testcontainers
남에게 설명한다면 어떻게 설명할 것인가?
추가 궁금한 질문들
- Spring Boot 통합:
@DynamicPropertySource/@ServiceConnection으로 컨테이너 접속 정보를 Spring 컨텍스트에 주입하는 방식의 차이는? (스프링 빈으로 관리할 때 vs Testcontainers가 생명주기를 관리할 때 차이) - 락/MVCC 테스트 설계: 실제 동시성 시나리오(두 트랜잭션이 같은 행에 경쟁)를 Testcontainers + 멀티스레드로 재현할 때, 경쟁 상태를 결정적(deterministic)으로 만드는 방법은? (CountDownLatch / 의도적 sleep / 격리 수준별 기대 동작 정의)
- 테스트 격리 vs 공유의 트레이드오프: Singleton으로 컨테이너를 공유하면 테스트 간 상태 오염 위험이 생기는데, 깨끗한 상태를 보장하는 전략은? (트랜잭션 롤백 / 매 테스트 데이터 초기화 / 컨테이너 재생성)
TestContainers의 장점/이점
현재 제가 테스트 하려고 하는 기능이 동시에 여러 사용자가 예약 등록을 할때, 1개의 예약만 확정이 되도록 하는 것이었습니다.
하지만, 현재 H2를 사용하고 있는데, 실제 운영환경에서 MySQL을 사용한다고 가정하면, H2와 MySQL은 락 모델이 달라서 FOR UPDATE에 대해서 서로 다르게 깨질 가능성이 있다고 생각했습니다.
그래서 락 동작에 대한 검증을 하기 위해서는 실제 DB를 사용해야한다고 생각이 들었습니다.
그래서 TestContainers를 사용해서 실제 MySQL 컨테이너를 띄워서 실제 락 동작 검증을 했기 때문에 더 확실한 동시성 테스트를 진행할 수 있었습니다.