스프링은 스프링 컨테이너를 통해서 의존성 주입 기능을 지원한다. 컨테이너에 들어가는 객체들을 빈이라고 칭하고, 스프링은 빈으로 등록할 객체를 찾은 뒤에 컨테이너에 등록하고, 필요한 경우 컨테이너에서 빈을 빼오는 일을 수행한다.
이 과정은 그냥 애플리케이션을 시작하면 자동으로 이루어지지만, 어떤 매커니즘으로 이루어지는지 궁금했기 때문에, 조사해보고자한다.
코드 중에서 불필요한 부분은 생략했다. 주로 내가 공부하려는 기능과 무관한 코드거나, 클래스 사전설정 코드거나, 로그를 다루는 로직이다.
Bean 등록과정
먼저 ApplicationContext에 Bean을 등록하기 위해서는 두 가지 과정이 필요하다.
Bean은 스프링 컨테이너에 등록될 때 Bean만 등록되는 것이 아니라 BeanDefinition이라고하는, Bean의 메타데이터를 담고 있는 객체도 함께 저장된다. 주로 패키지이름 + 클래스이름, 스코프 또는 생애주기, 빈이 의존 중인 다른 객체에 대한 정보가 들어가있다.
따라서 컴포넌트 스캔 과정에서는
1. @ComponentScan 어노테이션이 붙은 클래스들을 찾는다.
2. 그 클래스들을 BeanDefinition 객체를 만든 후, ApplicationContext에 등록한다.
3. 이후 그 클래스들의 패키지를 토대로 전체 패키지를 조사한다. 이 때는 클래스 메타데이터를 활용해 @Bean, @Component 등의 어노테이션이 붙었는지 확인한다. 그리고 BeanDefinition 객체를 만든다.
4. 이후 BeanDefinition 객체를 토대로 실제 객체를 생성하고, 의존성을 주입한다.
정도의 과정이 필요하다.
이 Bean을 등록하고 생성하는 과정은 모두 BeanFactory 인터페이스에서 기인한다.
┌───────────────┐
│ │ ┌───────────────┐ ┌───────────────┐
│ BeanFactory │ │ Application │ │ResourcePattern│
│ (interface) │ → → → → │ EventPublisher│ │ Resolver │
│ │ ↓ │ (interface) │ │ (interface) │
└───────────────┘ ↓ └───────────────┘ └───────────────┘
↓ ↓ ↓ ↓
┌───────────────┐ ┌───────────────┐ ↓ ┌───────────────┐ ↓ ┌───────────────┐
│ Listable │ │ Hierarchical │ ↓ │ Environment │ ↓ │ MessageSource │
│ BeanFactory │ │ BeanFactory │ ↓ │ Capable │ ↓ │ (interface) │
│ (interface) │ │ (interface) │ ↓ │ (interface) │ ↓ │ │
└───────────────┘ └───────────────┘ ↓ └───────────────┘ ↓ └───────────────┘
↓ ↓ ↓ ↓ ↓ ↓
↓ ↓ ┌───────────────┐ ↓ ↓ ↓
↓ ↓ │ Application │ ↓ ↓ ↓
→ → → → → → → → → → → →│ Context │ ← ← ← ← ← ← ← ← ← ← ← ←
│ (interface) │
└───────────────┘
이정도의 계층구조로 생겼으며, BeanFactory 인터페이스가 가장 기본적인 빈 생성 및 등록을 관리한다.
컴포넌트 스캔 시작하기
가장 기본은 메인 메서드에 있는 SpringApplication.run 클래스
SpringApplication.run(DemoApplication.class, args);
↓
SpringApplication(primaySources).run(args)
↓
public ConfigurableApplicationContext run(String... args) {
...
ConfigurableApplicationContext context = null;
...
try {
...
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
...
}
...
}
run 메서드를 타고가다보면 최종적인 run 메서드가 나온다.
여기서 ApplicationContext를 생성하고, 몇 가지 초기설정을 마친다. 그리고 호출하는 메서드가 prepareContext() 메서드와 refreshContext() 메서드. 전자는 컴포넌트 스캔을 위해서 @ComponentScan 어노테이션이 있는 클래스의 정보를 가져와 ApplicationContext에 등록하는 것이고, 후자는 전자에서 담은 클래스의 패키지를 토대로 스캔을 진행한다.
@ComponentScan 어노테이션이 속한 패키지 가져오기
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, @Nullable Banner printedBanner) {
context.setEnvironment(environment);
...
if (!AotDetector.useGeneratedArtifacts()) {
// Load the sources
Set<Object> sources = getAllSources();
Assert.state(!ObjectUtils.isEmpty(sources), "No sources defined");
load(context, sources.toArray(new Object[0])); //여기서
}
...
}
먼저 prepareContext 메서드.
ApplicationContext의 설정을 다루며, 이후에 load() 메서드를 호출한다. load() 메서드에 전해지는 매개변수 sources는 getAllSources() 메서드를 통해 얻어지며, 이는 보통 main 메서드가 속한 클래스다. @SpringBootApplication 어노테이션이 속한 클래스.
public static void main(Stringp[] args) {
SpringApplication.run(DemoApplication.class, args);
}
스프링 어플리케이션의 main 메서드를 보면 인자로 args와 함께 메인클래스의 .class를 같이 넘겨주는데, .class가 바로 load() 메서드로 같이 넘어간다.
//SpringApplication 클래스
protected void load(ApplicationContext context, Object[] sources) {
...
loader.load();
}
↓
//BeanDefinitionLoader 클래스
void load() {
for (Object source : this.sources) {
load(source);
}
}
↓
private void load(Object source) {
if (source instanceof Class<\?> type) {
load(type);
return;
}
...
}
↓
private void load(Class<\?> source) {
...
if (isEligible(source)) {
this.annotatedReader.register(source);
}
}
//AnnotatedBeanDefinitionReader 클래스
public void register(Class<\?>... componentClasses) {
for (Class<\?> componentClass : componentClasses) {
registerBean(componentClass);
}
}
↓
public void registerBean(Class<\?> beanClass) {
doRegisterBean(beanClass, null, null, null, null);
}
↓
private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
Class<? extends Annotation> @Nullable [] qualifiers, @Nullable Supplier<T> supplier,
BeanDefinitionCustomizer @Nullable [] customizers) {
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
return;
}
abd.setAttribute(ConfigurationClassUtils.CANDIDATE_ATTRIBUTE, Boolean.TRUE);
abd.setInstanceSupplier(supplier);
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
abd.setScope(scopeMetadata.getScopeName());
String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
...
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}
load() 메서드를 따라가다보면 doRegisterBean() 메서드가 나온다. 위에서 @SpringBootApplication 어노테이션이 붙은 메서드의 .class 파일을 넘겨주었는데, 이를 토대로 BeanDeifinition 객체를 생성하고 ApplicationContext에 등록한다.
가장 마지막줄의 AnnotatedGenericBeanDefinition과, 생성된 클래스 메타데이터를 통해 생성된 beanName을 토대로 BeanDefinitionHolder에 전달, 최종적으로 ApplicationContext에 저장한다. BeanDefinitionHolder를 사용하는 이유는 BeanDefinition이 여러 개니까 일괄적인 처리를 위해서 사용하는듯하다.
그래서 여기까지 가장 기본이 되는 메인클래스의 BeanDefinition을 만들어 ApplicationContext에 저장했다. 이외에도 내부적으로 사용되는 빈 약간도 ApplicationContext에 들어간다.
@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {
System.out.println("Let's inspect the beans provided by Spring Boot:");
// Spring Boot 에서 제공되는 Bean 확인
String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
System.out.println(beanName);
}};
}

