티스토리 뷰

Spring

[Spring] SpringBoot 소스 코드 분석하기, @SpringBootApplication 어노테이션의 속성과 구성요소 - (3)

망나니개발자 2022. 2. 12. 10:00
반응형

이번에는 SpringBoot의 실행 과정을 소스 코드로 직접 살펴보려고 합니다. 지난번 애플리케이션 컨텍스트에 이어서 오늘은 @SpringBootApplication 어노테이션을 자세히 살펴보도록 하겠습니다.

아래의 내용은 SpringBoot 2.6.3를 기준으로 작성되었습니다.

 

 

 

1. @SpringBootApplication 어노테이션의 속성과 구성요소


[ @SpringBootApplication 어노테이션의 속성 ]

SpringBoot 프로젝트를 생성하면 다음과 같은 메인 클래스와 함수가 자동으로 생성된다. 우리가 가장 먼저 주목해서 살펴볼 부분은 @SpringBootApplication 어노테이션이다.

@SpringBootApplication
public class TestingApplication {

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

 

 

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

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

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

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

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

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

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

 

 

@SpringBootApplication 어노테이션을 통해 설정한 값들은 각각 다음의 기능으로 연결되어 동작한다.

  • exclude: 특정 클래스를 자동 설정에서 제외함
  • excludeName: 클래스의 이름으로 자동 설정에서 제외함
  • scanBasePackages: 컴포넌트 스캔(빈 탐색)을 진행할 베이스 패키지를 설정함
  • scanBasePackageClasses: 컴포넌트 스캔(빈 탐색)을 진행할 베이스 클래스를 설정함
  • nameGenerator: 빈 이름 생성을 담당할 클래스를 설정함
  • proxyBeanMethods: @Bean 메소드를 프록시 방식으로 처리하도록 설정함

 

 

proxyBeanMethods를 제외한 것들은 모두 설명을 읽고 쉽게 이해할 수 있으므로 proxyBeanMethods만 살펴보도록 하자.

proxyBeanMethods는 @Bean으로 빈을 등록하는 메소드에 프록시 패턴을 적용할 것인지를 결정하는 속성이다. proxyBeanMethods의 기본값은 true이며, 별다른 설정을 하지 않았다면 @Bean 메소드에 프록시가 기본으로 적용된다. @Bean 메소드에 프록시가 필요한 이유는 해당 메소드를 직접 호출하는 경우에도 항상 싱글톤 스코프를 강제하여 1개의 객체만을 생성하기 위함이다.

예를 들어 다음과 같은 빈 객체 생성 코드가 있다고 하자.

@Configuration(proxyBeanMethods = false)
public class MyBeanConfiguration {

    @Bean
    public CommonBean commonBean() {
        return new CommonBean();
    }

    @Bean
    public MyFirstBean myFirstBean() {
        return new MyFirstBean(commonBean());
    }

    @Bean
    public MySecondBean mySecondBean() {
        return new MySecondBean(commonBean());
    }
}

 

 

위의 코드에서 우리는 CommonBean 객체를 생성하는 commonBean() 메소드를 직접 호출하고 있다. 만약 proxyBeanMethods를 false로 설정하면 MyBeanConfiguration 클래스에 프록시가 적용되지 않아 총 2개의 CommonBean이 생성이 된다. 이렇게 여러 개의 빈이 생성되는 상황은 일반적으로 우리가 원하는 그림이 아니다. 스프링은 그래서 기본적으로 proxyBeanMethods를 true로 설정하고 바이트 조작 라이브러리인 CGLib을 통해 프록시 패턴을 적용한다. 그래서 해당 @Bean 메소드가 호출되어 이미 빈이 생성되었으면 존재하는 빈을 찾아서 반환함으로써 1개의 빈이 생성되도록 처리한다.

 

 

이제 다음으로 @SpringBootApplication 어노테이션에 붙어있는 하위 어노테이션을 살펴볼 차례이다. @SpringBootApplication은 다음의 3가지 어노테이션을 합쳐둔 어노테이션이므로 각각 살펴보도록 하자.

