java 스프링 부트 @SpringBootApplication 깊게 정리

내가 참여한 스프링 부트 토이 팀프로젝트 분석 시작

 

 

 

2년전에 나는 휴학하는 동안 팀 프로젝트에 참여했었다.

그 후 2년동안 국가의 부름을 받고 복무를 하는 동안 틈틈이 자바 스프링말고 다른 영역을 공부해봤다. 복무와 공부를 마치고 자바 스프링이 적성에 제일 맞다고 판단하였고 스프링에 대해 더 자세히 공부하고 싶어서 돌아왔다.

공부할 순서 : 스프링 부트(현재) -> 스프링 프레임워크(최종테크트리) //(chatgpt 활용도 곁들여서)

다른 언어에 비해 자바는 코드를 깊이있게 해석할 수 있어 안전성과 유지보수성이 뛰어났다. 그러한 매력적인 점 또한 날 끌어들인 요인이였다.

따라서 2년이 지난 지금에서야 해당 팀프로젝트를 분석한다.

이미 많은 시간이 지났으므로 디테일하게 분석하지는 못하지만

기초부터 분석하여

내가 무엇을 사용해서 어떤 것을 만든 건지 정확하게 알아보고

해결하지 못한 것은 무엇인지 파악하고

알아내지 못한 것을 기록하여

다음번 프로젝트를 시작할때 그 부분에 대해

제대로 알고 있는 상태로 프로젝트를 완성하고

해당 프로젝트를 제대로 분석할 수 있도록

깊이 있는 공부가 가능하도록

해당 프로젝트 분석을 시작한다.

 

 

 

package RandomChatting;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}
 

위의 코드가 애플리케이션의 시작부분이다.

가장 먼저 보이는 것은 import

import는 다른 패키지에 들어있는 클래스를 사용할 수 있게 해주는 키워드다.

위와 같은 경우는 패키지 형태의 클래스 라이브러리를 import하는 경우이다.

첫번째 import는 SpringApplication 클래스를 imoort하고 있고

