Home @SpringBootConfiguration
Post
Cancel

@SpringBootConfiguration

스프링 부트의 @SpringBootConfiguration을 학습하며 작성된 글입니다. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.

image







1. @SpringBootConfiguration


@SpringBootApplication 어노테이션을 살펴보면, 여기에는 @SpringBootConfiguration이 포함되어 있습니다. 이 어노테이션은 스프링 부트에서 사용되는 특별한 어노테이션으로, @Configuration을 확장한 형태입니다. 일반적으로 이는 애플리케이션을 실행하는 메인 클래스, 즉 핵심 설정 클래스에 사용되며, @SpringBootConfiguration이 붙은 클래스는 애플리케이션의 주요 동작을 정의하고 관리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@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을 확장한다고 설명했는데요, 이는 @Configuration의 기능을 포함합니다. 즉 해당 어노테이션이 붙은 클래스를 빈으로 등록해 줍니다. 이를 확인하기 위해 @SpringBootApplication이 붙은 클래스와 별개로 아래와 같은 클래스를 만든 후 애플리케이션을 실행시켜 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootConfiguration
public class TestConfiguration {

    private static final Logger log = LoggerFactory.getLogger(TestConfiguration.class);

    public TestConfiguration() {
        log.info("---------------------------------------------------------------------------");
        log.info("TEST");
        log.info("---------------------------------------------------------------------------");
    }

}









그러면 아래와 같이 @SpringBootConfiguration 빈을 붙인 클래스가 빈으로 등록된 것을 볼 수 있습니다. 해당 어노테이션이 붙은 클래스는 빈으로 @Configuration과 마찬가지로 빈으로 등록되며, 이는 @SpringBootApplication과 함께 사용될 수 있습니다.

image

물론 이를 사용하는 것은 괜찮지만 스프링 부트가 설정한 다양한 설정과 얽힐 수 있기 때문에 일반적으로 한 프로젝트에서는 하나의 @SpringBootConfiguration만 사용하는 것을 권장드립니다.









2. 어디에 사용될까?


대표적으로 테스트 코드에서 테스트 문맥을 구성할 때 해당 어노테이션이 사용됩니다. 통합 테스트를 실행하면 이는 우리가 작성한 애플리케이션의 context를 읽어와 테스트 환경을 구성하는데, 이 과정에서 우리가 만든 애플리케이션의 context를 읽어올 때 사용하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {

    ......

    // 우리가 작성한 애플리케이션의 context를 읽어 테스트를 위한 문맥 구성
    @Override
    public TestContext buildTestContext() {
        TestContext context = super.buildTestContext();
        verifyConfiguration(context.getTestClass());
        WebEnvironment webEnvironment = getWebEnvironment(context.getTestClass());
        if (webEnvironment == WebEnvironment.MOCK
            && deduceWebApplicationType() == WebApplicationType.SERVLET) {
            context.setAttribute(ACTIVATE_SERVLET_LISTENER, true);
        } else if (webEnvironment != null && webEnvironment.isEmbedded()) {
            context.setAttribute(ACTIVATE_SERVLET_LISTENER, false);
        }
        return context;
    }
    
    ......
    
}

하지만 @SpringBootConfiguation은 우리가 직접적으로 사용할 일은 사실 거의 없습니다. 대부분 스프링부트가 해당 어노테이션을 읽어 자동으로 설정을 해주기 때문입니다.









테스트에서 해당 어노테이션을 어디서 읽는지 한 번 살펴보겠습니다. 이는 SpringBootTestContextBootstrapper 클래스의 findConfigurationClass에서 context를 읽기 위해 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {

    ......

    private Class<?> findConfigurationClass(Class<?> testClass) {
        String propertyName = "%s.SpringBootConfiguration.%s"
            .formatted(SpringBootTestContextBootstrapper.class.getName(), testClass.getName());
        String foundClassName = this.aotTestAttributes.getString(propertyName);
        if (foundClassName != null) {
            return ClassUtils.resolveClassName(foundClassName, testClass.getClassLoader());
        }
        
        // @SpringBootConfiguration가 붙은 클래스를 조회
        Class<?> found = new AnnotatedClassFinder(SpringBootConfiguration.class).findFromClass(
            testClass);
        Assert.state(found != null, "Unable to find a @SpringBootConfiguration, you need to use "
            + "@ContextConfiguration or @SpringBootTest(classes=...) with your test");
        this.aotTestAttributes.setAttribute(propertyName, found.getName());
        return found;
    }
    
    ......
    
}









