문제 상황
레디스를 사용하는 테스트코드에서 임베디드 레디스가 아닌 테스트컨테이너를 선택했습니다. 전체 테스트를 실행했을 때
레디스 컨테이너에 연결이 실패하는 현상이 발생하고 있습니다.
현재 레디스와 레디스 테스트 컨테이너의 설정입니다.
// 프로덕션 코드에서 RedisTemplate Bean 등록
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
// 테스트 컨테이너 설정
@Testcontainers
public class TestContainerConfig {
private static final String REDIS_IMAGE = "redis:7.0.8-alpine"; // 레디스 컨테이너 이미지
private static final int REDIS_PORT = 6379; // 레디스 포트
private static final GenericContainer redis;
static {
redis = new GenericContainer(REDIS_IMAGE)
.withExposedPorts(REDIS_PORT)
.waitingFor(Wait.forListeningPort());
redis.start(); // 컨테이너 시작
System.setProperty("spring.data.redis.host", redis.getHost()); // Host 설정
System.setProperty("spring.data.redis.port", String.valueOf(redis.getFirstMappedPort())); // Redis 포트에 할당된 포트를 찾아서 설정
}
}
현재 레디스와 레디스 테스트 컨테이너의 설정입니다.
하지만 테스트가 계속 실패하는것이 아니라 50%의 확률로 실패하고 있었습니다.
문제 해결 과정
가정 1. TestContainerConfig가 동작하지 않는다.
🔍 Redis Container가 동작하지 않아서 연결이 되지 않는것을 예상했지만 Redis Container는 정상적으로 실행되었다.
가정 2. redis.getHost() , redis.getFirstMappedPort() 가 제대로 불러오지 못한다.
컨테이너 포트가 왜 59955냐고 생각하실 수 있겠지만 테스트 컨테이너는 비어있는 포트 랜덤을 뽑아서 포트 포워딩하기 때문에 랜덤으로
포트가 결정됩니다.
🔍 Host와 Port 모두 정상적으로 셋팅된다.
더 이상의 예상되는 부분이 없어서 공식문서를 참고했습니다.
참고 공식문서
Manual container lifecycle control - Testcontainers for Java
Manual container lifecycle control While Testcontainers was originally built with JUnit 4 integration in mind, it is fully usable with other test frameworks, or with no framework at all. Manually starting/stopping containers Containers can be started and s
java.testcontainers.org
TestContainer에서는 Redis를 정식으로 지원하지 않기 때문에 GenericContainer를 사용하는것 외에는 다른점이 없었습니다.
추가적인 디버깅으로 원인 분석을 시작했고 Break Point는 아래와 같습니다.
디버깅 결과
- 테스트가 성공할때는 TestContainerConfig에 Point가 먼저 동작하며 테스트가 실패할때는 RedisConfig가 먼저 동작함을 알 수 있었습니다. 그리고 기본적으로 생성되는 아래에 테스트가 스프링 컨테이너가 동작하는 테스트 보다 먼저 동작 할때 테스트가 깨지는 것을 알 수 있었습니다.
@SpringBootTest
class BacklogApplicationTests {
@Test
void contextLoads() {
}
}
여기서 테스트에서 ApplicationContext를 테스트간에 공유한다는 내용이 떠올랐고 기본 테스트와 컨테이너를 사용하는 클래스가 Application Context를 공유하는지 테스트 하기위해 로그 레벨을 Debug로 조정하고 DefaultContextCache 클래스에 Break Point를 사용했습니다.
DefaultContextCache는 내부에 Map을 갖고 있어 테스트 환경을 Key, 사용되는 Context를 Value로 해서 환경이 똑같다면 Context를 새로 만들지 않고 보관및 반환하는 ContextCache의 구현체입니다.
private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap = new ConcurrentHashMap<>(32);
디버깅 결과 같은 Context를 공유하는 것을 확인했고, 기본 contextLoads() 테스트에서는 테스트 컨테이너 설정이 동작하지 않기때문에 레디스의 Host와 Port 가 기본값으로 설정되는 것을 확인할 수 있었습니다.
결론
ContextCache() 테스트가 먼저 실행될 때 기본 설정 값으로 포트와 호스트가 설정되고 Context를 공유하기 때문에 간헐적으로 실패하는 테스트가 발생합니다. Context를 분리해서 사용하게끔 설정하는 방법에 대해 알아보겠습니다.
@DynamicPropertySource를 사용해 동적으로 테스트 환경 속성을 설정해줍니다.
일반적으로 이 어노테이션은 테스트 시에 데이터베이스, 메시지 브로커, 또는 다른 인프라 요소의 주소와 같은 속성들을 동적으로 결정하고 설정할 필요가 있을 때 사용된다고 알고 있습니다.
@Testcontainers
public class TestContainerConfig {
private static final String REDIS_IMAGE = "redis:7.0.8-alpine";
private static final int REDIS_PORT = 6379;
private static final GenericContainer redis;
static {
redis = new GenericContainer(REDIS_IMAGE)
.withExposedPorts(REDIS_PORT)
.withReuse(true);
redis.start();
}
@DynamicPropertySource
private static void registerRedisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", () -> redis.getMappedPort(REDIS_PORT)
.toString());
}
}
'BE > Spring' 카테고리의 다른 글
[JPA] LazyLoading could not initialize proxy - no Session 문제 해결하기 (1) | 2023.10.30 |
---|---|
@DataJpaTest에서 @Repository 사용하기 (1) | 2023.09.06 |
@SpringBootApplication 이란? (0) | 2023.08.10 |
스프링부트3 Spring REST docs + Swagger UI 사용하기 (0) | 2023.08.04 |
Spring DataJPA 쿼리메서드 생성 조건 (0) | 2023.07.19 |