  • @SpringBootConfiguration 어노테이션
  • @EnableAutoconfiguration 어노테이션
  • @ComponentScan 어노테이션

 

 

 

[ @SpringBootApplication 어노테이션의 구성 요소 ]

1. @SpringBootConfiguration 어노테이션

@SpringBootConfiguration는 @Configuration의 하위 어노테이션으로 @Configuration과 동일한 역할을 수행한다. 실제로 @SpringBootConfiguration의 코드를 살펴보면 @Configuration외에 별다른 것이 없다는 것을 확인할 수 있다.

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

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

}

 

 

@SpringBootConfiguration 어노테이션은 애플리케이션에 1개만 존재해야 하며, 일반적으로 @SpringBootApplication 안에 포함되어 있으므로 따로 작성할 필요는 없다. 해당 어노테이션이 불필요해 보임에도 불구하고 존재하는 이유는 해당 어노테이션을 기준으로 설정들을 불러오기 위함이다. 대표적으로 SpringBoot가 제공하는 통합 테스트 어노테이션인 @SpringBootTest는 이를 사용한다.

protected Class<?>[] getOrFindConfigurationClasses(MergedContextConfiguration mergedConfig) {
    Class<?>[] classes = mergedConfig.getClasses();
    if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) {
        return classes;
    }
    
    Class<?> found = new AnnotatedClassFinder(SpringBootConfiguration.class)
        .findFromClass(mergedConfig.getTestClass());
    Assert.state(found != null, "Unable to find a @SpringBootConfiguration, you need to use "
        + "@ContextConfiguration or @SpringBootTest(classes=...) with your test");
    logger.info("Found @SpringBootConfiguration " + found.getName() + " for test " + mergedConfig.getTestClass());
    return merge(found, classes);
}

 

 

@SpringBootTest는 테스트 설정을 위해 먼저 @SpringBootConfiguration를 가진 클래스를 찾는다. 그리고 찾은 클래스를 중심으로 하위에서 설정들을 자동으로 찾는다. 일반적으로 @SpringBootConfiguration는 @SpringBootApplication 안에 포함되어 있으므로 메인 클래스가 찾아진다.

 

 

 

2. @EnableAutoconfiguration 어노테이션

@EnableAutoConfiguration은 우리가 필요할 것 같은 빈들을 자동으로 설정해주는 자동 설정 기능을 활성화하는 어노테이션이다. 자동 설정을 적용할 클래스 선별은 AutoConfigurationImportSelector가 처리하는데, 특정 클래스를 설정으로 추가하는 @Import를 통해 해당 클래스를 추가해준다. 즉, @EnableAutoConfiguration를 붙이면 AutoConfigurationImportSelector까지 추가된다.

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

 

 

AutoConfigurationImportSelector는 자동 설정할 대상들을 결정하는데, 자동 설정이 가능한 목록은 spring-boot-autoconfigure.jar의 META-INF 디렉토리의 spring.factories에서 확인가능하다. 해당 파일을 보면 매우 다양한 자동 설정을 스프링이 지원하고 있음을 확인할 수 있다. spring.factories에는 자동 설정이 아닌 애플리케이션 초기화를 위한 정보들도 존재하는데, 이러한 이유로 spring-boot-autoconfigure 외에 spring-boot, spring-data-jpa에도 spring.factories가 존재하며 자세한 내용은 다음 포스팅에서 살펴보도록 하자.

 

 

AutoConfigurationImportSelectors는 다음 메소드를 통해 자동 설정이 가능한 후보군들을 모두 불러온다.

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
        getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. 
        If you are using a custom packaging, make sure that file is correct.");
    return configurations;
}

 

 