public class SpringApplication {

 

두번째 import는 @interface 즉 커스텀으로 만든 합성 어노테이션인 SpringBootApplication 어노테이션 클래스를 import하고 있다.

public @interface SpringBootApplication {

 

이 두줄은 애플리케이션 Spring Initializr가 시작 애플리케이션 클래스를 만들때 애플리케이션 실행문과 함께 넣어주는 import 문장이다.

/*

(Spring Initializr란 dependency(의존성)같은 설정 몇개를 원하는 방식으로 커스텀하면 자동으로

스프링 부트 프로젝트 생성해주는 프로그램이다.

보통 이것을 이용하여 스프링 부트 프로젝트를 제작하기 시작한다.

밑의 사이트에 접속하여 사용할 수 있다.)

https://start.spring.io/

*/

해당 라이브러리들을 import함으로써 해당 클래스들을 사용할 수 있다.

사용하는 경우가 바로 나오는데 다음 문장인 실행문에 붙어있는 어노테이션인 @SpringBootApplication이다.

이번 글에서는 @SpringBootApplication에 대해서 알아보도록 한다.

 

 

 


 

 

 

@SpringBootApplicaion

@SpringBootApplication는 위에서 import한 라이브러리중 하나인 클래스의 합성 어노테이션이다.

내부의 코드를 확인해보면 아래와 같다.

/**
 * Indicates a {@link Configuration configuration} class that declares one or more
 * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
 * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
 * annotation that is equivalent to declaring {@code @Configuration},
 * {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @author Andy Wilkinson
 * @since 1.2.0
 */
@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 {

    /**
     * Exclude specific auto-configuration classes such that they will never be applied.
     * @return the classes to exclude
     */
    @AliasFor(annotation = EnableAutoConfiguration.class)
    Class<?>[] exclude() default {};

    /**
     * Exclude specific auto-configuration class names such that they will never be
     * applied.
     * @return the class names to exclude
     * @since 1.3.0
     */
    @AliasFor(annotation = EnableAutoConfiguration.class)
    String[] excludeName() default {};

    /**
     * Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
     * for a type-safe alternative to String-based package names.
     * <p>
     * <strong>Note:</strong> this setting is an alias for
     * {@link ComponentScan @ComponentScan} only. It has no effect on {@code @Entity}
     * scanning or Spring Data {@link Repository} scanning. For those you should add
     * {@link org.springframework.boot.autoconfigure.domain.EntityScan @EntityScan} and
     * {@code @Enable...Repositories} annotations.
     * @return base packages to scan
     * @since 1.3.0
     */
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

    /**
     * Type-safe alternative to {@link #scanBasePackages} for specifying the packages to
     * scan for annotated components. The package of each class specified will be scanned.
     * <p>
     * Consider creating a special no-op marker class or interface in each package that
     * serves no purpose other than being referenced by this attribute.
     * <p>
     * <strong>Note:</strong> this setting is an alias for
     * {@link ComponentScan @ComponentScan} only. It has no effect on {@code @Entity}
     * scanning or Spring Data {@link Repository} scanning. For those you should add
     * {@link org.springframework.boot.autoconfigure.domain.EntityScan @EntityScan} and
     * {@code @Enable...Repositories} annotations.
     * @return base packages to scan
     * @since 1.3.0
     */
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    Class<?>[] scanBasePackageClasses() default {};

    /**
     * The {@link BeanNameGenerator} class to be used for naming detected components
     * within the Spring container.
     * <p>
     * The default value of the {@link BeanNameGenerator} interface itself indicates that
     * the scanner used to process this {@code @SpringBootApplication} annotation should
     * use its inherited bean name generator, e.g. the default
     * {@link AnnotationBeanNameGenerator} or any custom instance supplied to the
     * application context at bootstrap time.
     * @return {@link BeanNameGenerator} to use
     * @see SpringApplication#setBeanNameGenerator(BeanNameGenerator)
     * @since 2.3.0
     */
    @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    /**
     * Specify whether {@link Bean @Bean} methods should get proxied in order to enforce
     * bean lifecycle behavior, e.g. to return shared singleton bean instances even in
     * case of direct {@code @Bean} method calls in user code. This feature requires
     * method interception, implemented through a runtime-generated CGLIB subclass which
     * comes with limitations such as the configuration class and its methods not being
     * allowed to declare {@code final}.
     * <p>
     * The default is {@code true}, allowing for 'inter-bean references' within the
     * configuration class as well as for external calls to this configuration's
     * {@code @Bean} methods, e.g. from another configuration class. If this is not needed
     * since each of this particular configuration's {@code @Bean} methods is
     * self-contained and designed as a plain factory method for container use, switch
     * this flag to {@code false} in order to avoid CGLIB subclass processing.
     * <p>
     * Turning off bean method interception effectively processes {@code @Bean} methods
     * individually like when declared on non-{@code @Configuration} classes, a.k.a.
     * "@Bean Lite Mode" (see {@link Bean @Bean's javadoc}). It is therefore behaviorally
     * equivalent to removing the {@code @Configuration} stereotype.
     * @since 2.2
     * @return whether to proxy {@code @Bean} methods
     */
    @AliasFor(annotation = Configuration.class)
    boolean proxyBeanMethods() default true;

}

 

먼저 가장 위의 javadoc 주석을 확인해보면

하나 이상의 @Bean 메서드를 선언하고 auto-configuration과 component scanning을 작동시키는

구성클래스를 나타낸다

이는 @Configuration,@EnableAutoConfiguration과 @ComponentScan을 선언하는 것과 동일한

편리한 어노테이션이다

라고 나와있다.

@SpringBootApplication이 @Configuration,@EnableAutoConfiguration과 @ComponentScan 3개를 대신할 수 있다는 뜻이다.

다음으로 코드의 어노테이션 부분을 확인해보면

@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 {

 

여러 어노테이션이 있는데 이 어노테이션들은 합성 어노테이션을 만드는데 도움이 되는 메타 어노테이션들이다.

@Target은 해당 합성 어노테이션이 적용될 대상을 지정하는 어노테이션(대상: Type=>클래스 및 인터페이스)

@Retention은 해당 합성 어노테이션이 적용된 대상의 메모리의 유지기간을 결정하는 어노테이션(기간: Runtime=>클래스파일에 포함되고 jvm이 로드해서 사용)

@Documented는 해당 합성 어노테이션의 정보가 javadoc 문서에 포함하도록 하는 어노테이션

@Inherited는 해당 합성 어노테이션을 상속 가능하도록 만드는 어노테이션이다.

그 밑에는 위의 javadoc 주석에서 말한 @SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan이 있다.

주석에서 설명한대로 해당 어노테이션을 메타 어노테이션으로 가짐으로써 3개의 역할을 수행할 수 있는 것이다.

해당 어노테이션들을 분석해보도록 하자.

 

 

 


 

 

 

1. @SpringBootConfiguration

@Configuration가 메타 어노테이션으로 달린 어노테이션,.. javadoc 주석에 따르면 @Configuration과 동일한 역할을 한다고 한다.

@SpringBootConfiguration은 스프링 부트에서 사용하는 @Configuraion이라 할 수 있는 것이다.

 

해당 어노테이션 클래스의 내부 코드를 살펴보면 아래와 같다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {

    @AliasFor(annotation = Configuration.class)
    boolean proxyBeanMethods() default true;

}

 

기본 제공 메타 어노테이션들을 제외한다면

@Configuration과 @Indexed 이 녀석들이 남는데

@Indexd는 Redis의 보조 인덱스 생성에 사용되는 어노테이션이라고 한다.

//(Redis란 메모리에 올려서 사용할 수 있는 키-값 데이터베이스 서버다.)

실행문 쪽을 보면 @AliasFor(annotation = Configuration.class) 의 문장으로

@Configuration의 데이터를 가져오는 것으로 주석의 내용처럼

@Configuration과 같은 역할을 수행한다는 것을 알 수 있다.

@Configuration 내부의 코드를 살펴보면

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
	@AliasFor(annotation = Component.class)
	String value() default "";
    
	boolean proxyBeanMethods() default true;
}

 

위와 같은데 @Component를 달고있다.

@AliasFor(annotation = Component.class)로 @Component의 데이터를 가지고 오는 것으로 역할을 이어받고 있다.

@Configuration이 @Component의 역할을 받아 @Configuration과 @SpringBootConfiguration의 관계처럼 서로 비슷한 어노테이션인 것 처럼 보이지만 둘은 차이가 있는 어노테이션이다.

내부의 메소드 요소 proxyBeanMethods()의 javadoc 주석에 따르면 proxyBeanMethods로 CGLIB를 사용해 프록시 메서드(대리 메서드 - 기존과 같은 역할을 수행하는 메서드)를 만들어 싱글톤으로 bean을 등록해 사용해준다고 나와있다. 즉 싱글톤 패턴을 보장해주는 차이가 있는 것이다.

//(스프링 버전에 따라서는 싱글톤 패턴 설정을 바꿀 수도 있다고도 한다. 추후에 지식이 늘어나면 알아보자.)

또한 사용방식도 차이가 있는데 @Bean과 @Configuration 그리고 @Component의 javadoc 주석을 추가로 참고해보면

@Component는 클래스에서 선언하여 bean으로 등록할 대상이 하나의 "클래스"가 되지만 @Configuration은 @Configuration이 달린 하나의 클래스 안에 여러 @Bean을 선언하여 여러 메서드를 bean으로 등록하는 방식으로 @Configuration을 선언한 클래스의 내부에 있는 @Bean이 달린 "메서드"가 등록 대상이 된다고 한다. 이후 둘다 @ComponentScan에 걸려 @ComponentScan내부에서 bean으로 등록이 되는 것이다.

위와 같은 차이로 @Configuration 과 @Bean은 특수한 상황에서 사용하는데

직접 제어가 어려운 라이브러리나 애플리케이션 전범위에서 사용되는 클래스 같은 곳에서 사용되곤한다.

/*

(스프링 bean이란 스프링 컨테이너에 의해 관리되는 재사용 가능하도록 인스턴스화된 자바 객체이다. 사용자가 커스텀하는 설정이라고 이해하면 된다. 스프링 bean 등록방법은 @Configuration@Bean/@Component등의 어노테이션 이외에도 XML도 있지만 XML을 주로 다루는 스프링 프레임워크 bean 파트 공부할 때 같이 공부하도록 한다.)

(@Component를 스캔하기 위해 사용되는 @ComponentScan은 위에서 본 @SpringBootApplication에 붙어있던 3가지 어노테이션 중 하나이다. @SpringBootApplication안에 넣어놓음으로써 애플리케이션을 시작하며 시작파일을 중심으로@Component와 친구들을 스캔해주는 것이다. 자세한 것은 밑의 "2. @ComponentScan" 부분에서)

*/

 

 

위와 같이 @Configuration과 @Component처럼 차이가 나는 것도 아닌데 @Configuration의 역할을 거의 그대로 이어받는 @SpringConfiguration을 스프링부트에서 따로 만들어 사용해준 이유는 무엇인가..?

그러한 이유를 @SpringBootTest에서 확인할 수 있다는 힌트를 스택 오버 플로우에서 얻었다.

https://stackoverflow.com/questions/69851544/what-does-it-mean-by-springbootconfiguration-allows-the-configuration-to-be-fou

//(@SpringBootConfiguraation과 @Configuration의 차이를 알아보다가 찾게되었다.)

 

여담이지만 우리가 만든 프로젝트에서는 내부의 스프링 테스트 파일을 삭제하고 외부로 돌려 같은 환경을 만들어 테스트를 해봤기 때문에 해당 프로젝트 내부에 @SpringBootTest가 남아있지 않았다. 따라서 다른 프로젝트의 @SpringBootTest내부에 들어가서 확인해봤다. //(결과적으로 테스트는 그냥 내부에서 조율하면서 테스트하는게 좋은 것 같다.)

@SpringBootTest 내부 코드를 간단히만 확인해보면

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
public @interface SpringBootTest {

 

위와 같이 나와있는데 어노테이션에서 보여주듯이

2개의 클래스중 하나에서 원하는 답을 얻을 수 있으리라 생각 되었다.

이름을 달리했기 때문에 @SpringBootConfiguration를 특정해서 찾아 무언가를 할 것이란 추측을 해서 키워드를 "찾다"와 "가져오다"의 find와 get으로 잡았는데 다행히 find로 바로 나왔다.

//(이래서 변수명이나 함수명이 매우 중요한 듯 하다.)

이후 해당 코드를 사용하는 다른 코드에서 어떻게 사용하는지 확인했다.

@SpringBootTestContextBootstrapper 클래스에서 다음과 같은 코드를 찾을 수 있었다.

    protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) {
        Class<?>[] classes = this.getOrFindConfigurationClasses(mergedConfig);
        List<String> propertySourceProperties = this.getAndProcessPropertySourceProperties(mergedConfig);
        MergedContextConfiguration mergedConfig = this.createModifiedConfig(mergedConfig, classes, StringUtils.toStringArray(propertySourceProperties));
/////생략/////
///중간생략///
/////생략/////
protected Class<?>[] getOrFindConfigurationClasses(MergedContextConfiguration mergedConfig) {
    Class<?>[] classes = mergedConfig.getClasses();
    if (!this.containsNonTestComponent(classes) && !mergedConfig.hasLocations()) {
        Class<?> found = this.findConfigurationClass(mergedConfig.getTestClass());
        Log var10000 = logger;
        String var10001 = found.getName();
        var10000.info("Found @SpringBootConfiguration " + var10001 + " for test " + mergedConfig.getTestClass());
        return this.merge(found, classes);
    } else {
        return classes;
    }
}
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());
    } else {
        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;
    }
}

 

해당 코드들을 살펴보자면 getOrFindConfigurationClasses()에서 findConfigurationClass()를 사용해서

@SpringBootConfiguration을 가진 클래스를 찾은 다음 getOrFindConfigurationClasses() 메소드에게

