blog

2024 소스 코드 공개 - 스프링부트 자동 어셈블리 원리!

서문 자동 어셈블리는 개발자의 구성 부담을 크게 덜어줍니다. 그렇다면 자동 어셈블리란 무엇일까요? 자동 어셈블리는 개발자가 직접 설정할 필요 없이 스프링이 필요한 빈을 로드하여 호...

May 19, 2025 · 13 min. read
シェア

서문.

SpringBoot의 자동 어셈블리는 개발자의 구성 부담을 많이 덜어줍니다.

그렇다면 자동화된 조립이란 무엇일까요?

자동 어셈블리는 개발자가 직접 구성하지 않고도 필요한 빈을 로드하여 호출 시간을 단축할 수 있도록 하는 Spring 시작 프로세스입니다.

다음은 Spring 스타트업 클래스입니다:

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

자동 어셈블리는 주로 @SpringBootApplication 통해 , 방법을 알아보려면 계속 읽어보세요.

&&

주석 분석

Spring BootApplication 어노테이션은 다음과 같은 여러 어노테이션으로 나뉘며, EnableAutoConfiguration 대한 핵심 어노테이션을 자동으로 어셈블한 다음 이 어노테이션의 분석에 중점을 둡니다.

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

EnableAutoConfiguration

주석 분석:

메타 주석 외에도 두 가지 주요 주석이 있는데, 하나는 @AutoConfigurationPackage 다른 하나는 @Import(AutoConfigurationImportSelector.class), 아래에서 하나씩 분석합니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
 
 /**
 * Environment property that can be used to override when auto-configuration is
 * enabled.
 */
 String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
 
 /**
 * Exclude specific auto-configuration classes such that they will never be applied.
 * @return the classes to exclude
 */
 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
 */
 String[] excludeName() default {};
 
}

AutoConfigurationPackage

마찬가지로, 메타 노트 외에 노트 @Import(AutoConfigurationPackages.Registrar.class) 남습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
 
 /**
 * Base packages that should be registered with {@link AutoConfigurationPackages}.
 * <p>
 * Use {@link #basePackageClasses} for a type-safe alternative to String-based package
 * names.
 * @return the back package names
 * @since 2.3.0
 */
 String[] basePackages() default {};
 
 /**
 * Type-safe alternative to {@link #basePackages} for specifying the packages to be
 * registered with {@link AutoConfigurationPackages}.
 * <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.
 * @return the base package classes
 * @since 2.3.0
 */
 Class<?>[] basePackageClasses() default {};
 
}

분석을 계속 진행하면 핵심 메서드인 AutoConfigurationPackages.Registrar.class 가진 registerBeanDefinitions 클래스가 임포트된 것을 볼 수 있으며, 이는 사용자 자신의 패키지에서 빈을 로드하는 데 중점을 둡니다.

자동 조립된 원두는 두 부분으로 나뉩니다:

하나는 사용자가 정의한 빈이고 다른 하나는 사용자가 pom 파일에 도입한 종속 빈입니다.

/** * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing * configuration. */ 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)); } }

AutoConfigurationImportSelector

이 클래스에는 종속 빈을 빈 컨테이너에 로드하는 핵심 메서드인 getAutoConfigurationEntry 있습니다.

 protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
 if (!isEnabled(annotationMetadata)) {
 return EMPTY_ENTRY;
 }
 AnnotationAttributes attributes = getAttributes(annotationMetadata);
 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
 configurations = removeDuplicates(configurations);
 Set<String> exclusions = getExclusions(annotationMetadata, attributes);
 checkExcludedClasses(configurations, exclusions);
 configurations.removeAll(exclusions);
 configurations = getConfigurationClassFilter().filter(configurations);
 fireAutoConfigurationImportEvents(configurations, exclusions);
 return new AutoConfigurationEntry(configurations, exclusions);
 }

이 메서드 내부의 주요 로직입니다:

