본문 바로가기
Spring

면접관 : 스프링부트 auto-configuration의 동작방식을 설명해주세요. - 2

by irerin07 2024. 2. 16.
728x90

면접관 : 스프링부트 auto-configuration의 동작방식을 설명해주세요. - 1

 

우선 우리가 본 콘솔 출력 내용을 다시 한 번 보도록 하겠습니다.

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

   AopAutoConfiguration matched:
      - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)

   AopAutoConfiguration.AspectJAutoProxyingConfiguration matched:
      - @ConditionalOnClass found required class 'org.aspectj.weaver.Advice' (OnClassCondition)
      
-- 생략 --

Negative matches:
-----------------

   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'jakarta.jms.ConnectionFactory' (OnClassCondition)

   AopAutoConfiguration.AspectJAutoProxyingConfiguration.JdkDynamicAutoProxyConfiguration:
      Did not match:
         - @ConditionalOnProperty (spring.aop.proxy-target-class=false) did not find property 'proxy-target-class' (OnPropertyCondition)

   AopAutoConfiguration.ClassProxyingConfiguration:
      Did not match:
         - @ConditionalOnMissingClass found unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition)

-- 생략 --

Exclusions:
-----------

    None


Unconditional classes:
----------------------

    org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration

    org.springframework.boot.actuate.autoconfigure.availability.AvailabilityHealthContributorAutoConfiguration

    org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration

    org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration

    org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

    org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration​

 

Positive matches

Positive matches안에 들어있는 AutoConfiguraion의 목록은 현재 활성화가 된 AutoConfiguration을 의미합니다.

Negative matches

Negative matches안에 들어있는 AutoConfiguration의 목록은 활성되지 않은 AutoConfiguration을 의미합니다.

Exclusions

AutoConfiguration들 중, 포함되지 않도록 설정한 AutoConfiguration을 보여줍니다.

Unconditional classes

조건없이 항상 활성화 되는 AutoConfiguration의 목록입니다.

 

이 목록들에는 스프링 부트가 기본적으로 제공하는 AutoConfiguration외에도 사용자가 직접 작성한 AutoConfiguration역시 보여줍니다.

 

그럼 스프링 부트는 무엇을 기준으로 활성화 할지, 비활성화 할지 결정하는 것일까요?

위 콘솔 내용을 잘 살펴보면 공통적으로 등장하는 어노테이션이 보입니다.

바로 @Conditional 어노테이션 입니다.

@Conditional

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

@Conditional 어노테이션은 element를 하나 등록을 해야 하는데 해당 element의 타입은 Condition이라는 인터페이스를 구현한 클래스입니다.

 

@Conditional 어노테이션이 달린  AutoConfiguration 클래스들은 지정된 모든 Condition 인터페이스를 만족해야 구성 요소가 등록이 됩니다.

 

그럼 우선 Condition이 무엇인지 스프링 문서를 보도록 하죠.


A single condition that must be matched in order for a component to be registered.
Conditions are checked immediately before the bean definition is due to be registered and are free to veto registration based on any criteria that can be determined at that point.

Conditions must follow the same restrictions as a BeanFactoryPostProcessor and take care to never interact with bean instances. For more fine-grained control over conditions that interact with @Configuration beans, consider implementing the ConfigurationCondition interface.

Multiple conditions on a given class or on a given method will be ordered according to the semantics of Spring's Ordered interface and @Order annotation. See AnnotationAwareOrderComparator for details.

컴포넌트가 등록되기 위해 반드시 일치해야 하는 단일 조건입니다.
Condition은 빈 정의가 등록되기 직전에 확인되며 해당 시점에 결정할 수 있는 기준에 따라 자유롭게 등록을 거부할 수 있습니다.

Condition은 BeanFactoryPostProcessor와 동일한 제한을 따라야 하며 빈 인스턴스와 상호 작용하지 않도록 주의해야 합니다. 구성 빈과 상호 작용하는 조건을 보다 세밀하게 제어하려면 구성 조건 인터페이스를 구현하는 것을 고려하세요.