해당 어노테이션이 없다면 테스트를 위한 문맥을 구성할 수 없습니다. 이를 테스트하기 위해 아래와 같이 @SpringBootApplication을 주석처리하고 테스트를 실행해 보겠습니다.

1
2
3
4
5
6
7
8
//@SpringBootApplication
public class StudySpringBootApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudySpringBootApplication.class, args);
    }

}









그러면 다음과 같은 예외가 발생하는 것을 볼 수 있는데, 테스트를 위한 문맥을 구성할 수 없는 것입니다.

image









이는 Assert.state에서 발생한 예외로, context 구성을 위해 @SpringBootConfiguration이 붙은 클래스를 조회했지만, 이를 발견하지 못해 발생한 예외입니다.

image









코드 레벨에서 보면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {

    ......

    private Class<?> findConfigurationClass(Class<?> testClass) {
        String propertyName = "%s.SpringBootConfiguration.%s"
            .formatted(SpringBootTestContextBootstrapper.class.getName(), testClass.getName());
        String foundClassName = this.aotTestAttributes.getString(propertyName);
        if (foundClassName != null) {
            return ClassUtils.resolveClassName(foundClassName, testClass.getClassLoader());
        }
        
        // @SpringBootConfiguration가 붙은 클래스를 조회
        Class<?> found = new AnnotatedClassFinder(SpringBootConfiguration.class).findFromClass(
            testClass);
        
        // 찾지 못하면 예외 발생
        Assert.state(found != null, "Unable to find a @SpringBootConfiguration, you need to use "
            + "@ContextConfiguration or @SpringBootTest(classes=...) with your test");
        this.aotTestAttributes.setAttribute(propertyName, found.getName());
        return found;
    }
    
    ......
    
}









3. 의문점


여기서 두 가지 의문점이 들었는데, @SpringBootApplication을 주석처리 했을 때의 테스트가 될까?, @SpringBootApplication을 주석처리 한 채 @SpringBootConfiguation만 사용하면 테스트가 정상적으로 돌아갈까?였습니다. 우선 결론부터 말하면 둘 다 실행되지 않습니다.

image

image









첫 번째 케이스를 알기 위해서는 어디서 basePackage를 읽는지 알아야 합니다. 스프링 부트에서 테스트가 실행되면 우선 @SpringBootConfiguation이 붙은 클래스를 찾아 context를 읽어옵니다. 이후 해당 클래스를 찾으면 ComponentScanAnnotationParser를 통해 basePackage를 찾습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ComponentScanAnnotationParser {

    ......

    public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan,
        
        ......

        Set<String> basePackages = new LinkedHashSet<>();
        String[] basePackagesArray = componentScan.getStringArray("basePackages");
        for (String pkg : basePackagesArray) {
            String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            Collections.addAll(basePackages, tokenized);
        }
        ......

    }

}









아래와 같이요. 따라서 @SpringBootApplication이 주석처리되면 테스트 자체를 돌릴 수 없습니다. @SpringBootConfiguration 어노테이션을 읽지를 못하니까요.

image









두 번째 경우도 비슷한데, @SpringBootApplication 어노테이션은 아래와 같이 복합적인 어노테이션으로 구성되어 있습니다. @SpringBootConfiguration 하나만 있다고 동작하는 것이 아니라 @EnableAutoConfiguration, @ComponentScan 등을 통해 빈으로 등록된 클래스를 읽어와 context를 형성하는데, @SpringBootApplication 없이 @SpringBootConfiguration만으로는 이런 문맥을 만들 수 없습니다. 따라서 두 가지 모두 테스트가 실행되지 않습니다.

1
2
3
4
5
6
7
8
@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) })









4. 정리


이 어노테이션은 스프링 부트에서 사용되는 특별한 어노테이션으로, @Configuration을 확장한 형태입니다. 일반적으로 이는 애플리케이션을 실행하는 메인 클래스, 즉 핵심 설정 클래스에 사용되며, @SpringBootConfiguration이 붙은 클래스는 애플리케이션의 주요 동작을 정의하고 관리합니다. 이는 우리가 직접 사용할 일은 거의 없으며, 테스트에서 애플리케이션의 context를 읽어올 때 사용됩니다.


This post is licensed under CC BY 4.0 by the author.

Dao와 Repository의 차이점은 무엇일까?

Observer Pattern