결과값(찾아낸 클래스나 그러한 클래스를 못찾았다는 결과)을 전달해서 processMergedContextConfiguration()에서 해당 클래스를 중심으로 설정들을 찾아 가져오는 방식이다.

즉, @SpringBootConfiguration을 중심으로 Configuration들을 찾아보는 방식을 위해 이름을 달리하여 사용하였다.

이러한 메서드들을 보아 @SpringBootConfiguration과 @Configuration을 구분할 필요가 있는 것이 확인 되었다.

 

 

정리하자면 bean으로 등록한다는 표시를 해주는 @Configuration의 역할을 이어받은 @SpringBootConfiguration이 @SpringBootApplication에 붙어있다는 것은 스프린 컨테이너 부분에서 처리해야 하는 하나 이상의 configuration @Bean 메서드가 합성 어노테이션인 @SpringBootApplication 내부에 포함되어 있음(@SpringBootApplcation javadoc 주석에 나와있는 것 처럼)을 나타낸다. 또한 @SpringBootConfiguration과 @Configuration의 이름을 달리해줌으로써 해당위치를 특정하여 설정찾기의 중심으로 삼았다.

 

 

 

2. @ComponentScan

javadoc 주석에 따르면 @Configuration과 Component들을 스캔하는 역할을 한다고 한다.

 

내부 코드를 탐색해보면

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

    String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

    boolean useDefaultFilters() default true;

    Filter[] includeFilters() default {};

    Filter[] excludeFilters() default {};

    boolean lazyInit() default false;

    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    @interface Filter {

       FilterType type() default FilterType.ANNOTATION;

       @AliasFor("classes")
       Class<?>[] value() default {};

       @AliasFor("value")
       Class<?>[] classes() default {};

       String[] pattern() default {};

    }

}

 