SpringFactoriesLoader.loadFactoryNames로 가보면 클래스 로더를 사용해서 spring.factories에서 값을 불러옴을 확인할 수 있다.

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 loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    // 캐시에서 먼저 꺼냄
    Map<String, List<String>> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    result = new HashMap<>();
    try {
        Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        while (urls.hasMoreElements()) {
            ... // spring.factories의 내용을 읽어서 (타입 클래스, 구현체)로 저장함
        }
        
        ... // Replace all lists with unmodifiable lists containing unique elements
        
        cache.put(classLoader, result);
    } catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
            FACTORIES_RESOURCE_LOCATION+ "]", ex);
    }
    return result;
}

 

 

위의 코드가 매우 길고 복잡하니 큰 그림에서만 이해하고 넘어가도록 하자.

위의 메소드는 전체 jar 파일로부터 spring.factories들을 불러오는 기능으로, 스프링 애플리케이션 시작 시에 매우 자주 호출된다. 문제는 해당 호출이 디스크에서 값을 읽어오므로 처리 속도가 상당히 느리다는 것인데, 이러한 이유로 스프링은 캐싱 방식을 적용하고 있다. loadSpringFactories 메소드의 처음 로직을 보면 캐시에서 값을 꺼내고, 없으면 디스크에서 읽도록 처리하고 있는 것을 확인할 수 있다.

@EnableAutoConfiguration이 적용될 때에는 이미 spring.factories의 값들이 메모리에 불러와진 상태이므로 파일로부터 읽지는 않는다. (해당 과정은 스프링 애플리케이션이 준비되는 과정에서 처음 파일로부터 불러와지며 다음 포스팅에서 자세히 볼 수 있다.)

 

자동 설정을 위한 auto-configuration 클래스들은 (당연하게도) @Configuration이 있는 설정 클래스들인데, 어떤 클래스의 존재 유무를 판단하기 위해 @Conditional 어노테이션이 사용된다. 많은 조건 어노테이션들 중에서 다음이 많이 사용된다.

  • @ConditionalOnClass: 해당 클래스가 클래스 패스에 존재하는 경우
  • @ConditionlOnBean: 해당 클래스나 이름이 빈 팩토리에 포함되어 있는 경우
  • @ConditionalOnMissingBean: 해당 빈이 등록되어있지 않은 경우
  • 기타 등등

 

다음의 코드는 Json을 직렬화 해주는 Gson 라이브러리의 자동 설정 GsonAutoConfiguration 클래스이다.

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Gson.class)
@EnableConfigurationProperties(GsonProperties.class)
public class GsonAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public GsonBuilder gsonBuilder(List<GsonBuilderCustomizer> customizers) {
        GsonBuilder builder = new GsonBuilder();
        customizers.forEach((c) -> c.customize(builder));
        return builder;
    }

    @Bean
    @ConditionalOnMissingBean
    public Gson gson(GsonBuilder gsonBuilder) {
        return gsonBuilder.create();
    }

    @Bean
    public StandardGsonBuilderCustomizer standardGsonBuilderCustomizer(GsonProperties gsonProperties) {
        return new StandardGsonBuilderCustomizer(gsonProperties);
    }

    ...

}

 

 

위의 코드에서 볼 수 있듯이 자동 설정은 해당 클래스가 클래스 패스에 존재하는 경우에만 해당 설정 클래스를 활성화하고, @ConditionalOnMissingBean을 통해 해당 빈이 존재하지 않는 경우에만 빈을 등록하는 메소드가 호출되도록 하고 있다.

@ConditionalOnMissingBean이 자주 사용되는 이유는 우리가 직접 설정한 빈과 자동 설정으로 등록되는 빈이 중복될 경우 우리가 직접 설정한 빈에 우선순위를 부여하기 위함이다. 우리가 정의한 빈들이 먼저 등록된 이후에 auto-configuration이 적용된다. 

만약 우리가 특정한 자동 설정을 제외하고 싶다면 위에서 살펴보았던 exclude나 excludeName으로 제외할 수 있다. 그 외에도 프로퍼티의 spring.autoconfigure.exclude를 통해서도 제외할 수 있다.

 

