SpringBoot : @AutoConfiguration 파헤쳐보기

이가희·2025년 4월 21일

spring + java

목록 보기
12/14
post-thumbnail

SpringBoot 같이 알아가기 [목차]

chapter 의 제목을 누르시면 각 페이지로 이동합니다.
chapter설명
1. Spring Boot 의 개념 Spring Boot 는 Spring 의 업그레이드 버전 아니야? Spring Boot 가 왜 탄생하게 되었고, 어떤 것인지에 대한 설명이 있습니다.
2. 독립 실행형 Servlet application 구축'독립 실행형 ' 의 개념을 알고, 직접 FrontController 까지 구축해보면서 SpringBoot 의 Dispatcher Servlet 동작 방식을 이해하는 기반을 다집니다.
3. Containerless Web Application앞선 이해를 기반으로 Spring Boot 의 @SpringBootApplication , SpringBootApplication.run() 을 유사하게 구현해보며 Spring Boot 동작 방식을 이해합니다. (포함되는 개념 : Spring Container, Dispatcher Servlet, Configuration , ComponentScan 등)
4. @AutoConfiguration 파헤쳐보기Spring Boot 의 @AutoConfiguration 을 유사하게 직접 구현하여, Spring Boot가 수 많은 빈들을 어떻게 자동으로 구성해주는지 학습합니다.
5. 자동 구성 빈 오브젝트의 디폴트 값 변경하기Spring Boot 는 수 많은 빈들을 자동으로 구성해주고, 디폴트 값을 유연하게 변경할 수 있도록 합니다. 코드를 통해 이 과정을 이해합니다.

SpringBoot 는 @AutoConfiguration 을 이용해 수 많은 구성 정보를 제공해준다.

자신의 SpringBoot 프로젝트에서 org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 보면 SpringBoot 가 몇 개의 자동 구성을 제공해주는지 알 수 있다.

SpringBoot 3.3.10 version 기준 org.springframework.boot:spring-boot-starter-web 을 의존성에 추가하면 152개를 자동으로 구성해주고 있다.

직접 자동 구성 방식으로 빈을 등록해보고, 이를 대체도 해 보면서 @AutoConfiguration 의 동작 방식에 대해 이해를 해보자.


1. 빈 오브젝트의 역할과 구분

빈 오브젝트는 애플리케이션 로직 빈 , 애플리케이션 인프라스트럭쳐 빈 , 컨테이터 인프라스트럭쳐 빈으로 구분할 수 있다.

빈 오브젝트 구분 이미지

  1. 애플리케이션 로직 빈
    애플리리케이션의 비즈니스 로직을 담고 있는 클래스로 만들어지는 빈이다.

  2. 애플리케이션 인프라스트럭쳐 빈
    빈 구성 정보에 의해 컨테이너에 등록되는 빈이지만 애플리케이션의 로직이 아니라 애플리케이션이 동작하는데 꼭 필요한 기술 기반을 제공하는 빈이다.
    전통적인 스프링 애플리케이션에서는 빈으로 등록되지 않지만 스프링 부트에서 구성 정보에 의해 빈으로 등록되어지는 ServletWebServerFactoryDispatcherServlet 등이 여기에 포함된다.

  3. 컨테이너 인프라스트럭쳐 빈
    스프링 컨테이너 기능을 확장해서 빈의 등록과 생성, 관계설정, 초기화 등의 작업에 참여하는 빈이다. 개발자가 작성한 구성 정보에 의해 생성되는게 아니라 컨테이너가 직접 만들고 사용한다.

애플리케이션의 로직 빈은 개발자가 직접 구성 정보를 작성하여, @ComponentScan 을 통해 등록이 된다면 스프링부트에서 애플리케이션 인프라스트럭쳐 빈은 @AutoConfiguration 을 통해 자동으로 빈으로 등록되어지고 있다.


2. 동적인 자동 구성 정보

2.1 자동 구성 기본 형태 구현

SpringBoot 는 ServletWebServerFactory 을 자동으로 빈으로 등록한다.
여기서 신기한 점은, SpringBoot 에 Tomcat 의존성을 넣으면 application 을 실행할 때 Tomcat 이 뜨고, Jetty 의존성을 넣으면 Jetty 가 뜬다.

어떻게 이렇게 동적으로 구성 정보가 생성되는 것일까?

이번에 직접 구현하면서 원리를 알아볼 것이다.

우선 @EnableMyAutoConfiguration 어노테이션을 생성하여, 애플리케이션 인프라스트럭쳐 빈에 해당하는 ServletWebServerFactoryDispatcherServlet 이 등록되도록 한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({DispatcherServletConfig.class , TomcatWebServerConfig.class})
public @interface EnableMyAutoConfiguration {
}

이제 이 어노테이션을 run method 가 있는 부트 시작 클래스에 달아준다.
이렇게 하면 @Import 에 등록된 클래스 안의 빈들이 부트가 시작될 때 등록이 된다.

여기서 @EnalbeMyAutoConfiguration 어노테이션을 스프링부트스럽게 조금 변경해 볼 것이다.
ImportSelector 를 사용할 것인데, ImportSelector 을 통해서 내부 파일을 이용해 구성 정보를 받을 수가 있다.

먼저 DeferredImportSelector 를 구현한 클래스를 아래와 같이 생성한다.


import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.util.stream.StreamSupport;

public class MyAutoConfigImportSelector implements DeferredImportSelector {
    private final ClassLoader classLoader;