주어진 클래스 또는 주어진 메서드에 대한 여러Condition은 Spring의 Ordered 인터페이스와 @Order 어노테이션의 의미론에 따라 정렬됩니다. 자세한 내용은 AnnotationAwareOrderComparator를 참조하세요.

문서에 따르면 Condition은 어떤 컴포넌트가 등록되기 위한 조건임을 알 수 있습니다. 

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

Condition 클래스의 안을 들여다보면 boolean을 반환하는 matches라는 메서드가 있습니다. 해당 메서드는 현재 사용중인 스프링 컨테이너와 애플리케이션이 돌아가는 환경에 대한 정보를 얻을 수 있는 ConditoinContext라는 오브젝트와 @Conditional 어노테이션이 붙어있는 위치의 다른 어노테이션들의 정보를 사용할 수 있는 메타데이터를 필요로 합니다.

 

그리고 이 Condition의 구현체중에 SpringBootCondition이라는 것이 있습니다. 스프링부트 공식 문서에서는 SpringBootCondition을 다음과 같이 설명합니다.

Base of all Condition implementations used with Spring Boot. Provides sensible logging to help the user diagnose what classes are loaded.

스프링 부트와 함께 사용되는 모든 Condition 구현의 베이스. 사용자가 어떤 클래스가 로드되었는지 진단하는 데 도움이 되는 합리적인 로깅을 제공합니다.

 

스프링 부트는 등록 되어있는 모든 AutoConfiguration들을 등록하는 것이 아니라 이러한 내부 메커니즘을 통해 등록할 AutoConfiguration들을 선별하는 과정을 거치게 되는 것이죠.

 

이런 @Conditional 어노테이션은 굉장히 종류가 많지만 대표적인 몇가지만 알아보도록 하겠습니다.

 

@ConditionalOnBean

특정 bean들의 존재유무에 대해서 다루는 필터/조건입니다.

    @Configuration(
        proxyBeanMethods = false
    )
    @Conditional({DispatcherServletRegistrationCondition.class})
    @ConditionalOnClass({ServletRegistration.class})
    @EnableConfigurationProperties({WebMvcProperties.class})
    @Import({DispatcherServletConfiguration.class})
    protected static class DispatcherServletRegistrationConfiguration {
        protected DispatcherServletRegistrationConfiguration() {
        }

        @Bean(
            name = {"dispatcherServletRegistration"}
        )
        @ConditionalOnBean(
            value = {DispatcherServlet.class},
            name = {"dispatcherServlet"}
        )
        public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
            DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
            registration.setName("dispatcherServlet");
            registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
            Objects.requireNonNull(registration);
            multipartConfig.ifAvailable(registration::setMultipartConfig);
            return registration;
        }
    }

DispatcherServletAutoConfiguration의 일부분을 가져온 코드블록입니다.dispatcherServletRegistration(...)메서드 위에 @ConditionalOnBean 어노테이션이 붙어있는 것을 볼 수 있습니다.이 어노테이션은 어떤 특정 빈이 생성되어 있는 경우에만 조건이 만족되므로, 해당 메서드를 실행하기에 앞서 메서드 실행에 필수적인 빈이 미리 생성되었는지 확인할 수 있습니다.

 

 

@ConditionalOnClass

@ConditionalOnClass는 classpath에 특정 클래스가 존재하는 경우에 조건을 만족합니다.

@AutoConfigureOrder(Integer.MIN_VALUE)
@AutoConfiguration(
    after = {ServletWebServerFactoryAutoConfiguration.class}
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({DispatcherServlet.class})
public class DispatcherServletAutoConfiguration {
    public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
    public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

    public DispatcherServletAutoConfiguration() {
    }
    
    //...
}

DispatcherServletAutoConfiguration 클래스에는 @ConditionalOnClass(..) 어노테이션이 붙어 있는것을 확인할 수 있습니다.

 

@ConditionalOnWebApplication

@ConditionalOnWebApplication 어노테이션은 실행되는 애플리케이션이 웹 애플리케이션인 경우에 조건을 만족합니다. 기본적으로 어떤 유형의 웹 애플리케이션이라도 통과하지만 type 속성을 사용해서 조건을 강화할 수 있습니다.

@AutoConfiguration
@ConditionalOnNotWarDeployment
@ConditionalOnWebApplication
@EnableConfigurationProperties({ServerProperties.class})
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
    public EmbeddedWebServerFactoryCustomizerAutoConfiguration() {
    }
    
    // ...
    
}

 