이 메서드는 모든 후보 구성을 가져오는 데 사용되며, 이 메서드는 스프링팩토리로더를 통해 META-INF/spring.factories 내부의 모든 구성을 로드하는 데 사용됩니다.

List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
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 메서드에 대한 주요 분석은 loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList()); 메서드가 내부의 모든 구성을 classLoader.getResources(FACTORIES_RESOURCE_LOCATION); 이유를 설명합니다.

	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()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					String[] factoryImplementationNames =
							StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
					for (String factoryImplementationName : factoryImplementationNames) {
						result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
								.add(factoryImplementationName.trim());
					}
				}
			}
			// Replace all lists with unmodifiable lists containing unique elements
			result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
					.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
			cache.put(classLoader, result);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
		return result;
	}

계속해서 더 자세히 살펴보면 이 메서드는 주어진 이름의 모든 리소스를 찾고, 여기서 주어진 이름은 META-INF/spring.factories, 이 메서드가 하는 일은 프로젝트 종속성 내의 모든 리소스 파일을 체크 아웃하는 것임을 알 수 있습니다.

 public Enumeration<URL> getResources(String name) throws IOException {
 Objects.requireNonNull(name);
 @SuppressWarnings("unchecked")
 Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
 if (parent != null) {
 tmp[0] = parent.getResources(name);
 } else {
 tmp[0] = BootLoader.findResources(name);
 }
 tmp[1] = findResources(name);
 return new CompoundEnumeration<>(tmp);
 }

spring-boot-autoconfigure

이 종속성을 강조한 이유는 일반적으로 사용하는 대부분의 종속성이 이 자동 구성 종속성을 사용하기 때문입니다.

소스 구조를 열면 자동 구성에 원하는 파일인 spring.factories가 있는 것을 확인할 수 있습니다.

이 파일을 열어보면 초기화 클래스, 리스너, 다음 주인공인 EnableAutoConfiguration 등을 포함할 수 있는 여러 자동 구성 클래스가 있는 것을 볼 수 있습니다. 그렇다면 왜 이렇게 많은 클래스가 로드되지 않는지, 그 이유에 대해서는 계속 읽어보시기 바랍니다.

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
......생략하기에는 너무 많은 내용

EnableAutoConfiguration 로드 중

위에서 언급한 메서드에 getCandidateConfigurations 함수가 있는데, getSpringFactoriesLoaderFactoryClass() 이 메서드는 간단하며 EnableAutoConfiguration.class; 클래스를 반환합니다.

/** * Return the auto-configuration class names that should be considered. By default * this method will load candidates using {@link SpringFactoriesLoader} with * {@link #getSpringFactoriesLoaderFactoryClass()}. * @param metadata the source metadata * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation * attributes} * @return a list of candidate configurations */ 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; } /** * Return the class used by {@link SpringFactoriesLoader} to load configuration * candidates. * @return the factory class */ protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }

그런 다음 loadFactoryNames 메서드를 계속 살펴보십시오. 이해를 돕기 위해 디버그를 수행하면 기본값이 EnableAutoConfiguration 구성 클래스의 키를 가져 오는 것을 볼 수 있으며 여기에는 133 개가 있으며이 133 개는 org 내부의 spring.factories입니다. 구성 클래스 아래에 있는 springframework.boot.autoconfigure.EnableAutoConfiguratio입니다. 여기까지입니다. 133개의 클래스가 모두 주입되지 않았다면 어디를 찾아서 어떤 클래스를 찾아야 할까요? 정답은 '아니요, 온디맨드 인젝션'입니다. 그렇다면 온디맨드 인젝션을 호출하는 방법은 무엇일까요? 다음 분석입니다.

온디맨드 인젝션

여기서 주입할 133개의 콩 중 몇 개를 무작위로 분석하고 나머지는 같은 방식으로 분석합니다.

1.org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration

여기에서 이 구성 클래스를 분석하면 이 클래스가 RestTemplate의 자동 구성 클래스임을 알 수 있으며, 먼저 이 클래스의 어노테이션에 집중하세요.