    public MyAutoConfigImportSelector ( ClassLoader classLoader){
        this.classLoader = classLoader;
    }

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //자동 구성에 사용할 config 목록을 가져오는 메서드
        Iterable<String> candidates = ImportCandidates.load(MyAutoConfiguration.class, classLoader);
        //이 메서드가 읽어오는 경로는 아래와 같다.
        //resources/META-INF/spring/파라미터로 넘겨준 클래스의 풀 경로.imports
        return StreamSupport.stream(candidates.spliterator() , false).toArray(String[]::new);
    }
}

그리고 config 목록을 담을 파일을 생성한다.
정확한 경로에 파일을 만들어 아래와 같이 작성하였다.
(등록 할 클래스의 정확한 풀 경로를 작성해야 한다.)

tobyspring.helloboot.config.autoconfig.TomcatWebServerConfig
tobyspring.helloboot.config.autoconfig.DispatcherServletConfig

그런 다음 이전의 @EnableMyAutoConiguration 어노테이션에 만든 importSelector 클래스를 등록한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyAutoConfigImportSelector.class)
public @interface EnableMyAutoConfiguration {
}

이렇게까지만 만들어도 스프링부트의 파일
org.springframework.boot.autoconfigure.AutoConfiguration.imports 이 어떤 역할을 하는지 이해가 될 것이다.
이 파일은 스프링부트 내부의 ImportSelector 가 읽는 파일이고, 이 파일 안에 있는 클래스들은 자동 구성 빈으로 등록되는 대상이다.
다만 모든 것들을 빈으로 등록하지는 않을 것이다.
조건에 따라 빈으로 등록할 것인데 그 부분에 대해 이제 구현해 보겠다.

2.2 조건부 자동 구성 구현

이제 조건에 따른 자동 구성을 구현 해 보겠다.

그래들에 Jetty 의존성을 추가 ( implementation 'org.springframework.boot:spring-boot-starter-jetty' )
하고 아래와 같이 JettyWebserverConfig 클래스를 생성한다.


@Configuration
public class JettyWebServerConfig {
    @Bean("jettyWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new JettyServletWebServerFactory();
    }
}

그리고 imports 파일은 아래처럼 수정한다.

tobyspring.helloboot.config.autoconfig.TomcatWebServerConfig
tobyspring.helloboot.config.autoconfig.JettyWebServerConfig
tobyspring.helloboot.config.autoconfig.DispatcherServletConfig

이대로 부트를 실행하면

Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.boot.web.servlet.server.ServletWebServerFactory' available: expected single matching bean but found 2: tomcatWebServerFactory,jettyWebServerFactory
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1317)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:486)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:341)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:334)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1261)
	at tobyspring.helloboot.containerless.MySpringApplication$1.onRefresh(MySpringApplication.java:15)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:619)
	at tobyspring.helloboot.containerless.MySpringApplication.run(MySpringApplication.java:31)

이러한 에러가 발생한다.
ServletWebServerFactory 에 대한 빈이 여러 개여서 어떤 것을 선택할지 모르겠다는 에러이다.

SpringBoot 는 톰캣 라이브러리가 있으면 톰캣을, 제티 라이브러리가 있으면 제티를 띄운다.
이러한 동작 방식은 @Conditional 어노테이션을 통해 구현할 수 있다.

우선 Condition 을 구현한 클래스를 아래와 같이 생성한다.

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;

import java.util.Map;

public class MyOnClassCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attrs = metadata.getAnnotationAttributes(ConditionalMyOnClass.class.getName());
        String value = (String) attrs.get("value");
        return ClassUtils.isPresent(value, context.getClassLoader());
    }
}

로직을 잠깐 설명하자면,
Conditionmatches 메소드에는 두 개의 파라미터가 있다.
ConditionContext 로 Spring 의 실행 컨텍스트 정보를 알 수 있고,
AnnotatedTypeMetadata@Conditional(MyOnClassCondition.class) 가 붙은 대상 클래스의 어노테이션 정보를 받을 수 있다.

따라서 위의 코드는, 대상 클래스의 어노테이션 정보에서 "value" 값을 가져오고, 현재 애플리케이션 클래스패스에 "value" 값이 존재하는지 확인하는 코드이다.

이제 위의 클래스를 등록할 어노테이션을 생성한다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE , ElementType.METHOD})
@Conditional(MyOnClassCondition.class)
public @interface ConditionalMyOnClass {
    String value();
}

JettyWebServerConfigTomcatWebSerConfig 클래스를 아래와 같이 수정한다.


@Configuration
@ConditionalMyOnClass("org.eclipse.jetty.server.Server")
public class JettyWebServerConfig {
    @Bean("jettyWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new JettyServletWebServerFactory();
    }
}
@Configuration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
    @Bean("tomcatWebServerFactory")
    public ServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }
}

여기서 org.eclipse.jetty.server.Server 는 Jetty 라이브러리의 핵심이 되는 클래스이다.
따라서 이 클래스패스가 있다면 Jetty 라이브러리가 애플리케이션에 있는 것이다.

이제 애플리케이션에서 톰캣 라이브러리만 있다면 톰캣이 실행되고, 제티만 있다면 자동적으로 제티가 실행이 될 것이다.


이렇게 SpringBoot 와 유사하게 웹서버를 자동 구성 등록 해 보았다.

자동 구성 등록의 이해에 도움이 되었으면 좋겠다.

참조 : 토비의 스프링 부트 - 이해와 원리

profile
안녕하세요 개발하는 사람입니다.

0개의 댓글