@EnableAutoConfiguration이 안보이는데요?

그런데 분명 스프링의 자동 설정을 사용하기 위해서는 @EnableAutoConfiguration을 등록해야 한다고 했는데 도대체 어디 숨어있는거죠?

 

정답은 바로 이곳에 있습니다.

@SpringBootApplication
public class SpringbootWebAutoconfigurationApplication {

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

}

우리가 스프링 부트로 개발하며 수도없이 봐 왔던 @SpringBootApplication 안으로 들어가 봅시다.

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

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

여기 숨어있었습니다.

그런데 그 아래에 굉장히 익숙한 친구도 하나 보이네요? 바로 @ComponentScan입니다. base-package가 정의되지 않으면 해당 어노테이션이 붙은 classpath하위의 @Component 어노테이션을 스캔하여 빈으로 등록해주는 역할을 합니다.

 

거기에 @SpringBootConfiguration이라는 어노테이션도 보이는데 이 어노테이션은 사실 @Configuration과 같은 기능을 하고 있습니다.

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

 

결국 우리가 매번 봐 오던 @SpringBootApplication 어노테이션은 @SpringBootConfiguration(@Configuration), @EnableAutoConfiguration 그리고 @ComponentScan을 합쳐 둔 어노테이션이었던 것입니다.

 

이제 우리는 스프링 부트 애플리케이션을 실행하면 컨텍스트가 생성되고, 개발자가 정의한 Component들이 먼저 스캔되어 빈으로 등록이 되고, AutoConfiguration의 빈들이 등록 되게 되는 것임을 알게 되었습니다.

 

@Configuration(proxyBeanMethods = false)

AutoConfiguration 설정을 보다 보면 @Configuration(proxyBeanMethods = false)라 설정 되어있는 경우를 볼 수 있습니다.

이 proxyBeanMethods가 true로 설정이 되어 있다면 CGLIB을 통해 런타임에 동적으로 Configuration 객체의 프록시를 생성해줍니다.

그런데 왜 이 설정을 false로 해주는 걸까요?

스프링부트 깃헙에 올라온 내용을 보면 그 답을 알 수 있는데 간단히 말하자면, @Configuration 어노테이션이 달려 있는 Auto-Configuration클래스들은 ConfigurationClassPostProcessor에 의해 cglib 프록시가 생성이 된다고 합니다. 그런데 이 과정이 생각보다 비싸고, 대부분의 auto-configuration 클래스에는 불필요하기 때문이라고 합니다.

 

Spring Boot

여기까지 알아본 내용들을 바탕으로 정리를 하자면, 스프링 부트는 웹 개발에 있어서 필요한 기능들을 미리 구성 정보로 등록해 두고 조건이나 필요에 따라 자동으로 등록해주고 관련 설정을 선언해주는 기능을 제공하고 있음을 알 수 있습니다.

 

개발자는 매핑 작업, 서블릿 등록, 컨텍스트 설정 구성등의 작업을 신경쓰지 않고 오롯이 비즈니스 로직 개발에 집중할 수 있도록 도움을 주는 것이죠.

 

출처 : 

https://lob-dev.tistory.com/33

https://www.programmersought.com/article/86306432392/

https://scshim.tistory.com/420

https://bj-lee.tistory.com/2

https://dveamer.github.io/backend/SpringBootAutoConfiguration.html

https://tecoble.techcourse.co.kr/post/2021-10-14-springboot-autoconfiguration/

https://brunch.co.kr/@anonymdevoo/49

https://thepracticaldeveloper.com/spring-boot-autoconfiguration/

https://www.inflearn.com/course/%ED%86%A0%EB%B9%84-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%9D%B4%ED%95%B4%EC%99%80%EC%9B%90%EB%A6%AC/dashboard

https://www.marcobehler.com/guides/spring-boot-autoconfiguration

 

 

 

 

728x90