[Spring] TestContainers, @TestConfiguration 사용시 @DynamicPropertySource 가 적용되지 않는 문제와 해결방안

2025. 4. 10. 18:22·Spring

들어가며

@TestConfiguration 내부에서 Testcontainers와 @DynamicPropertySource를 사용해 테스트 환경을 구성했으나, @DynamicPropertySource가 의도대로 동작하지 않아 문제가 발생했다. 이 글은 해당 문제의 원인과 해결 방법을 정리한 내용이다. 

문제가 발생한 테스트 환경 

테스트환경에서는 TestContainers 를 이용해 Redis 를 사용하고 있다. RedisConnectionFactory 설정부분은 다음과 같다.

애플리케이션 코드의 RedisConfig 에서 생성자로 RedisProperties 를 주입받고 있고, 이 값을 바탕으로 Redis 서버와의 연결을 시도한다.

package org.springframework.boot.autoconfigure.data.redis;

@ConfigurationProperties(prefix="spring.data.redis")
public class RedisProperties {

    private String host = "localhost";

	...
        
    private int port = 6379;
}

RedisProperties 는 위와 같이 구성되어 있으며 Spring Boot의 AutoConfiguration 에 의해 등록된다.

따라서 spring-boot-starter-data-redis 의존성만 추가해주면 RedisConnectionFactory, RedisTemplate, StringRedisTemplate 등의 Bean이 자동 등록되고, 별다른 설정을 하지 않는다면 localhost 와 6379 번 포트로 연결된다.

 

다음으로 TestContainer 를 이용해 Redis 에 접속하는 부분을 @TestConfiguration 으로 정의했다.

registry.add("spring.data.redis.port", () -> REDIS_CONTAINER.getMappedPort(REDIS_PORT));

여기서 @DynamicPropertySource 를 이용해 spring.data.redis.port 에 해당하는 값을 TestContainers 가 할당해주는 랜덤포트 값으로 변경시켜주었다. (아마 여기까지 보고 문제의 원인을 파악하신 분들도 있을 것이다)

 

마지막으로 테스트 클래스에서 @Import 를 이용해 위에서 만든 (TestContainers 설정이 있는)  RedisTestConfig 를 Import 해, 

테스트 클래스에서 Redis 가 실행되도록 구성했다.

 

위처럼 구성한 이유는 아래와 같다.

  • TestRedisConfig를 여러 테스트 클래스에서 @Import 로 등록하더라도, static 블록은 클래스 로딩 시 한 번만 실행되므로 컨테이너가 중복 생성되지 않는다. 따라서  테스트 클래스가 끝나더라도 REDIS_CONTAINER는 JVM이 유지되는 한 살아있기 때문에 컨테이너가 내려갔다 올라가는 시간이 추가로 발생하지 않는다.
  • @TestConfiguration은 @Configuration 과 달리 컴포넌트 스캔 대상이 아니다. @Import 로 등록하던가 테스트 클래스 내부에서 static inner class 로 정의해야 한다. Redis 를 사용하는 테스트들에서 재사용할 필요가 있어서 별도 클래스로 분리했다.

문제 상황

 

위에서처럼 테스트환경을 구성하고 돌려보면 호스트의 6379 번 포트로 접속을 시도한다.

TestContainers 의 hostPort 는 63341 번으로 매핑이 되어 실행되고 있기 때문에 접속을 할 수 없어 위처럼 에러로그가 출력된다.

 

이전에도 TestContainers 를 이용해 통합테스트 환경을 구성한 적이 있었다. 그 당시에는 IntegrationTest 라는 abstract 클래스를 만들어놓고 통합테스트 클래스들에서 상속받아 동작하도록 구성했고, 정상적으로 동작했다. 그러면 지금 상황에서 동작하지 않는 이유는 무엇일까?

 

디버그를 돌려보니

 

@TestConfiguration 클래스가 JVM에 로드되면서 static 블록이 실행된다. (<clinit> ) 이후 Spring 컨텍스트가 초기화되며 @Configuration 에 의해 빈이 등록되면서 redisConnectionFactory 메서드가 실행되고 host, port 가 매핑된다.

@DynamicPropertySource 가 붙은 configureProperties 메서드는 실행조차 되지 않았다.

원인 및 해결 방법

필자가 간과한 것이 있는데, @Configuration, @TestConfiguration 은 사용될 빈을 등록하기 위해 사용된다. @DynamicPropertySource 는 빈 등록을 위한 것이 아니기 때문에 (아래에서 나오겠지만, 그보다 더 일찍 처리된다.) @Configuration이나 @TestConfiguration 내부에 있다고 해서 즉, @Import(TestRedisConfig.class) 만 한다고 해서 내부의 @DynamicPropertySource가 호출되지 않는다.  따라서 목적에 맞지 않게 사용한 것이다. 그렇다면 올바르게 사용하려면 어떤식으로 해야 할까?

 

먼저 @DynamicPropertySource 가 어떻게 동작하는지 확인해보았다.

실제로 확인해보기 위해 테스트클래스에 @DynamicPropertySource 가 붙은 메서드에 브레이크포인트를 걸고 디버깅을 진행했다. 디버거는 코드를 중단점으로 안내한 실행경로를 역순으로 표시하므로, 보기 편하게 시작지점부터 나열해 보았다.

 

SpringApplication 의 run 이 실행되고, prepareContext 를 호출하는 부분부터 시작한다.

 

여기서 createApplicationContext() 로 만든 context 를 넣어 applyInitializers 메서드를 호출하게 되고

 

getInitializers() 로 ApplicationContextInitializer 를 구현한 클래스들을 가져와 initialize 를 하나씩 호출한다.

 

이중 ContextCustomizerAdapter 클래스의 initalize 가 호출되면 ContextCustomizer 를 구현한 클래스의 customizeContext 메서들을 호출한다.  

 

DynamicPropertiesContextCustomizer 의 customizeContext 가 호출된 것을 확인할 수 있다. methods 변수에는 테스트 클래스에서 찾은 @DynamicPropertySource가 붙은 static 메서드가 저장되어 있다. (즉, configureProperties 메서드) Spring의 Environment는 여러 개의 PropertySource( application.yml, system properties 등)를 계층적으로 가지고 있다. 여기서는 그 중 MutablePropertySources를 가져와 추가해주는 구성인데 addFirst(...) 를 이용해 가장 우선순위가 높은 위치에 새 PropertySource를 삽입한다.  여기서는 DynamicValuesPropertySource 가 삽입되는데 두번째 인자인 Map<String, Supplier<Object>> 에 buildDynamicPropertisMap() 메서드의 호출결과를 넘겨준다.

 

buildDynamicPropertiesMap() 메서드에서는 결과를 담을 map 을 생성한 후 DynamicPropertyRegistry 를 람다 표현식으로 정의해, ReflectionUtils 의 invokeMethod 의 두 번째 파라미터인 args 에 넣어서 호출한다.  (target이 null인 이유는, 호출하려는 @DynamicPropertySource 메서드가 static이기 때문이다.)

 

method.invoke(target,args) 는 Java Reflection API 의 메서드로 Method 객체를 런타임에 동적으로 호출한다. (여기서는 configureProperties 가 호출)  args 에는 위에서 구성한 dynamicPropertyRegistry 가 담긴다.

 

configureProperties 메서드에서 registry.add(...)가 실행되면, 위에서 만든 map (Map<String, Supplier<Object>>)에 키-값 쌍이 등록된다.

 

따라서 "spring.data.redis.port" 가 name 으로 등록되고 valueSupplier 부분에 들어갈 람다표현식이 () -> REDIS_CONTAINER.getMappedPort(REDIS_PORT) 에 해당한다. 결론적으로 동적 프로퍼티가 Environment에 반영되므로, 테스트 시점에서 @Value, @ConfigurationProperties 등으로 주입이 가능해진다.

 

prepareContext()는 아직 빈들이 생성되기 전에, 테스트 설정이나 프로파일 적용, 초기 설정자, 커스터마이저들(ApplicationContextInitializer 등) 을 컨텍스트에 반영하는 사전작업을 하는 단계다.  @TestConfiguration 이나 @Configuration 클래스는 prepareContext()에서는 아직 등록되지 않고, refreshContext() 호출 이후에 BeanDefinition으로 등록되며, 그 안의 @Bean 메서드들이 처리되기 시작한다.

 

@Configuration 이 붙은 redisConnectionFactory 빈은 prepareContext() 에서 프로퍼티 설정이 끝난 이후에 실행되는 것을 확인할 수있다.

 

따라서, 올바르게 등록하려면 @SpringBootTest 의 ApplicationContext 의 prepareContext() 부분에서 설정한 변수값이 들어가야 하기 때문에 @DynamicPropertySource 붙은 메서드는 는 테스트 클래스 혹은 상위 클래스에 정의되어야 prepareContext() 에서 값을 정상적으로 설정할 수 있다.

 