위와 같다. 코드를 보면 여러가지 요소들이 있는데 javadoc 주석에 따르면 

스캔할 패키지를 담아두는 듯 하는 basePackages와 basePackageClasses나 스캔대상에서 제외해야할 것들을 정해놓는 필터, 필터의 세부 설정들 그리고 스캔 범위 등이 있다.

 

스캔, 필터와 관련된 정보가 필요하거나

오류가 났을때 각 오류에 해당하는 부분을 확인해야할때는 여기서 주석을 보면서 찾으면 될 것 같다.

이 중에서 특히 중요해 보이는 요소들만 골라서 살펴보자면

먼저 해당 어노테이션을 설명하는 가장 위에 있는 javadoc 주석

* <p>Either {@link #basePackageClasses} or {@link #basePackages} (or its alias
* {@link #value}) may be specified to define specific packages to scan. If specific
* packages are not defined, scanning will occur from the package of the
* class that declares this annotation.

 

basePackageClasses 또는 BasePackages를 지정하여 특정 패키지를 탐색하고 특정 패키지가 정의되지 않은 경우 이 어노테이션을 선언한 클래스의 패키지에서 탐색이 수행된다

라고 나와있다.

스캔 범위를 특정하지 않으면 디폴트로 @ComponentScan이 달린 클래스의 패키지가 요소 basePackages나 basePackageClasses가 되어 해당 패키지를 중심으로 스캔을 진행한다는 것이다.

이때 해당 패키지를 중심으로 스캔한다는 것은 해당 패키지와 패키지 하위에 있는 패키지들과 클래스들까지 탐색한다는 뜻이다.

 

다음으로 useDefaultFilters()

/**
 * Indicates whether automatic detection of classes annotated with {@code @Component}
 * {@code @Repository}, {@code @Service}, or {@code @Controller} should be enabled.
 */
boolean useDefaultFilters() default true;

 

@Component의 하위 어노테이션들인 @Repository, @Service, 또는 @Controller 어노테이션이 달린 클래스들의 자동감지 활성화 여부를 나타낸다

라고 나와있다.

@Repostory,@Service,@Controller 내부에 들어가보면 @Configuration과 같이 @Component가 메타 어노테이션으로 달려있다. 그렇기 때문에 해당 어노테이션들을 자동감지해야하는지 체크하는 변수가 있는 것이다. (디폴트로 true)

 

다음으로 nameGenerator()

/**
 * The {@link BeanNameGenerator} class to be used for naming detected components
 * within the Spring container.
 * <p>The default value of the {@link BeanNameGenerator} interface itself indicates
 * that the scanner used to process this {@code @ComponentScan} annotation should
 * use its inherited bean name generator, e.g. the default
 * {@link AnnotationBeanNameGenerator} or any custom instance supplied to the
 * application context at bootstrap time.
 */
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

 

스프링 컨테이너 내에서 감지된 component의 이름을 지정하는데 사용된다

@ComponentScan은 상속된 bean의 이름을 짓는데 사용한다

라고 한다.

주석에서 나와있는대로 bean의 이름을 짓는데 사용된다.

 

마지막으로 ClassPathScanningCandidateComponentProvider

/**
 * Controls the class files eligible for component detection.
 * <p>Consider use of {@link #includeFilters} and {@link #excludeFilters}
 * for a more flexible approach.
 */
String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

 

필터들을 사용해 bean으로 등록하기 적합한 요소들을 찾아 제어한다.

/.

(javadoc 주석에 나오는 includeFilter와 excludeFilter는 대상을 각각 스캔 대상에서 포함시키는/제외시키는 필터이다. @SpringBootApplication에서도 @ComponentScan을 사용해서 TypeExcludeFilter와 AutoConfigurationExcludeFilter를 제외시켰다.)

./

주석만 봐서는 몰랐지만 ClassPathScanningCandidateComponentProvider 내부에 들어가 코드를 보니 어느 정도 확인이 되었다. 코드가 너무 많고 변수명도 다 길고 섞여있어서 눈 빠지는 줄 알았다.

해당 클래스의 javadoc 주석과 내부 요소들을 참고해 역할을 요약하자면 필터에서 걸러져 최종적으로 받은 데이터들을 클래스 내부에서 여러 과정을 거쳐 설정으로 등록하는 것이다. 즉, 받은 데이터들을 bean으로 등록해주는 중요한 부분이라는 뜻이다. 등록과 관련해서 필요한 정보가 있을 때는 @ComponentScan내부를 우선적으로 확인해본뒤 다음으로 해당 클래스 부분을 확인해 보면 될 것 같다.

 

 

정리하자면 @ComponentScan은 @Component, @Configuration, @Reposity, @Service, @Controller등(등을 붙인 이유는 해당 어노테이션들을 메타 어노테이션으로 가지고 있는 어노테이션들도 찾아야할 수도 있기 때문)이 붙어있는 클래스들을 찾아서 필터에 맞게 걸러준 다음 이름을 짓고 ClassPathScanningCandidateComponentProvider를 이용해 bean으로 등록하는 역할을 한다.

 

 

 

3. @EnableAutoConfiguration

javadoc 주석에 따르면 스프링 애플리케이션에 필요할 것 같은 bean을 자동으로 추측하여 추가해주는 기능을 가능하도록 하는 역할을 한다고 한다.

//(필요한 bean을 추측하는 기능 자체는 내부의 메타 어노테이션중 하나가 할 것이라는 것을 알 수 있다.)

 

내부 코드를 탐색해보자면

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};

}

 

