개발

TestContainers로 MySQL + Redis 통합 테스트

정재익 2025. 9. 5. 14:05


이번 글에서는 CommentClosureQueryAdapter 통합 테스트와 PostCacheSyncAdapter 테스트를 TestContainers 기반으로 통합 테스트 환경으로 바꾸면서 겪은 삽질과 해결 과정을 정리해보겠습니다.

1. 시작: 댓글 테스트에서 MySQL만 사용

처음에는 댓글 테스트에서 MySQL 컨테이너만 사용했습니다.

@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

@DynamicPropertySource
static void dynamicProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", mysql::getJdbcUrl);
    registry.add("spring.datasource.username", mysql::getUsername);
    registry.add("spring.datasource.password", mysql::getPassword);
}




문제 없었음: MySQL만 쓰니까 컨테이너 초기화와 데이터 소스 주입이 안정적이었음.

성공 포인트: 댓글 클로저 조회 동작이 실제 MySQL 환경에서 검증됨.

2. 글 캐시 테스트: MySQL + Redis 도입

다음 단계는 인기 게시글 조회 및 상세 조회 기능을 Redis까지 포함한 환경에서 테스트하는 것이었습니다.

@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:latest")
        .withExposedPorts(6379)
        .withReuse(true);




MySQL과 Redis를 동시에 띄우려고 하니 기존 방식대로 테스트 클래스 안에 @DynamicPropertySource와 @TestConfiguration을 두면 연결이 간헐적으로 실패했습니다.

Redis 연결 시 Connection refused 에러 발생.

여러 번 삽질 후, 공통 설정 클래스로 분리하기로 결정.

3. 고민과 여러 번의 삽질
3-1. DynamicPropertySource 위치 문제

@DynamicPropertySource
static void dynamicProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", mysql::getJdbcUrl);
    registry.add("spring.datasource.username", mysql::getUsername);
    registry.add("spring.datasource.password", mysql::getPassword);

    registry.add("spring.data.redis.host", redis::getHost);
    registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
}




처음에는 각 테스트 클래스 안에서 MySQL과 Redis를 동시에 설정했음.

Redis 연결이 테스트 시작 시점에서 종종 실패 → 컨테이너 시작 순서 문제라고 생각했지만 static { redis.start(); } 도 넣어도 간헐적 실패 지속.

3-2. TestConfiguration 내부에서 Redis 빈 등록

@TestConfiguration
static class TestConfig {
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory factory = new LettuceConnectionFactory(redis.getHost(), redis.getMappedPort(6379));
        factory.afterPropertiesSet();
        return factory;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}




테스트 클래스 안에서 빈을 정의했지만, 테스트 컨텍스트 초기화 시점과 컨테이너 준비 시점이 맞지 않아 여전히 실패.

3-3. JPAQueryFactory 직접 생성 시도

@TestConfiguration
static class TestConfig {
    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
        return new JPAQueryFactory(entityManager);
    }
}




댓글 테스트처럼 JPAQueryFactory를 직접 만들었지만, Redis까지 포함된 환경에서는 빈 주입 순서 때문에 또 다른 실패 발생.

4. 해결: 공통 TestContainersConfiguration 클래스 통합

결국 모든 테스트에서 공통으로 사용하는 TestContainersConfiguration 클래스를 만들고, MySQL과 Redis를 통합했습니다.

@TestConfiguration(proxyBeanMethods = false)
public class TestContainersConfiguration {

    @Bean
    @ServiceConnection
    MySQLContainer<?> mysqlContainer() {
        return new MySQLContainer<>("mysql:8.0")
                .withDatabaseName("testdb")
                .withUsername("test")
                .withPassword("test");
    }

    static GenericContainer<?> redis = new GenericContainer<>("redis:latest")
            .withExposedPorts(6379)
            .withReuse(true);

    static { redis.start(); }

    @Bean
    @Primary
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redis.getHost(), redis.getMappedPort(6379));
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
        return new JPAQueryFactory(entityManager);
    }

    @Bean
    public EncryptionUtil encryptionUtil() {
        return new EncryptionUtil();
    }
}



이제 댓글 테스트와 글 캐시 테스트 모두 이 공통 클래스를 가져다 쓰면 안정적으로 MySQL과 Redis 환경에서 통합 테스트 가능.

테스트 클래스에는 더 이상 컨테이너나 빈 정의를 두지 않아 깔끔하게 유지 가능.