Spring Boot의 자동 구성 원리

Spring Boot 자동 구성 분석

1. 애플리케이션 시작 클래스 주요 구성 요소

@SpringBootApplication 어노테이션은 Spring Boot 애플리케이션의 진입점이며, 여러 중요한 어노테이션을 결합하여 자동 구성을 가능하게 합니다.

@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 {
    // ...
}
이 어노테이션은 내부적으로 @Import를 통해 Registrar 클래스를 로드하고, 이 클래스는 registerBeanDefinitions 메서드를 호출하여 애플리케이션 패키지 아래의 컴포넌트들을 스캔합니다.

2. 자동 구성의 핵심 흐름

2.1 @EnableAutoConfiguration 어노테이션

@EnableAutoConfiguration은 자동 구성 기능을 활성화하는 데 사용됩니다. 이를 통해 AutoConfigurationImportSelector 클래스가 import되며, 이는 자동 구성의 진입점입니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigSelector.class)
public @interface EnableAutoConfiguration {
    // ...
}
AutoConfigSelector는 selectImports 메서드를 통해 후보 구성 클래스 목록을 가져오고, 최종적으로 spring.factories 파일에서 관련 항목들을 읽어옵니다.

2.2 AutoConfigSelector 핵심 메서드

아래 코드는 후보 구성 클래스들을 필터링하고 조건에 맞는 것들만 선택하는 과정을 보여줍니다.

public class AutoConfigSelector implements DeferredImportSelector {
    public String[] selectImports(AnnotationMetadata metadata) {
        if (!isEnabled(metadata)) {
            return new String[0];
        }
        
        ConfigEntry entry = getConfigEntry(metadata);
        return StringUtils.toArray(entry.getConfigurations());
    }

    protected ConfigEntry getConfigEntry(AnnotationMetadata metadata) {
        if (!isEnabled(metadata)) {
            return EMPTY_ENTRY;
        }

        List<String> candidates = getCandidates(metadata);
        candidates = removeDuplicates(candidates);

        Set<String> exclusions = getExclusions(metadata);
        validateExclusions(candidates, exclusions);
        candidates.removeAll(exclusions);

        candidates = applyConditions(candidates);
        notifyEvents(candidates, exclusions);

        return new ConfigEntry(candidates, exclusions);
    }

    protected List<String> getCandidates(AnnotationMetadata metadata) {
        return SpringLoader.loadFactories(getFactoryClass(), getClassLoader());
    }
}

3. 상세한 프로세스 및 소스 추적

3.1 후보 구성 클래스 로드하기

SpringLoader는 META-INF/spring.factories 파일로부터 후보 구성 클래스 목록을 로드합니다.

protected List<String> getCandidates(AnnotationMetadata metadata) {
    List<String> configurations = SpringLoader.loadFactories(
            getFactoryClass(), getClassLoader());
    return configurations;
}

protected Class getFactoryClass() {
    return EnableAutoConfiguration.class;
}

3.2 SpringLoader.loadFactories 소스

이 메서드는 모든 jar 파일 내부의 spring.factories 파일을 검색하고 해당 내용을 파싱합니다.

public static List<String> loadFactories(Class factoryType, ClassLoader loader) {
    String typeName = factoryType.getName();
    return loadFactoriesMap(loader).getOrDefault(typeName, Collections.emptyList());
}

private static Map loadFactoriesMap(ClassLoader loader) {
    MultiValueMap result = cache.get(loader);
    if (result != null) {
        return result;
    }

    try {
        Enumeration<URL> urls = (loader != null ?
                loader.getResources("META-INF/spring.factories") :
                ClassLoader.getSystemResources("META-INF/spring.factories"));
        result = new LinkedHashMap<>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            for (Map.Entry entry : properties.entrySet()) {
                String type = ((String) entry.getKey()).trim();
                for (String impl : StringUtils.split((String) entry.getValue(), ",")) {
                    result.add(type, impl.trim());
                }
            }
        }
        cache.put(loader, result);
        return result;
    } catch (IOException e) {
        throw new IllegalArgumentException("Failed to load factories", e);
    }
}

3.3 조건 필터링 - ConditionFilter

조건 필터는 각 구성 클래스가 적용 가능한지 평가합니다.

public class ConditionFilter {
    private final ConditionEvaluator evaluator;

    public Iterable<String> filter(Collection<String> classNames) {
        List<String> results = new ArrayList<>();
        for (String className : classNames) {
            if (isCandidate(className)) {
                MetadataReader reader = getMetadataReader(className);
                if (!evaluator.shouldSkip(reader)) {
                    results.add(className);
                }
            }
        }
        return results;
    }
}

3.4 ConditionEvaluator 평가

ConditionEvaluator는 각 조건 어노테이션이 충족되는지 확인합니다.

public class ConditionEvaluator {
    public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
        List<Condition> conditions = collectConditions(metadata);
        AnnotationAwareOrderComparator.sort(conditions);

        for (Condition condition : conditions) {
            if (!condition.matches(context, metadata)) {
                return true; // 조건 미충족 시 스킵
            }
        }
        return false;
    }
}

4. 대표적인 조건 어노테이션

4.1 @ConditionalOnClass 소스

클래스 경로에 특정 클래스가 존재할 경우에만 적용됩니다.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(ClassCondition.class)
public @interface ConditionalOnClass {
    Class[] value() default {};
    String[] name() default {};
}

class ClassCondition extends FilteringCondition {
    public ConditionOutcome evaluate(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (hasClass(context, metadata)) {
            return ConditionOutcome.match();
        }
        return ConditionOutcome.noMatch();
    }
}

4.2 @ConditionalOnMissingBean 소스

컨테이너에 특정 Bean이 없을 때만 적용됩니다.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(MissingBeanCondition.class)
public @interface ConditionalOnMissingBean {
    Class[] value() default {};
    String[] name() default {};
}

class MissingBeanCondition extends BeanRegistry implements ConfigurationCondition {
    public ConditionOutcome evaluate(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (!hasBean(context, metadata)) {
            return ConditionOutcome.match();
        }
        return ConditionOutcome.noMatch();
    }
}

5. 자동 구성 예제

DispatcherServlet 구성 클래스는 다음과 같이 정의될 수 있습니다.

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(WebApplicationType.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(type = "org.springframework.web.servlet.DispatcherServlet")
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
    // ...
}

태그: SpringBoot AutoConfiguration SpringFactoriesLoader

8월 23일 05:14에 게시됨