위와 같다. 기본 제공 메타 어노테이션들이 이번에도 보이는데 이것들을 제외하고 보자면

@AutoConfigurationPackage와 @Import(AutoConfigurationImportSelector.class)가 있다.

먼저 @AutoConfigurationPackage내부를 살펴보면

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

}

 

위와 같은데 javadoc 주석에 따르면 AutoConfigurationPackages를 사용해 패키지를 지정하지 않았을 경우 디폴트로 해당 어노테이션이 달린 클래스의 패키지가 등록이 된다고 한다.

탐색할 패키지 지정과 관련있기에 @ComponentScan의 요소였던 basePackages, basePackageClasses와 똑같은 요소들이 보인다. 어노테이션 쪽에는 @EnableAutoConfiguration의 @Import(AutoConfigurationImportSelector.class)와 같이 이번에도 @Import가 달려있는데 이번에는 AutoConfigurationPackages 클래스 내부의 Registrar라는 클래스를 @Import하고 있다.

@Import와 AutoConfigurationPackages의 javadoc 문서에 따르면

@Import란 @Configuration 클래스들을 import할 수 있게 해주는 어노테이션

AutoConfigurationPackages는 나중에 참조할 수 있도록 패키지를 저장하기 위한 클래스라고 한다.

즉, 나중에 참조할 수 있도록 만들어 놓은 패키지 저장 클래스의 Registrar를 import한 것이다.

내부의 Registrar부분을 살펴보자면

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
       register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
       return Collections.singleton(new PackageImports(metadata));
    }

}

 

위와 같다.

2개의 인터페이스를 implements하고 2개의 메서드를 override하고 있다.

그중에서 DeterminableImports 인터페이스에서 override한 determineImports 메서드는 주석에 따르면 import 집합을 return 해주는 메서드이고 ImportBeanDefinitionRegistrar 인터페이스에서 override한 registerBeanDefintions 메서드는 어노테이션의 데이터에 맞게 BeanDefinitionRegistry 타입의 bean 정의를 등록해주는 메서드라고 한다.

registerBeanDefintions 메서드가 Registrar의 핵심인 것으로 추측된다.

즉, @AutoConfigurationPackage는 AutoConfigurationPackages 클래스의 Registrar를 import 해서 bean정의를 등록하는 기능을 가질 수 있게 되는 것이다.

 

이러한 @AutoConfigurationPackage를 메타 어노테이션으로 가지고 있는 @EnableAutoConfiguration 또한 같은 기능을 가지고 있게 된다.

 

 

다음으로 @EnableAutoConfiguration의 또 다른 어노테이션인 @Import(AutoConfigurationImportSelector.class)에 대해 알아본다.

@Import(AutoConfigurationImportSelector.class)는 방금 위에서 한 것과 같이 AutoConfigurationImportSelector 클래스를 import해온다는 뜻이다.

AutoConfigurationImportSelector 클래스의 내부코드는 522줄가량 되기 때문에 생략하고 중요해 보이는 부분만 올리도록한다.

/**
 * {@link DeferredImportSelector} to handle {@link EnableAutoConfiguration
 * auto-configuration}. This class can also be subclassed if a custom variant of
 * {@link EnableAutoConfiguration @EnableAutoConfiguration} is needed.
 *
 */

 

일단 AutoConfigurationImportSelector 클래스를 설명해주는 javadoc 주석에 따르면

EnableAutoConfiguration을 처리하기 위한 DeferredImportSelector로써 이 클래스는 @EnableAutoConfiguration의 커스텀 variant가 필요하다면 서브 클래스가 될 수 있다고 한다.

