어제 오늘 내일

[Spring Boot] "H2랑 MySQL 문법이 달라서 터졌어요..." TestContainers로 완벽한 통합 테스트 환경 구축하기 본문

IT/SpringBoot

[Spring Boot] "H2랑 MySQL 문법이 달라서 터졌어요..." TestContainers로 완벽한 통합 테스트 환경 구축하기

hi.anna 2026. 3. 24. 09:58

 
우리는 보통 테스트할 때 빠르고 간편한 H2 데이터베이스(In-memory)를 사용합니다. 하지만 운영 환경은 MySQL이나 PostgreSQL을 쓰죠. 여기서 치명적인 문제가 발생합니다.

  • MySQL: INSERT IGNORE... (지원함)
  • H2: "그게 뭐죠? 에러 뿜!" (지원 안 함, 혹은 문법 다름)

결국 "테스트는 통과했지만 배포하면 망하는" 시한폭탄을 안고 가는 셈입니다.
이 문제를 해결하기 위해 등장한 것이 바로 Testcontainers입니다.


1. Testcontainers가 뭔가요?

자바 코드(JUnit)에서 Docker 컨테이너를 직접 제어하는 라이브러리입니다.

  1. 테스트가 시작되면 (@BeforeAll)
  2. 도커로 진짜 MySQL 컨테이너를 띄웁니다.
  3. 테스트를 수행합니다.
  4. 테스트가 끝나면 (@AfterAll) 컨테이너를 삭제합니다.

즉, 내 컴퓨터에 MySQL을 깔지 않아도, 테스트 순간만큼은 완벽한 운영 환경을 복제해 내는 것이죠.


2. 준비물: Docker Desktop

이 기술은 도커를 사용하므로, 개발 PC에 Docker Desktop이 켜져 있어야 합니다. (이게 유일한 단점입니다.)


3. 스프링 부트 3.1+ 설정 (의존성 추가)

스프링 부트 3.1부터는 Testcontainers 지원이 엄청나게 강력해졌습니다. build.gradle에 다음을 추가하세요.

dependencies {
    // 1. 기본 라이브러리
    testImplementation 'org.springframework.boot:spring-boot-testcontainers'

    // 2. 주니터 5와 연동
    testImplementation 'org.testcontainers:junit-jupiter'

    // 3. 내가 쓸 DB 컨테이너 (MySQL)
    testImplementation 'org.testcontainers:mysql'
}

4. 실전 코드: "@ServiceConnection"의 마법

예전에는 IP랑 포트를 찾아서 설정 파일에 넣어주는 복잡한 코드가 필요했습니다 (@DynamicPropertySource).
하지만 이제는 @ServiceConnection 어노테이션 하나면 스프링이 알아서 다 연결해 줍니다.

통합 테스트 클래스 만들기 ()

@DataJpaTest // JPA 관련 빈만 로드
@AutoConfigureTestDatabase(replace = Replace.NONE) // H2로 바꿔치기 하지 마! (내꺼 쓸 거야)
@Testcontainers // 1. 이 클래스는 컨테이너를 씁니다.
class MemberRepositoryTest {

    // 2. 도커로 MySQL 8.0 버전을 띄워라!
    @Container
    @ServiceConnection // ★ 핵심: 스프링아, 이 컨테이너 정보를 네 DataSource로 써!
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");

    @Autowired
    MemberRepository memberRepository;

    @Test
    void 회원저장_테스트() {
        // given
        Member member = new Member("testUser");

        // when
        Member savedMember = memberRepository.save(member);

        // then
        assertThat(savedMember.getId()).isNotNull();
        // 진짜 MySQL에서 저장되고 조회된 것임!
    }
}

실행 결과:
테스트를 돌려보면 콘솔창에 도커 로그가 찍히면서 MySQL 컨테이너가 뿅 하고 떴다가, 테스트가 끝나면 사라집니다.


5. Redis도 띄워볼까요?

DB뿐만 아니라 Redis도 똑같습니다. 로컬에 Redis 깔 필요 없습니다.

@SpringBootTest
@Testcontainers
class RedisTest {

    @Container
    @ServiceConnection
    static GenericContainer<?> redis = new GenericContainer<>("redis:alpine")
            .withExposedPorts(6379); // 6379 포트 열어줘

    @Autowired
    StringRedisTemplate redisTemplate;

    @Test
    void 레디스_테스트() {
        redisTemplate.opsForValue().set("key", "value");
        String value = redisTemplate.opsForValue().get("key");
        assertThat(value).isEqualTo("value");
    }
}

6. 단점은 없나요? (속도 이슈)

세상에 공짜는 없습니다. Testcontainers는 느립니다.
매번 테스트할 때마다 도커 이미지를 다운받고(처음 한 번만), 컨테이너를 띄우는 데 몇 초가 걸립니다.

  • 해결책: 모든 테스트마다 띄우지 말고, 추상 클래스(AbstractIntegrationTest)를 하나 만들어서 컨테이너를 static으로 선언하세요. 그러면 딱 한 번만 띄우고 모든 테스트가 그 컨테이너를 재사용합니다. (싱글톤 패턴)
// 모든 통합 테스트의 부모 클래스
@Testcontainers
public abstract class IntegrationTestSupport {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
}

마치며

오늘의 결론입니다.

  1. Testcontainers를 쓰면 H2의 한계(문법 차이)를 극복할 수 있다.
  2. @ServiceConnection 덕분에 설정이 정말 간편해졌다. (Spring Boot 3.1+)
  3. Docker만 깔려있다면, 팀원 누구나 똑같은 환경에서 테스트를 돌릴 수 있다.

이제 "제 로컬에선 되는데요?"라는 변명은 통하지 않습니다. 완벽한 격리 환경에서 자신 있게 배포하세요!
다음 포스팅에서는 "이메일 형식 검사, if문으로 짜다가 밤새실 건가요?" 입력값 검증의 정석 @Valid와 커스텀 검증 어노테이션 만들기에 대해 알아보겠습니다.
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments