@TestConfiguration이 @SpringBootTest에서 Scan되지 않는 이유

H.J.SHIN·2024년 11월 12일
0

이슈

Spring에서 @SpringBootTest 어노테이션이 붙은 테스트 클래스를 실행할 때 @TestConfiguration이 붙은 설정 클래스는 자동으로 Bean 등록이 되지 않았다. 그래서 그 이유와 해결 방법에 대해 작성해보려고 한다.

package supernova.config;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import redis.embedded.RedisServer;
import redis.embedded.RedisServerBuilder;

import java.io.IOException;

@TestConfiguration
public class EmbeddedRedisConfig {
    private static final int REDIS_PORT = 6379;
    private static final String REDIS_HOST = "localhost";
    private RedisServer redisServer;

    @PostConstruct
    public void startRedis() throws IOException {
        redisServer = new RedisServerBuilder()
                .port(REDIS_PORT)
                .setting("maxmemory 128M")
                .build();
        redisServer.start();
        try {
            redisServer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @PreDestroy
    public void stopRedisServer() {
        if (redisServer != null) {
            redisServer.stop();
        }
    }

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://" + REDIS_HOST + ":" + REDIS_PORT);
        return Redisson.create(config);
    }
}

테스트를 수행할 때 실제 Redis 서버가 떠있지 않더라도 EmbeddedRedis를 통해 Redis 관련 테스트를 수행할 수 있는 환경을 구축하고 싶었다.

그리고 RedissonClient가 EmbeddedRedis에 connect하여 동시성 제어 또한 테스트하려고 하였다.

이 과정에서 테스트 환경에서 Bean 등록을 위한 Configuration으로 TestConfiguration이 있다는 사실을 알게되었다. 그래서 TestConfiguration을 src/test/java 패키지 아래에서 선언하도록 코드를 작성하였다.


@SpringBootTest

@SpringBootTest는 @SpringBootApplication이 선언된 매인 애플리케이션 클래스를 기준으로 모든 설정돠 Bean을 로드한다. 즉, @SpringBootApplication 하위 패키지를 모두 스캔하여 애플리케이션의 실제 Bean 환경을 재현하여 테스트 하기 위해 사용한다.


@SpringBootApplication

@SpringBootApplication은 @ComponentScan을 통해 @SpringBootApplication이 붙은 클래스 하위의 @Component를 스캔한다.

// @SpringBootApplication
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

....

}

@Configuration

@Configuration은 설정 클래스임을 나타내는 어노테이션으로 스프링 IoC 컨테이너가 관리하는 Bean을 생성하고, 구성하는데 사용한다.
@Configuration 어노테이션의 구성을 보면 @Component 어노테이션이 붙어있어 ComponentScan의 대상임을 알 수 있다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {

    .....
    
}

@TestConfiguration

@TestConfiguration은 @Configuration과 같이 Bean을 정의하지만, 테스트 환경에서만 사용하도록 설계되었다. 따라서 Springboot의 main이 아닌 test 패키지에서 사용한다. 즉, @SpringBootApplication의 하위 패키지에 포함되지 못한다.

그렇기 때문에 @TestConfiguration은 @Configuration과 다르게 SpringBootApplication의 하위 패키지가 스캔될 때 같이 스캔되지 못한다.

그럼에도 @TestComponent 또한 @Component을 포함하고 있기 때문에 명시적으로 Import한다면 test에서는 사용할 수 있다.


결론

@SpringBootTest는 @SpringBootApplication 하위 패키지를 모두 스캔하여 Bean을 등록한다. 하지만 @TestConfiguration은 테스트를 위한 설정파일이므로 @SpringBootApplication 하위 패키지에 포함되지 않는다. 따라서 자동으로 로드되지 못하며, 명시적으로 @Import해주어야 Bean을 등록할 수 있다.

원인을 알기위해 @TestComponent와 @Component를 찾아보고 했지만, 결론은 test 패키지와 main 패키지가 분리되어있기 때문이었다. 이런 기본적인 상식들을 간과한채 프로그래밍을 하다보면 문제가 발생했을 때 해결하는데 많은 시간이 소모되는 것 같다.

결국 TestConfiguration은 test 관련 라이브러리에 포함되어있으므로 src/test 하위에서만 사용할 수 있다. 그렇게 때문에 src/main/java 하위에 존재하는 SpringBootApplication 어노테이션의 스캔대상에 포함되지 않으므로 명시적으로 Import 하여야지만 사용할 수 있다.

@SpringBootTest
@Import(EmbeddedRedisConfig.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class RaceConditionTest {

...

}

기초적인 지식 부족으로 발생한 문제였지만, 테스트에서만 Bean으로 등록하고 싶은 설정들을 TestConfiguration으로 선언하면 실제 실행 환경에 영향을 주는 오류가 발생하지 않는다는 점은 알 수 있었다.

0개의 댓글