@EnableAutoConfiguration이 붙어있는 클래스는 특별한 의미를 갖는데, 특정 작업들을 위한 베이스 패키지가 되기 때문이다. 대표적으로 JPA의 @Entity 클래스를 탐색하는 작업은 @EnableAutoConfiguration이 붙어있는 클래스의 패키지를 기준으로 진행된다.

일반적으로 @EnableAutoConfiguration은 @SpringBootApplication 안에 포함되어 있으므로 자동 설정이 활성화되지만, 해당 어노테이션을 직접 붙여주는 경우에는 위와 같은 이유로 루트 패키지에 있는 클래스에 붙여주는 것이 좋다.

 

 

 

3. @ComponentScan 어노테이션

@ComponentScan은 빈을 등록하기 위한 어노테이션들을 탐색하는 위치를 지정한다. basePackageClasses나 basePackages를 통해 베이스 패키지를 설정할 수 있지만, 만약 설정해주지 않으면 해당 어노테이션이 붙은 클래스를 기준으로 진행된다. @ComponentScan은 @SpringBootApplication 안에 포함되어 있으므로 이러한 이유로 역시 스프링 부트의 메인 클래스는 루트 패키지에 두는 것이 좋다.

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

 

 

스캔이 진행되면 @Configuration과 @Bean 및 @Component의 하위 어노테이션들(@Repository, @Service, @Controller, @RestController, @ControllerAdvice, @RestControllerAdvice)이 있는 클래스 및 메소드를 찾는다. @SpringBootApplication에는 includeFilters와 excludeFilters가 적용되어 있는데 각각 다음의 기능을 한다.

  • includeFilters : 해당 클래스들을 컴포넌트 스캔 대상에 포함시킴
  • excludeFilters : 해당 클래스들을 컴포넌트 스캔 대상에서 제외시킴

SpringBoot에서는 TypeExcludeFilter와 AutoConfigurationExcludeFilter를 컴포넌트 스캔 대상에서 제외하고 있는데, TypeExcludeFilter와 AutoConfigurationExcludeFilter는 거의 spring-boot-test에서 내부적으로 사용되므로 스캔에서 제외되었다.

 

 

 

 

스프링부트 애플리케이션의 설정은 @SpringBootApplication 어노테이션으로 인해 상당히 많은 부분들이 추상화되어 있다. 그래서 매우 간단히 동작한다고 생각할 수 있지만, 내부를 들여다보면 애플리케이션을 실행하기 위한 다양한 장치들이 존재함을 확인할 수 있다. 이제 다음으로 SpringBoot 애플리케이션이 실제로 시작되는 run() 메소드 호출을 통해 SpringBootApplication이 생성되고 초기화되는 과정을 들여다보도록 하자.

 

 

 

 

 

 

위의 내용들은 개인적으로 공부를 하면서 작성을 한 내용이라 충분히 틀리거나 잘못된 내용들이 있을 수 있습니다. 혹시 수정 또는 추가할 내용들을 발견하셨다면 댓글 남겨주세요! 반영해서 수정하도록 하겠습니다:)

 

 

 

 

 

관련 포스팅

  1. SpringBoot 소스 코드 분석하기, SpringBoot의 장점과 특징 - (1)
  2. SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(Application Context)와 빈팩토리(BeanFactory) - (2)
  3. SpringBoot 소스 코드 분석하기, @SpringBootApplication 어노테이션의 속성과 구성요소 - (3)
  4. SpringBoot 소스 코드 분석하기, SpringBootApplication의 생성과 초기화 과정  - (4)
  5. SpringBoot 소스 코드 분석하기, SpringBootApplication의 run 메소드와 실행과정  - (5)
  6. SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(ApplicationContext)의 refreshContext 동작 과정 - (6)
  7. SpringBoot 소스 코드 분석하기, DispatcherServlet(디스패처 서블릿) 동작 과정 - (7)

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/03   »
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 27 28 29 30
31
글 보관함