실제로 생성된 Bean의 이름을 찍어보면 내부적으로 사용되는 빈 역시 많이 있음을 알 수 있다. 그중에서도 application 이라는 이름을 가진 빈은 메인클래스의 빈이다.
패키지 스캔 및 Bean 등록하기
SpringApplication.run(DemoApplication.class, args);
↓
SpringApplication(primaySources).run(args)
↓
public ConfigurableApplicationContext run(String... args) {
...
ConfigurableApplicationContext context = null;
...
try {
...
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
...
}
...
}
이제 prepareContext() 메서드에서 할 일은 끝났다. 그리고 prepareContext() 메서드에서 수정한 ApplicationContext를 가지고 refreshContext() 메서드가 호출된다.
@Override
public void refresh() throws BeansException, IllegalStateException {
try {
...
try {
...
invokeBeanFactoryPostProcessors(beanFactory);
...
}
}
}
↓
protected void invokeBeanFactoryPostProcessors (
ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory,
getBeanFactoryPostProcessors());
...
}
↓
final class PostProcessorRegistrationDelegate {
public static void invokeBeanFactoryPostProcessors(
...
registryProcessor.postProcessBeanDefinitionRegistry(registry);
...
}
}
↓
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor ... {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
...
processConfigBeanDefinitions(registry);
}
}
↓
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
...
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List\<BeanDefinitionHolder\> configCandidates = new ArrayList\<\>();
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (...) {
...
}
else if (...)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
...
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = CollectionUtils.newHashSet(configCandidates.size());
do {
...
parser.parse(candidates);
...
}
...
}
refresh() → invokeBeanFactoryPostProcessors() → postProcessBeanDefinitionRegistry → processConfigBeanDefinitions() 메서드로 흐름이 이어지게된다. 그리고 마지막으로 호출되는 parse() 메서드가 핵심이다.
parse() 메서드에 매개변수로 넘어가는 candidates는 BeanDeifinitionHolder의 Set이다.
refresh() 메서드에 들어오기 전에 메인클래스와 일부 빈들의 BeanDefinition을 ApplicationContext에 등록했는데, ApplicationContext를 가져와서 BeanDefinition들을 뽑은 후에 Set으로 만든 것이 candidates다. 즉, 이전에 ApplicationContext에 등록한 BeanDefinition들을 parse() 메서드에 넘긴다는 뜻
//parse() -> processConfigurationClass() -> doProcessConfigurationClass() 호출됨.
//아래는 doProcessConfigurationClass() 의 일부.
// Search for locally declared @ComponentScan annotations first.
Set\<AnnotationAttributes\> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class,
MergedAnnotation::isDirectlyPresent);
// Fall back to searching for @ComponentScan meta-annotations (which indirectly
// includes locally declared composed annotations).
if (componentScans.isEmpty()) {
componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(),
ComponentScan.class, ComponentScans.class, MergedAnnotation::isMetaPresent);
}
parse() 메서드는 processConfigurationClass() 메서드를 호출하게된다.
이 메서드는 @ComponentScan 어노테이셔을 찾는 과정이다. 위에는 단순히 @ComponentScan 어노테이션을 찾는 과정이고, 아래는 @SpringBootApplication 어노테이션과 같이 @ComponentScan 어노테이션이 코드에 명확하게 드러나지 않은 경우의 @ComponentScan 어노테이션을 찾는 과정이다.
if (!componentScans.isEmpty()) {
...
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder\> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
이후 @ComponentScan 어노테이션이 붙은 클래스를 토대로 parse() 메서드를 돌린다. scannedBeanDefinitions가 parse() 후 BeanDefinition을 Set으로 담아두는 역할을 한다. 이후 반복문은 만약 BeanDefinition 클래스가 래퍼클래스일 경우 원본 BeanDefinition을 가져오는 역할을 수행한다.
//ComponentScanAnnotationParser 클래스
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
...
Set<String\> basePackages = new LinkedHashSet<\>();
...
return scanner.doScan(StringUtils.toStringArray(basePackages));
}
scannedBeanDefinitions를 가져오는 parse() 메서드를 보면, 스캐너를 호출해 설정을 마치고 doScan() 메서드를 설정한다.
여기서 basePackages는 앞서 @ComponentScan 어노테이션이 붙은 클래스의 패키지에 해당한다. 즉, @ComponentScan 어노테이션이 붙은 클래스의 패키지를 쫙 검사한다.
public @interface ComponentScan {
/**
* Alias for {@link #basePackages}.
* <p>Allows for more concise annotation declarations if no other attributes
* are needed — for example, {@code @ComponentScan("org.example")}
* instead of {@code @ComponentScan(basePackages = "org.example")}.
*/
@AliasFor("basePackages")
String[] value() default {};
ComponentScan 어노테이션은 이렇게 패키지를 미리 설정할 수도 있고, 만약 설정하지 않은 경우에는 스프링이 알아서 찾은 후에 basePackages 를 설정한다.
protected Set<BeanDefinitionHolder\> doScan(String... basePackages) {
...
Set<BeanDefinitionHolder\> beanDefinitions = new LinkedHashSet<\>();
for (String basePackage : basePackages) {
Set<BeanDefinition\> candidates = findCandidateComponents(basePackage);
...
}
return beanDefinitions;
}
↓
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
...
return scanCandidateComponents(basePackage);
}
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition\> candidates = new LinkedHashSet<\>();
try {
String packageSearchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern;
...
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPattern);
...
for (Resource resource : resources) {
String filename = resource.getFilename();
...
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
...
candidates.add(sbd);
}
else {
...
}
}
}
...
}
return candidates;
}
}
그리고 scanCandidateComponents() 메서드에서 컴포넌트 스캔의 핵심적인 기능들이 동작한다.
위에서 전해준 basePackages를 기준으로 패키지의 하위클래스들을 전부스캔한다.
classpath*:com/example/**/*.class
만약 패키지다 com.example이라면 이렇게 .class 파일을 모두 읽어온다. 읽어오는 과정은 스프링에서 만든 ResourcePatternResolver가 수행한다.
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter filter : this.excludeFilters) {
if (filter.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter filter : this.includeFilters) {
if (filter.match(metadataReader, getMetadataReaderFactory())) {
registerCandidateTypeForIncludeFilter(metadataReader.getClassMetadata().getClassName(), filter);
return isConditionMatch(metadataReader);
}
}
return false;
}
가져온 .class 파일은 isCandidateComponent() 메서드를 두 개 통과하게 되는데, 첫 번째 메서드가 다음과 같다.
첫 번째 반복문에서는 제외해야할 클래스인지 여부를 조사하고, 두 번째 반복문에서는 @Bean, @Component와 같이 추가해야할 클래스인지 여부를 조사한다.
이 방식은 리플렉션이 아니라, 바이트코드 단위에서 .class 파일을 읽어보는 방식이다. 따라서 리플렉션보다 빠르다고. 또한 클래스를 로딩하지 않고 어노테이션만 확인해야하는 컴포넌트 스캔 과정의 특성상, .class 파일을 읽어보는게 빠르다.
이를 ASM metadata Reading이라고도하는데, ASM은 바이트코드를 읽는 자바의 라이브러리다.
두 번째 반복문에서 isConditionMatch() 어노테이션이 있는데, 이는 ApplicationContext에 등록가능한 빈이더라도 이를 빈으로 등록하면 안되는 경우가 종종있다. 종종 @Component 어노테이션에 @Conditional 어노테이션을 추가하는 경우가 있는데, 만약 Conditional 어노테이션에서 필터링되어 빈으로 등록할 조건이 안되는 클래스는 빈으로 등록되지 않는다. 그 기능을 isConditionMatch() 어노테이션을 통해서 수행한다.
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
return (metadata.isIndependent() && (metadata.isConcrete() ||
(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
}
두 번째 isCandidateComponent() 메서드
별다른 것은 없고, 빈으로 등록되어야하는 클래스는 내부클래스 또는 익명클래스가 아니어야하고, 인터페이스나 추상클래스가 아니어야하지만, 만약 추상클래스더라도 @Lookup 어노테이션이 있다면 빈으로 등록할 수 있기때문에 필터링과정을 한 번 더 거치게된다.
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition\> candidates = new LinkedHashSet<\>();
try {
String packageSearchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern;
...
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPattern);
...
for (Resource resource : resources) {
String filename = resource.getFilename();
...
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
...
candidates.add(sbd);
}
else {
...
}
}
}
...
}
return candidates;
}
}
그래서 isCandidateComponent() 메서드를 두 번 통과한 경우 cadidates에 BeanDeifnition이 add된다.
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
...
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
다시 doScan() 메서드로 돌아가면 여기서 반환된 candidates에 있는 BeanDefinition을 하나씩 ApplicationConext에 등록한다.
Bean 생성하기
//AnnotationConfigurableApplicationContext
@Override
public void refresh() throws BeansException, IllegalStateException {
...
try {
...
try {
...
invokeBeanFactoryPostProcessors(beanFactory);
//invokeBeanFactoryPostProcessors를 통해서 위의 과정에서의
//BeanDefinition 등록이 일어나게된다.
...
finishBeanFactoryInitialization(beanFactory);
...
}
}
}
↓
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
...
beanFactory.preInstantiateSingletons();
}
↓
//DefaultListableBeanFactory 클래스
@Override
public void preInstantiateSingletons() throws BeansException {
...
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
...
try {
List<CompletableFuture<\?>> futures = new ArrayList<\>();
for (String beanName : beanNames) /* BeanDefinition 이름들의 배열 */ {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
if (!mbd.isAbstract() && mbd.isSingleton()) {
CompletableFuture<\?> future = preInstantiateSingleton(beanName, mbd);
...
}
...
}
}
}
여기까지 마쳤다면 다시 ApplicationContext 클래스로 돌아간다. 그리고 finishBeanFactoryInitialization 메서드가 호출된다.
그리고나서 BeanFactory 인터페이스의 preInstantiateSingletons() 메서드가 호출된다.
private @Nullable CompletableFuture<\?> preInstantiateSingleton(String beanName, RootBeanDefinition mbd) {
...
if (!mbd.isLazyInit()) {
try {
instantiateSingleton(beanName);
}
...
↓
private void instantiateSingleton(String beanName) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) {
getBean(beanName);
}
}
else {
getBean(beanName);
}
}
↓
//AbstractBeanFactory 클래스
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
↓
protected <T> T doGetBean(
String name, @Nullable Class<T> requiredType, @Nullable Object @Nullable [] args, boolean typeCheckOnly)
throws BeansException {
...
// Create bean instance.
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
});
}
여기서 본격적으로 Bean을 생성한다.
BeanNames 배열에서 빈 이름을 가져오고, 이 이름을 토대로 ApplicationContext에서 빈을 가져온다. 그리고 createBean 메서드를 통해 빈을 생성한다.
//AbstractAutowireCapableBeanFactory 클래스
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] args)
throws BeanCreationException {
...
try {
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
...
return beanInstance;
}
...
}
↓
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] args)
throws BeanCreationException {
// Instantiate the bean.
BeanWrapper instanceWrapper = null;
...
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
Object bean = instanceWrapper.getWrappedInstance();
...
// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
...
}
...
}
//이 윗과정에서는 생성된 Bean 토대로 BeanWrapper 클래스를 만든 후에 넘긴다
↓
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
...
if (hasInstantiationAwareBeanPostProcessors()) {
...
for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
...
}
}
boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);
if (needsDepCheck) {
PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
checkDependencies(beanName, mbd, filteredPds, pvs);
}
...
}
createBean -> doCreateBean -> populateBean() 메서드로 이어지게된다.
doCreateBean() 메서드에서는 본격적으로 빈을 생성한다. createBeanInstance() 메서드를 통해서 빈을 생성한다.
if (mbd.getFactoryMethodName() != null) {
return instantiateUsingFactoryMethod(beanName, mbd, args);
}
...
// Candidate constructors for autowiring?
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}
// Preferred constructors for default construction?
ctors = mbd.getPreferredConstructors();
if (ctors != null) {
return autowireConstructor(beanName, mbd, ctors, null);
}
// No special handling: simply use no-arg constructor.
return instantiateBean(beanName, mbd);
}
createBeanInstance() 메서드 내부. 각각 팩토리메서드, Constructor Injection, No-args Constructor 메서드다.
@Configuration
class AppConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate rt = new RestTemplate();
rt.setRequestFactory(...);
return rt;
}
}
이렇게 스프링이 관리하지 않는 특정 객체를 커스텀한 뒤 빈으로 등록하는 경우가 있는데, 이 경우는 팩토리 메서드에 해당한다고한다.
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
...
if (hasInstantiationAwareBeanPostProcessors()) {
...
for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
...
}
}
boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);
if (needsDepCheck) {
PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
checkDependencies(beanName, mbd, filteredPds, pvs);
}
...
}
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
}
...
return pvs;
}
populateBean() 메서드는 DI를 담당한다. 그중에서도 postProcessProperties 메서드는 어디에 DI를 해야할지 조사하고, inject() 메서드를 호출한다. PropertyValues pvs는 필드 한 개를 뜻한다.
protected void inject(Object target, @Nullable String requestingBeanName, @Nullable PropertyValues pvs)
throws Throwable {
if (!shouldInject(pvs)) {
return;
}
if (this.isField) {
Field field = (Field) this.member;
ReflectionUtils.makeAccessible(field);
field.set(target, getResourceToInject(target, requestingBeanName));
}
else {
try {
Method method = (Method) this.member;
ReflectionUtils.makeAccessible(method);
method.invoke(target, getResourceToInject(target, requestingBeanName));
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
inject() 메서드에서는 본격적으로 필드에 값을 주입한다. 필드에 이미 값이 주입되었다면 값을 주입하지 않고 넘어가고, 그렇지 않은 경우는 리플렉션을 통해서 값을 주입한다.
스프링에서 DI는
1. 필드의 타입
2. @Primary 어노테이션을 통해 미리 특정 구현체를 주입할 것을 설정
3. @Qualifier 어노테이션 사용
순으로 타입을 지정한다.
만약 3번까지해도 필드에 주입할 타입을 고르지 못한 경우에는 에러가 발생한다.
이 직후 postProcessAfterInitialization 메서드가 호출되어 PostConstruct 어노테이션이 붙은 메서드가 호출되면서 스프링 비즈니스 로직으로 넘어가게된다.
그래서 대략적으로 흐름을 살펴보자면..
1. 먼저 코드가 실행되면 SpringApplication이 먼저 동작한다.
2. 여기서 @ComponentScan 어노테이션이 있는 클래스부터 BeanDefinition 객체를 만들어 컨테이너에 등록한다.
3. BeanDefinition에 있는 클래스의 패키지를 전부 조사하여 빈으로 등록할 수 있다면 BeanDefinition 객체를 만들어 컨테이너에 등록한다.
4. 이후 컨테이너에 있는 BeanDefinition 객체를 토대로 객체를 생성한다. 팩토리메서드, 생성자주입, No-args 생성자 등 다양한 방법을 사용할 수 있다.
5. 생성이 끝났다면 리플렉션을 사용해서 필드 또는 메서드에 DI를 주입한다.
의 과정으로 이루어진다고 설명할 수 있겠다.
'CS > 백엔드' 카테고리의 다른 글
| [백엔드] 인증요청 시의 SecurityFilterChain의 동작과정 (0) | 2026.02.18 |
|---|---|
| [백엔드] 로그인 시의 SecurityFilterChain의 동작과정 (0) | 2026.02.16 |
| [백엔드] Spring Security 첫 걸음 - DelegatingFilterProxy, FilterChainProxy로 Web Context Filter에 Spring Bean Filter 등록하기 (0) | 2026.02.14 |
| [백엔드] OAuth는 어떻게 진행되는가? (0) | 2026.02.06 |
| [BckEnd] 스프링없이 순수JDK로 간단한 개발해보기 (0) | 2026.01.04 |
