TestContainers로 MySQL + Redis 통합 테스트
이번 글에서는 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 환경에서 통합 테스트 가능.
테스트 클래스에는 더 이상 컨테이너나 빈 정의를 두지 않아 깔끔하게 유지 가능.