과정을 요약해보면 아래와 같다.

  • 테스트 클래스에서 @DynamicPropertySource 존재 확인
  • DynamicPropertiesContextCustomizer가 해당 메서드를 수집
  • 수집된 메서드는 DynamicPropertiesContextCustomizer에 담김 (Set<Method>)
  • ContextCustomizerAdapter로 감싸짐 ( ApplicationContextInitializer<ConfigurableApplicationContext> 로 등록되기 위해)
  • ApplicationContext 초기화 준비 (loadContext())
  • applyInitializers()에서 ContextCustomizerAdapter.initialize() 호출
  • 메서드 호출 시 DynamicPropertyRegistry를 통해 테스트 설정값이 Environment에 실제로 등록됨
  • 이후 refresh() 호출되며 실제 빈 생성 및 테스트 실행 시작

 

@TestConfiguration 내에서 TestContainers 설정 방법

그렇다면 @TestConfiguration 내에서 TestContainers 설정을 할 수 있는 방법은  없을까? 

static {
    REDIS_CONTAINER = new GenericContainer<>(REDIS_DOCKER_IMAGE_TAG)
            .withExposedPorts(REDIS_PORT);
    REDIS_CONTAINER.start();
    System.setProperty("spring.data.redis.port", REDIS_CONTAINER.getMappedPort(REDIS_PORT)
            .toString());
}

static 블록을 활용해서 설정할 수 있다. static 블록은 JVM 클래스 로딩 시점에 한 번만 실행되고, 스프링 컨텍스트나 refresh()와는 무관하게 클래스가 처음 로딩되는 순간에 바로 수행되므로 가장 먼저 실행되게 된다. REDIS_CONTAINER.start() 호출 아래에서 getMappedPort 가져와서 설정해주면 된다.

 

References

  • https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-dynamicpropertysource.html
  • https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/context/DynamicPropertySource.html
  • https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/ReflectionUtils.html
  • https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#setProperties-java.util.Properties-

'Spring' 카테고리의 다른 글

[Spring Security] Preflight는 성공하지만 실제 요청에서 CORS 오류가 발생하는 문제 해결하기  (0) 2025.05.26
[Spring] WireMock 로 Feign (ApacheHttp5Client) 테스트시 NoHttpResponseException 발생 케이스와 해결방법  (0) 2025.05.06
[Spring] API 응답에서 직접 정의한 Error code 는 왜 사용할까?  (0) 2025.03.16
[Spring] OpenApI 3.0 Swagger 문서 작성 및 설정 방법  (1) 2025.02.11
[SpringBoot] MapStruct 제대로 활용하기  (0) 2024.11.03
'Spring' 카테고리의 다른 글
  • [Spring Security] Preflight는 성공하지만 실제 요청에서 CORS 오류가 발생하는 문제 해결하기
  • [Spring] WireMock 로 Feign (ApacheHttp5Client) 테스트시 NoHttpResponseException 발생 케이스와 해결방법
  • [Spring] API 응답에서 직접 정의한 Error code 는 왜 사용할까?
  • [Spring] OpenApI 3.0 Swagger 문서 작성 및 설정 방법
기억은 RAM, 기록은 HDD
기억은 RAM, 기록은 HDD
  • 기억은 RAM, 기록은 HDD
    적립식 개발
    기억은 RAM, 기록은 HDD
  • 전체
    오늘
    어제
    • 분류 전체보기 (45)
      • Gradle (1)
      • 알고리즘 (14)
        • 강한 연결 요소 (1)
        • BFS (1)
        • 다이나믹 프로그래밍 (2)
        • 그리디 (1)
        • 투 포인터 (2)
        • 비트마스크 (1)
        • 스택 (1)
        • 백트래킹 (1)
        • 유니온-파인드 (1)
        • 기초 기하학 (1)
        • 분할정복을 이용한 거듭제곱 (1)
        • 볼록 껍질 (1)
      • JPA (3)
      • Java (9)
      • Spring (9)
      • Git&GitHub (2)
      • Infra (4)
  • 최근 글

  • 인기 글

  • 태그

    자바 callable
    @embedded
    enum정리
    spring cors 해결
    투 포인터
    자바synchronized키워드
    정렬
    thread 와 runnable
    enum활용법
    java
    java synchronized
    자바future
    Github
    비트마스킹
    자바 runnable
    완전탐색
    java 라이브러리 추가
    기하학
    데몬쓰레드
    githubworkflow
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.1
기억은 RAM, 기록은 HDD
[Spring] TestContainers, @TestConfiguration 사용시 @DynamicPropertySource 가 적용되지 않는 문제와 해결방안
테마상단으로

티스토리툴바