AutoConfigurationImportSelector가 DeferredImportSelector로써 작동한다는 것인데

클래스의 시작 부분에서도 확인할 수 있다.

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
       ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

 

해당 부분을 본다면 DeferredImportSelector를 implements를 하고 있음이 확인된다.

DeferredImportSelector 인터페이스의 javadoc 주석에 따르면

/**
 * A variation of {@link ImportSelector} that runs after all {@code @Configuration} beans
 * have been processed.

 

모든 @Configuration bean이 처리된 후 실행되는 ImportSelector의 변형이라고 한다.

interface 시작 부분에서도 extends ImportSelector 한다고 나와있다.

public interface DeferredImportSelector extends ImportSelector {

 

그러니 DeferredImportSelector를 알기 위해서는 ImportSelector 인터페이스를 알아야한다.

public interface ImportSelector {

    String[] selectImports(AnnotationMetadata importingClassMetadata);

    @Nullable
    default Predicate<String> getExclusionFilter() {
       return null;
    }

}

 

ImportSelector 인터페이스의 javadoc 주석을 보면 매우 중요한 인터페이스임을 알 수 있는다.

/**
 * Interface to be implemented by types that determine which @{@link Configuration}
 * class(es) should be imported based on a given selection criteria, usually one or
 * more annotation attributes.

 

주어진 선택 기준(일반적으로 하나 이상의 어노테이션)을 기반으로 어떤 @Configuration 클래스를 import해와야 하는지 결정하는 타입의 구현체라고 한다.

내부의 요소들을 각각의 주석을 해석해서 확인하면

import하는 @Configuration 클래스의 어노테이션 메타데이터를 기반으로 어떤 클래스가 import 되어야 하는지 이름을 선택하고 return 하는 selectImports()

import하는 configuration 클래스중에서 제외시킬 수 있는 조건자를 return하는 getExclusionFilter()

가 있다.

이때 메타데이터를 기반으로 어떤 것을 import하는지 결정하는 selectImports()이 핵심인 것 같다.

 

즉, 이러한 ImportSelector를 extends해서 변형시킨 DeferredImportSelector 또한

일부 configuration 클래스를 필터링하며 어노테이션 메타데이터를 기반으로 어떤 클래스가 import 되어야 하는지 선택하는 기능을 가지고 있다. 또한 DeferredImportSelector부분에서는 ImportSelector를 extends하여 같은 기능을 가지면서 추가로 디테일하게 필요한 기능들을 커스텀하고 있다.

그리고 다시 이렇게 커스텀한 DeferredImportSelector를 AutoConfigurationImportSelector 클래스가  implements 해옴으로써 중요한 역할을 하는 부분들을 override했다.

//(implements는 extends와 다르게 상속받은 메서드를 override(재정의)하여 사용한다는 차이가 있다.)

따라서 AutoConfigurationImportSelector 클래스가 "3. @EnableAutoConfiguration"의 시작 부분에서 말한 필요한 bean을 추측하는 기능을 가진 어노테이션이라고 볼 수 있을 것이다.

 

 

AutoConfigurationImportSelector는 방금 위에서 알아본 것과 같이 필요할 것 같은 클래스를 결정하는 기능을 가지고 있는데 결정한 뒤 클래스를 어디서 가져오는 걸까?

그건 바로 AutoConfigurationImportSelector내부의 다른 코드로부터 추측이 가능하다.

//(이번에도 "가져온다"의 get으로 키워드를 추측해서 찾아봤다.)

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = new ArrayList<>(
          SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
    ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
    Assert.notEmpty(configurations,
          "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

 

해당 코드의 javadoc 주석을 보면

고려해야할 configuration 클래스들을 return한다. getSpringFactoriesLoaderFactoryClass()와 함께 ImportCandidates를 사용해 후보 클래스들을 로드한다고 한다.

//(버전호환성때문에 SpringFactoriesLoader 와 getSpringFactoriesLoaderFactoryClass()를 같이 사용한다고 한다.)

해당 메서드를 통해 클래스를 가져올 수 있는 것이다.

즉, 해당 메서드에서 bean 클래스가 어디서 오는지 단서를 얻을 수 있다는 것이다.

위의 코드 내부를 보면 SpringFactoriesLoader.loadFactoryNames() 부분이 핵심이 되는 것을 알 수 있기 때문에  SpringFactoriesLoader 내부를 한번 살펴보도록 한다.

	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
/////생략/////
///중간생략///
/////생략/////
	public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    	ClassLoader classLoaderToUse = classLoader;
    	if (classLoaderToUse == null) {
        	classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    	}

    	String factoryTypeName = factoryType.getName();
    	return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
	}
/////생략/////
///중간생략///
/////생략/////
    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            Map<String, List<String>> result = new HashMap();

            try {
                Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");

 

SpringFactoriesLoader 코드의 내부에 들어오면 위와 같다.

보면 추측한대로 factories의 리소스가 META-INF/spring.factories에 위치해 있다는 정보를 여기서 얻을 수 있었다.

 

우리 프로젝트에서도 spring-boot-autoconfigure-2.7.0.jar에서 찾을 수 있었다.

BUT 내부의 데이터에서 큰 수확을 얻을 순 없었다.

내부에 다양한 외부 라이브러리들이 있었지만 이것이 깃허브에서 노트북으로 옮겨오면서 노트북의 데이터가 들어간건지 프로젝트를 만들던 당시 상태 그대로의 데이터인지 확인을 할 필요가 있었다.

그러나 프로젝트를 진행한지 이미 2년이상 지나서 프로젝트 관련 데이터들을 잃어버려 비교분석이 불가능했다.

정말 아쉽다.

다음번 프로젝트부터는 해당 부분을 포함한 여러가지 참고 데이터들을 기록하는 습관을 들여야겠다.

또한 다음번 프로젝트를 마무리하고 분석할때 spring.factories를 주제로 하여 비교분석하는 식으로 하여 공부 해봐야겠다. 반성한다.

 

다시 돌아가서 loadFactoryNames를 보면 loadSpringFactories를 이용해 spring.factories로부터 데이터를 가져오는 것을 알 수 있다.

이때 loadSpringFactories는 cache 방식을 사용하는 것을 볼 수 있는데 이것으로 데이터 호출의 시간을 단축하여 후보 클래스들을 호출할때 불필요한 시간낭비를 줄이는 것을 알 수 있다.

 

 

정리하자면 @EnableAutoConfiguration은

@AutoConfigurationPackage에서 AutoConfigurationPackages의 Registrar를 import하는 덕분에 bean 정의를 등록할 수 있고

@Import(AutoConfigurationImportSelector.class)에서는 DeferredImportSelector를 implements하는 AutoConfigurationImportSelector를 import하여 필요한 bean 클래스를 추측해 META-INF의 spring.factories로부터 외부 라이브러리 bean을 cache 방식으로 가져올 수 있다.

 

 

 


 

 

 

4. 내부 요소

3가지 어노테이션에 대해 자세하게 살펴봤다. @SpringBootApplication은 지금까지 분석한 3가지 어노테이션의 기능들을 사용할 수 있는 것이다.

다음으로 @SpringBootApplication 내부에 있는 요소들을 주석을 통해 알아보도록 한다.

@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};

 

exclude() 특정 자동 configuration 클래스를 제외시킨다

@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};

 

excludeName() 특정 자동 configuration 클래스 이름을 제외시킨다

@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};

 

scanBasePackages() 어노테이션이 달린 component들을 스캔하기 위한 @ComponentScan 베이스 패키지를 설정한다

@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};

 

scanBasePackageClasses() 어노테이션이 달린 component들을 스캔하기 위한 @ComponentScan 베이스 패키지 클래스를 설정한다

@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

 

nameGenerator() 스프링 컨테이너 안에서 감지된 component의 이름을 짓기 위한 클래스를 설정한다

@SpringBootApplication에서는 상속된 bean의 이름을 짓기 위해 사용한다

@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;

 

proxyBeanMethods() @Bean 메서드를 프록시 방식으로 처리하도록 설정한다

 

아주 익숙한 설명들과 명칭들이다.

그렇다. 위의 요소들은 전부 이제까지 3가지의 어노테이션을 공부하며 봐왔던 것들과 매우 비슷한 요소들이다.

맨 처음 javadoc 주석의 해석처럼 3가지 어노테이션과 동일한 역할을 하고 있기 때문에 존재하고 있는 것이다.

 

 

 


 

 

 

이상 @SpringBootApplication에 대해서 알아보았다.

다음은 @SpringBootApplication과 함께 import되었던 SpringApplication 클래스에 대해 알아보도록 한다.

 

 

 

 

 

 

//(참고자료는 언급할때마다 링크로 넣어 두었다.)

 

 

 

 

 

 

잘못된 정보 말씀해주시면 수정합니다. 읽어주셔서 감사합니다.