@Configuration(proxyBeanMethods = false)//Spring에 이것이 구성 클래스라고 알리기
@AutoConfigureAfter(HttpMessageConvertersAutoConfiguration.class)//HttpMessageConvertersAutoConfiguration 이후의 어셈블리
@ConditionalOnClass(RestTemplate.class)//프로젝트 레드에 Rest 템플릿이 있나요?.class이 클래스는 어셈블리만 있는 경우
@Conditional(NotReactiveWebApplicationCondition.class)//반응형 웹 앱의 예와 아니오
public class RestTemplateAutoConfiguration {
	//.....
}

이 어노테이션을 통해 이러한 조건이 충족되는지 확인한 다음, 이 구성 클래스가 주입되는 컨테이너, 우선 restTemplateBuilderConfigurer, restTemplateBuilder, 주석 뒤에 코드에 작성한 어노테이션의 의미, 이 클래스가 컨테이너에 주입되어야 한다는 것을 확인할 수 있습니다.

public class RestTemplateAutoConfiguration {
	@Bean
	@Lazy// 
	@ConditionalOnMissingBean//주입하기 전에 그러한 빈이 없음을 나타냅니다.
	public RestTemplateBuilderConfigurer restTemplateBuilderConfigurer(
			ObjectProvider<HttpMessageConverters> messageConverters,
			ObjectProvider<RestTemplateCustomizer> restTemplateCustomizers,
			ObjectProvider<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers) {
		RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer();
		configurer.setHttpMessageConverters(messageConverters.getIfUnique());
		configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().collect(Collectors.toList()));
		configurer.setRestTemplateRequestCustomizers(
				restTemplateRequestCustomizers.orderedStream().collect(Collectors.toList()));
		return configurer;
	}
	@Bean
	@Lazy
	@ConditionalOnMissingBean
	public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer) {
		RestTemplateBuilder builder = new RestTemplateBuilder();
		return restTemplateBuilderConfigurer.configure(builder);
	}
	static class NotReactiveWebApplicationCondition extends NoneNestedConditions {
		NotReactiveWebApplicationCondition() {
			super(ConfigurationPhase.PARSE_CONFIGURATION);
		}
		@ConditionalOnWebApplication(type = Type.REACTIVE)
		private static class ReactiveWebApplication {
		}
	}
}

원두 컨테이너에 두 원두를 모두 넣으면 두 원두가 모두 존재한다는 것을 보여줍니다.

2.org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

자동 구성 클래스의 분석에서 이것이 Redis 자동 구성 클래스임을 알 수 있으며, 조건이 충족되면이 클래스는 컨테이너에 redisTemplate 및 stringRedisTemplate이 될 것이며, 현재 프로젝트에는이 클래스에 RedisOperations가 없기 때문에 분명히이 조건을 충족하지 않으므로 다음을 확인합니다. 두 개의 빈을 살펴볼 필요가 없으며 주입되지 않으므로 다음을 확인합니다.

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}
	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		return new StringRedisTemplate(redisConnectionFactory);
	}
}

유효성 검사 프로세스:

 public static void main(String[] args) {
 ConfigurableApplicationContext run = SpringApplication.run(InitProjectTemplateApplication.class, args);
 boolean beanDefinition = run.containsBeanDefinition("redisTemplate");
 System.out.println("redisTemplate존재하나요?>"+beanDefinition);
 boolean builder = run.containsBeanDefinition("stringRedisTemplate");
 System.out.println("stringRedisTemplate존재하나요?>"+builder);
 }
  
redisTemplate존재하나요?>false
stringRedisTemplate존재하나요?>false

이 글은 여기까지이며, 궁금한 점이 있으시면 언제든지 공유해 주세요.

더 많은 기사를 계속 게시하세요!

Read next

학습 노트 React 클래스 라이프사이클 구성

일반적인 React 클래스 수명 주기 구성:,()

May 18, 2025 · 3 min read