안녕하세요. 개발자 Stark입니다. 2025년 첫 글입니다. 모두 새해 복 많이 받으세요~
이번 포스팅은 정말 길게 준비하고 있던 Spring 트랜잭션 시리즈의 시작을 알리는 글입니다. 한 달간 천천히 시간이 날 때마다 디버깅을 하면서 내부 동작을 분석하고 정리만 진행하고 있었는데 신년이 되었으니 새로운 시작을 알리기 위해 제가 선택한 첫 시리즈는 바로 프레임워크 분석이며
"스프링은 어떻게 @Transactional을 사용한 클래스나 메서드를 빈으로 등록하면서 Proxy 객체로 만들까?"입니다.
이를 위해 가장 기초가 되는 스프링 컨텍스트의 빈 등록 과정부터 시작해서 실제 트랜잭션 인터셉터의 동작까지 이번 시리즈를 통해 모든 것을 상세히 분석하고 정리할 예정입니다. 이번 포스팅에서는 스프링의 복잡한 "빈 등록 과정"을 가능한 자세히 설명하려고 하다 보니 글이 굉장히 길고 복잡하게 느껴질 수 있으니 너그럽게 이해해 주시면서 천천히 읽어봐 주셨으면 좋겠습니다.
참고로 이 글은 “Spring 기본 개념”(빈, 빈 생명주기, ApplicationContext나 BeanFactory 등)에 대해서 이해하고 오신 분들이 읽기에 최적화되어 있는 글입니다. 혹여나 내용이 이해하기 힘들다면 아래의 내용을 한번 보시고 천천히 같이 디버깅해 보며 내용을 다시 읽어보시면 조금 더 이해하기 쉬울 것입니다.
자, 그럼 시작해 봅시다!
코드 출처
설명을 시작하기 전에 라이선스 및 출처를 표시하도록 하겠습니다. "지금부터 설명드릴 소스코드는 스프링 부트(오픈소스, Apache License 2.0)에서 가져온 코드이며, 학습 목적으로 주석을 추가했습니다."
스프링에서 Proxy는 컨테이너를 초기화하며 등록된다.
Proxy는 스프링 서버가 실행되면서 다음 과정을 거쳐 생성됩니다.
- AbstractApplicationContext 클래스에 선언된 refresh() 메서드가 실행되면서 스프링 컨테이너 초기화가 시작됩니다.
- 이 과정에서 invokeBeanFactoryPostProcessors()가 호출되어 빈 정의(Bean Definition)를 등록하고
- registerBeanPostProcessors()로 프록시 생성을 담당할 프로세서인 BeanPostProcessor를 등록하고
- 마지막으로 finishBeanFactoryInitialization()에서 실제 빈 인스턴스를 생성하며, 이때 프록시(AOP 등)도 함께 생성됩니다.
그러니 AbstractApplicationContext 클래스의 refresh() 메서드를 살펴보면 어떻게 proxy 객체가 등록되는지 알 수 있습니다.
- 아래의 refresh() 메서드는 스프링 컨테이너를 초기화시키는 메서드이며 일반적으로 스프링 부트 애플리케이션에서 서버 시작 시 단 한 번만 호출됩니다. 그러나 만약 개발자가 명시적으로 ApplicationContext를 닫고(close()) 다시 refresh()를 호출한다면, 여러 번 호출될 수도 있습니다. (실무에서는 보통 서버를 재기동하지, 중간에 컨텍스트를 재생성하지 않습니다.)
public abstract class AbstractApplicationContext
extends DefaultResourceLoader
implements ConfigurableApplicationContext {
// ... (수많은 필드)
public void refresh() throws BeansException, IllegalStateException {
this.startupShutdownLock.lock();
try {
this.startupShutdownThread = Thread.currentThread();
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
this.postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (Error | RuntimeException var12) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var12);
}
this.destroyBeans();
this.cancelRefresh(var12);
throw var12;
} finally {
contextRefresh.end();
}
} finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
}
// ...
}
위의 코드가 전체 refresh() 코드입니다. 코드는 정말 예쁘게 extract Method로 분리되어 있지만 그렇다 하더라도 코드가 전체적으로 길다 보니 흐름이 한눈에 들어오지는 않습니다. 그러니 이번 목차에서는 refresh 메서드가 "스프링 컨텍스트를 초기화하는 핵심 메서드" 라는 것만 이해하고 넘어갑시다.
스프링 컨테이너 초기화를 담당하는 refresh() 메서드 분석
표를 통해 refresh() 메서드의 호출 흐름을 정리해 두었습니다.
단계 | 메서드/설명 | 핵심 포인트 |
1. 준비 단계 | prepareRefresh() | 환경/속성 설정, 컨텍스트 준비 작업 |
obtainFreshBeanFactory() | 새로운 BeanFactory 생성 | |
2. BeanFactory 후처리 Hook | postProcessBeanFactory(beanFactory) | 하위 클래스가 BeanFactory를 커스터마이징할 수 있는 훅 제공 |
3. 빈 정의 후처리 | invokeBeanFactoryPostProcessors() | BeanDefinitionRegistryPostProcessor: 빈 정의 추가/수정, BeanFactoryPostProcessor: 등록된 빈 정의 속성 수정 |
4. 빈 생성 후처리기 등록 | registerBeanPostProcessors(beanFactory) | AOP 등 프록시 생성 담당 BeanPostProcessor 등록, 우선순위별 정렬 및 등록 |
5. 메시지 소스, 이벤트 초기화 | initMessageSource(), initApplicationEventMulticaster(), registerListeners() |
국제화 메시지, 이벤트 멀티캐스터, 전통적 ApplicationListener 등록 |
6. Non-Lazy 빈 생성 & 프록시 적용 | finishBeanFactoryInitialization(beanFactory) | preInstantiateSingletons() → 모든 비-지연 빈 인스턴스화, AOP 프록시 적용(필요 시) |
7. 최종 마무리 | finishRefresh() | 남은 작업 완료, 애플리케이션 이벤트 트리거, 라이프사이클 시작 |
1. 메서드가 실행되면 먼저 Lock 처리를 합니다. (컨텍스트 초기화 시 동시성 문제 방지를 위한 잠금 설정)
- 동시성 문제 방지: Lock을 사용하여 컨텍스트 초기화 중 동시성 문제를 방지합니다.
- 초기 준비 작업: prepareRefresh()는 환경 초기화 및 설정 작업을 수행하고, obtainFreshBeanFactory()는 새로운 BeanFactory를 생성합니다.
this.startupShutdownLock.lock();
try {
this.startupShutdownThread = Thread.currentThread();
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
this.prepareRefresh(); // 컨텍스트 초기화 준비
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // BeanFactory 생성
this.prepareBeanFactory(beanFactory); // BeanFactory 기본 설정
2. 초기 준비 작업이 완료되면 새로운 try문을 만들고 그 안에서 주요 초기화 작업을 이어서 진행합니다.
try {
postProcessBeanFactory(beanFactory); // BeanFactory 후처리
// 시작점 표시
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
invokeBeanFactoryPostProcessors(beanFactory); // BeanFactoryPostProcessor 실행
registerBeanPostProcessors(beanFactory); // BeanPostProcessor 등록
// 종료점 표시
beanPostProcess.end();
가장 먼저 호출되는 postProcessBeanFactory(beanFactory) 메서드
- BeanFactory의 기본 설정을 추가/수정하기 위한 훅(hook) 메서드입니다.
- 하위 클래스들이 이 메서드를 오버라이드하여 BeanFactory를 직접 커스터마이징 합니다.
- 예: GenericWebApplicationContext에서 웹 관련 스코프를 추가할 때 사용합니다.
다음으로 호출되는 invokeBeanFactoryPostProcessors(beanFactory) 메서드
- 빈 정의(Bean Definition)를 로딩하고 수정하는 중요한 단계입니다.
- 두 가지 프로세서를 우선순위(PriorityOrdered -> Ordered -> 일반)에 따라 실행합니다.
- BeanDefinitionRegistryPostProcessor
- 빈 정의 자체를 추가/수정/삭제 가능
- 예: ConfigurationClassPostProcessor가 @Configuration, @Component 클래스들을 빈으로 등록
- BeanFactoryPostProcessor
- 이미 로딩된 빈 정의의 속성을 수정
- 예: PropertySourcesPlaceholderConfigurer가 ${} 플레이스홀더 처리
- BeanDefinitionRegistryPostProcessor
마지막으로 호출되는 registerBeanPostProcessors(beanFactory) 메서드
- 빈의 생성과 초기화 과정에 관여할 BeanPostProcessor들을 등록합니다.
- 프록시 생성 등을 담당하는 프로세서도 이 단계에서 등록됩니다.
- 이것도 우선순위로 등록하게 됩니다.
- PriorityOrdered 구현체
- Ordered 구현체
- 일반 BeanPostProcessor
- MergedBeanDefinitionPostProcessor
이 단계들을 거치면서 빈 정의가 로딩되고, 빈 생성을 위한 프로세서들이 등록되어 이후 실제 빈 생성 시 필요한 작업(프록시 생성 등)을 수행할 수 있게 됩니다.
3. 메시지 소스 및 이벤트 처리 초기화를 진행합니다.
- 메시지 소스와 이벤트 멀티캐스터를 초기화하여 애플리케이션 이벤트 처리 준비를 합니다.
initMessageSource(); // 메시지 소스 초기화
initApplicationEventMulticaster(); // 이벤트 멀티캐스터 초기화
onRefresh(); // 특정 컨텍스트에 필요한 처리
registerListeners(); // 리스너 등록
initMessageSource() 메서드
- 메시지 국제화와 관련된 작업을 수행하는 MessageSource 빈을 초기화합니다. 기본적으로 ResourceBundleMessageSource를 사용하여 애플리케이션의 국제화 메시지를 처리합니다.
initApplicationEventMulticaster() 메서드
- 스프링 애플리케이션에서 이벤트를 전달해 주는 핵심 객체, 즉 ApplicationEventMulticaster를 초기화합니다. 이벤트가 발생했을 때, 이 멀티캐스터가 애플리케이션 내 등록된 리스너들에게 이벤트를 “브로드캐스트(전달)”합니다.
- 참고로 ApplicationEventMulticaster는 전통적 방식(인터페이스 기반) 리스너든, 애노테이션(@EventListener) 기반 리스너든 동일하게 사용됩니다. 애플리케이션에서 이벤트가 발생했을 때, 어떤 리스너든 결국에는 이 멀티캐스터를 통해 이벤트를 전달받게 됩니다.
onRefresh() 메서드
- 특정 애플리케이션 컨텍스트에 필요한 초기화를 추가로 수행할 수 있도록 확장 지점을 제공합니다. 서브클래스에서 재정의할 수 있습니다.
registerListeners() 메서드
- 스프링 컨텍스트에서 ApplicationListener 인터페이스를 직접 구현한 빈(또는 빈 이름)을 찾아내어, 이미 초기화된 ApplicationEventMulticaster에 등록합니다. 등록된 리스너는 멀티캐스터가 이벤트를 “뿌려줄” 때 호출되어 이벤트를 수신합니다.
- 정리하자면 이 메서드는 전통적 방식(인터페이스 구현)에 대한 리스너만 등록하며 최신 스프링에서 사용 중인 방식인 @EventListener 어노테이션 방식은 별도로 EventListenerMethodProcessor를 사용하여 빈 후처리기로 처리됩니다.
구분 | 전통적 방식 | 애노테이션(@EventListener) 방식 |
구현체 | ApplicationListener 인터페이스 직접 구현 | 메서드에 @EventListener 어노테이션 추가 |
등록 시점 | registerListeners()에서 자동 스캔 | EventListenerMethodProcessor(빈 후처리기)가 스캔 |
실제 등록되는 대상 | 구현체(빈) 자체 | “프록시 Listener” (스프링이 내부적으로 생성) |
대표 장점 | 작동 구조가 간단하고 직관적 | 코드량이 줄고, 메서드 단위로 이벤트 처리 가능 |
대표 단점 | 리스너가 여러 개인 경우 코드량 증가 | 내부 동작을 이해하기 위해 후처리기 로직을 알아야 함 |
4. 마지막으로 빈 초기화를 완료시킵니다.
- finishBeanFactoryInitialization() 메서드에서 비-지연 싱글톤 빈이 초기화되고, 프록시 적용이 이루어집니다.
finishBeanFactoryInitialization(beanFactory); // 모든 비-지연 초기화 싱글톤 빈 초기화
finishRefresh(); // 초기화 완료 작업
각 메서드를 설명하기 전에 Lazy Bean이 뭔지 이해하고 넘어갑시다.
- 스프링에서 Lazy Bean이란, “컨테이너 초기화 시점에 즉시 생성되지 않고, 실제로 빈이 필요해지는 시점(호출 시점)에 비로소 생성되는 빈”을 의미합니다. 반대로 Non-Lazy(비-지연) 빈은 스프링 컨테이너가 기동 될 때(초기화 단계) 미리 인스턴스를 만들어둡니다.
- 참고로 “모든 빈을 Lazy로 만들면 초기에 빨라 보이지만, 실제로 서비스가 동작하면서 한꺼번에 생길 수도 있어, 런타임 지연이 발생할 수 있으니 사용 시 주의가 필요합니다.”
package com.example.demo.config;
import lombok.Getter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration
public class MyConfig {
// @Lazy 애노테이션을 통해 지연 로딩 설정
@Bean
@Lazy
public HeavyService heavyService() {
return new HeavyService();
}
public static class HeavyService {
@Getter
private final String name = "Heavy Service";
// setter를 대신해 필요한 메서드만 따로 제공
public void changeName(String newName) {
// ...
}
}
}
- @Lazy를 붙인 heavyService 빈은 컨텍스트 초기화 시점에는 인스턴스화되지 않고,
- 누군가 이 빈을 실제로 주입하거나 호출할 때 인스턴스가 생성됩니다.
Lazy Bean의 동작 방식은 다음과 같습니다.
- 스프링 컨테이너 기동 시
- Lazy Bean으로 등록된 빈은 BeanDefinition 정보만 기억해 두고, 실제 인스턴스는 만들지 않습니다.
- Lazy Bean으로 등록된 빈은 BeanDefinition 정보만 기억해 두고, 실제 인스턴스는 만들지 않습니다.
- 의존성 주입 혹은 직접 getBean() 호출 시
- 컨테이너는 “아, 이 빈은 Lazy였지!”를 확인하고, 그제야 인스턴스를 생성하여 반환합니다.
- 컨테이너는 “아, 이 빈은 Lazy였지!”를 확인하고, 그제야 인스턴스를 생성하여 반환합니다.
- 미사용 시 끝까지 미생성
- 애플리케이션 내에서 한 번도 호출되지 않는다면 아예 생성되지 않은 채로 종료될 수 있습니다.
- 애플리케이션 내에서 한 번도 호출되지 않는다면 아예 생성되지 않은 채로 종료될 수 있습니다.
그럼 우리가 일반적으로 등록하는 빈은 non-lazy-bean인가?
- 기본적으로는 대부분 @Component로 등록한 빈들은 Non-Lazy입니다. 스프링에서 별도 설정(@Lazy 애노테이션, spring.main.lazy-initialization=true 등)을 하지 않으면, 컨테이너 기동 시점에 미리 빈을 인스턴스화하는 Eager Initialization(=Non-Lazy) 방식을 사용하기 때문입니다.
- Non-Lazy(기본)
- @Component, @Configuration, @Bean 등으로 등록되면 기본적으로 즉시 생성
- 스프링 부트 기동 시점에 빈이 전부 만들어져서, 앱 로딩 속도에 영향을 미칠 수 있음
- Lazy
- @Lazy 또는 spring.main.lazy-initialization=true 설정을 통해 필요한 시점에만 생성
- 초기 부하를 줄일 수 있지만, 런타임에 생성 지연이 발생하므로 상황에 따라 주의가 필요
결국 대부분의 빈은 특별한 이유가 없으면 Non-Lazy(즉시 생성)로 두고, 정말 늦춰야 할 필요가 있는 빈만 Lazy로 선언하는 것이 일반적입니다. 이제 Lazy Bean에 대해서 이해했으니 호출되는 메서드를 분석해 봅시다.
먼저 호출되는 finishBeanFactoryInitialization(beanFactory) 메서드
- 모든 비-지연 싱글톤 빈(non-lazy singleton beans)을 초기화합니다.
- lazy-init으로 설정된 빈은 여기서 생성되지 않고, 실제로 빈이 필요해지는 시점에 생성됩니다.
- AOP 프록시 적용 등 실제 빈 인스턴스화가 이 단계에서 이뤄집니다.
- 이는 preInstantiateSingletons()를 호출하여 빈 정의를 기반으로 실제 빈 인스턴스를 생성하고, 필요한 경우 AOP 프록시를 적용하는 작업을 포함합니다.
다음으로 호출되는 finishRefresh() 메서드
- 최종적으로 리프레시 과정에서 남은 작업을 완료합니다. 애플리케이션 이벤트를 트리거하거나, 라이프사이클 인터페이스(Lifecycle 구현체)를 시작하는 등의 작업을 수행합니다.
5. 만약 refresh() 로직 처리 중 예외가 발생한다면 여기서 처리됩니다.
catch (Error | RuntimeException var12) {
this.destroyBeans(); // 빈 정리
this.cancelRefresh(var12); // 리프레시 취소
throw var12;
}
6. 메서드 최종 정리 작업 (편하게 설명하기 위해 2개의 finally를 합쳤습니다.)
} finally {
contextRefresh.end();
}
} finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
이렇게 refresh() 메서드가 종료됩니다.
refresh() 메서드 내부의 제일 중요한 3가지 메서드를 정리해 보았습니다.
1. invokeBeanFactoryPostProcessors() 메서드
- 이 메서드에서 “빈 정의”를 최종 조정 (ex: BeanDefinitionRegistryPostProcessor)합니다. 여기서 빈 정의가 마무리되어야 만 “AOP를 적용할지 말지”를 결정하는 BeanPostProcessor(등록 시점은 다음 단계)가 올바른 정보를 확인할 수 있습니다.
2. registerBeanPostProcessors() 메서드
- BeanPostProcessor 인터페이스를 구현한 클래스(예: AnnotationAwareAspectJAutoProxyCreator)들을 우선순위대로 등록합니다. 등록된 BeanPostProcessor들은 빈 생성 전후에 개입해서 다양한 작업(예: AOP 프록시 생성, 공통 로깅 처리 등)을 수행할 수 있게 됩니다. 따라서 이 단계에서 AOP 관련 프로세서가 등록되어야만 실제 빈이 생성될 때 해당 프로세서가 빈을 프록시로 감싸줄 수 있습니다.
3. finishBeanFactoryInitialization() 메서드
- 내부적으로 preInstantiateSingletons() 메서드를 호출하여 비-지연 싱글톤 빈(Non-Lazy Singleton Beans)들을 모두 인스턴스화합니다. 이미 등록된 BeanPostProcessor(예: AOP 관련)들이 빈 생성 과정에 개입하여 필요한 빈을 프록시로 감싸는 작업을 수행합니다. 이 단계에서 실제로 “빈 생성” → “프록시 적용”이 일어나므로, 스프링이 제공하는 AOP가 비로소 구체적인 클래스(빈) 위에 덧씌워집니다. 결과적으로, 우리가 주입받는 빈 중 일부는 “실제 구현체” 대신 “프록시 객체”가 됩니다.
지금까지는 refresh 메서드의 전체적인 흐름을 파악해 보았습니다. 이제부터는 위에서 설명드린 제일 중요한 3가지 메서드가 각각 어떻게 구성되어서 실행되고 있는지를 자세히 분석해 봅시다.
먼저 invokeBeanFactoryPostProcessors 메서드를 살펴봅시다.
시작하기에 앞서 Processor가 무엇인지 이해해 봅시다.
- Processor는 스프링의 빈 생명주기에서 특정 시점에 개입하여 처리를 수행하는 객체입니다.
- 즉, 빈을 수정하거나 새로운 빈을 등록하는 등의 작업을 담당합니다.
- Processor는 크게 두 종류가 있습니다.
// 1. 빈 정의 자체를 수정/추가/삭제할 수 있는 프로세서
// - 예: @Configuration, @Component 등의 클래스들을 빈으로 등록
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry);
}
// 2. 이미 등록된 빈 정의의 속성을 수정하는 프로세서
// - 예: ${} 플레이스홀더를 실제 값으로 변환
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory);
}
지금부터 분석할 invokeBeanFactoryPostProcessors 메서드는 이름을 통해 분석을 해보면 invoke(호출하다) + BeanFactory(빈 팩토리의) + PostProcessors(포스트 프로세서를) "즉, 빈 팩토리의 포스트 프로세서를 호출하여 빈 정의를 등록하거나 수정하는 작업을 수행한다." 이렇게 이해할 수 있습니다. 이제 실제 메서드 내부를 분석해 봅시다.
invokeBeanFactoryPostProcessors 메서드는 어떻게 동작할까요?
- 메서드 내부에서는 PostProcessorRegistrationDelegate라는 작업을 위임받아서 처리하는 클래스에 선언되어 있는 동일한 이름으로 선언된 invokeBeanFactoryPostProcessors 메서드를 호출합니다. 클래스명에 Delegate(위임)이 적혀있다는 것을 확인할 수 있습니다.
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, this.getBeanFactoryPostProcessors());
if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean("loadTimeWeaver")) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}
호출되는 PostProcessorRegistrationDelegate 클래스의 invokeBeanFactoryPostProcessors 메서드를 살펴봅시다. 이 메서드는 크게 두 가지 작업을 합니다.
- BeanDefinitionRegistryPostProcessor 실행: 새로운 빈 정의 등록
- BeanFactoryPostProcessor 실행: 빈 정의 속성 수정
/**
* [주석 요약]
*
* 스프링에서 BeanFactoryPostProcessor와 BeanDefinitionRegistryPostProcessor를 수행하는 로직.
* - 먼저 BeanDefinitionRegistryPostProcessor의 postProcessBeanDefinitionRegistry()를 호출하여
* BeanDefinition(빈 메타 정보)을 수정하거나 추가/삭제 등을 진행할 기회를 준다.
* - 이후 BeanFactoryPostProcessor의 postProcessBeanFactory()를 호출하여, BeanFactory를 커스터마이징할 수 있게 한다.
* - PriorityOrdered, Ordered 등의 인터페이스 구현 여부를 기준으로 실행 순서를 정렬하고,
* 중간에 새로 등록된 BeanDefinitionRegistryPostProcessor가 있다면 추가로 다시 처리한다(while 루프로 재반복).
*/
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory,
List<BeanFactoryPostProcessor> beanFactoryPostProcessors)
{
// 이미 처리된 Bean 이름을 담아둘 Set
Set<String> processedBeans = new HashSet<>();
// 만약 BeanFactory가 BeanDefinitionRegistry를 구현하고 있다면...
if (beanFactory instanceof BeanDefinitionRegistry registry) {
// BeanFactoryPostProcessor 중에서 일반적인 BeanFactoryPostProcessor 목록과
// BeanDefinitionRegistryPostProcessor 목록을 분리
List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();
// 1) 먼저 외부에서 전달된 beanFactoryPostProcessors를 순회하면서
// BeanDefinitionRegistryPostProcessor와 일반 BeanFactoryPostProcessor를 분류한다.
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor registryProcessor) {
// BeanDefinitionRegistryPostProcessor는 우선 registry에 대해 후처리를 수행
registryProcessor.postProcessBeanDefinitionRegistry(registry);
// 처리한 registryProcessor를 등록
registryProcessors.add(registryProcessor);
} else {
// 나머지는 regularPostProcessors에 추가
regularPostProcessors.add(postProcessor);
}
}
// 2) BeanDefinitionRegistryPostProcessor 중 PriorityOrdered를 구현한 애들만 먼저 찾는다
// 그리고 우선순위에 따라 정렬 후 실행
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();
String[] postProcessorNames = beanFactory.getBeanNamesForType(
BeanDefinitionRegistryPostProcessor.class, true, false);
// BeanDefinitionRegistryPostProcessor이면서 PriorityOrdered를 구현한 것들만 골라낸다.
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
// Bean을 가져와서 currentRegistryProcessors에 넣고, processedBeans에 이름을 추가
currentRegistryProcessors.add(
(BeanDefinitionRegistryPostProcessor)beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
// PriorityOrdered 구현체들을 순서대로 정렬한 다음, BeanDefinitionRegistry 후처리 실행
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(
currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
// 3) 이번엔 BeanDefinitionRegistryPostProcessor 중 Ordered를 구현한 애들만 처리
postProcessorNames = beanFactory.getBeanNamesForType(
BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(
(BeanDefinitionRegistryPostProcessor)beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
// Ordered 구현체들도 순서대로 정렬 후 후처리 실행
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(
currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
// 4) 나머지 BeanDefinitionRegistryPostProcessor(즉 PriorityOrdered, Ordered도 아니고
// 아직 처리되지 않은 것들)를 모두 처리할 때까지 반복
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(
BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(
(BeanDefinitionRegistryPostProcessor)beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
// 정렬 후 후처리 실행
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(
currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
}
// 이제 BeanDefinitionRegistryPostProcessor들이 모두 postProcessBeanDefinitionRegistry()를 마쳤으므로
// 각 BeanDefinitionRegistryPostProcessor에 대해 postProcessBeanFactory()를 호출
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
// 또, 일반 BeanFactoryPostProcessor들도 postProcessBeanFactory() 수행
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
} else {
// 만약 BeanDefinitionRegistry가 아니라면, 그냥 전달받은 BeanFactoryPostProcessor들만 수행
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}
// 5) 지금까지 처리되지 않은 BeanFactoryPostProcessor(BeanDefinitionRegistryPostProcessor가 아닌 애들)에 대해서
// PriorityOrdered, Ordered 순, 그다음 순서 없는 것 순으로 실행
String[] postProcessorNames = beanFactory.getBeanNamesForType(
BeanFactoryPostProcessor.class, true, false);
// PriorityOrdered를 구현한 BeanFactoryPostProcessor들
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
// Ordered만 구현한 BeanFactoryPostProcessor 이름
List<String> orderedPostProcessorNames = new ArrayList<>();
// 우선순위 지정 안 된 BeanFactoryPostProcessor 이름
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
// 만약 이미 처리된 적 있다면 건너뛰고
if (!processedBeans.contains(ppName)) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(
beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}
}
// 5-1) PriorityOrdered 구현체 먼저 수행
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);
// 5-2) 그 다음 Ordered 구현체
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(
beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);
// 5-3) 그 다음 순서가 지정되지 않은(PostProcessor 인터페이스만 구현) 구현체
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(
beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
// 순서 지정 없는 것들은 그냥 호출
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);
// 6) 마지막으로 BeanFactory의 메타데이터 캐시를 비워줌
beanFactory.clearMetadataCache();
}
이 메서드에서는 크게 두 종류의 프로세서를 실행합니다.
- 지금부터 드리는 설명을 통해 "왜 등록 단계와 수정 단계가 분리되어야 할까?"라는 의문점을 해소하실 수 있을 것입니다.
1. BeanDefinitionRegistryPostProcessor
- "빈을 등록하는 프로세서"입니다.
- 예: ConfigurationClassPostProcessor가 @Configuration 클래스를 찾아서 빈으로 등록합니다.
- 이 프로세서는 반드시 먼저 실행되어야 합니다. (빈이 있어야 그 다음 작업이 가능하므로)
2. BeanFactoryPostProcessor
- "빈의 설정을 수정하는 프로세서"입니다.
- 예: PropertySourcesPlaceholderConfigurer가 ${} 값을 실제 값으로 교체
- 이 프로세서는 빈이 등록된 후에 실행되어야 합니다.
3. 이렇게 프로세서 호출 순서가 매우 중요하기 때문에, 스프링은 아래와 같은 우선순위 체계를 만들어서 프로세서를 실행합니다.
- PriorityOrdered: “가장 먼저 실행해야 할 우선순위가 있는 후처리기”
- Ordered: “순서가 필요하지만, 절대적으로 가장 먼저는 아님”
- 일반(나머지): “우선순위가 지정되지 않은 일반 후처리기”
이 로직은 빈 등록 과정뿐 아니라 스프링 내부에서 우선순위를 지정하는 여러 장소(인터셉터, 필터, 등)에서도 비슷하게 적용되므로, “스프링의 일관된 설계”입니다. 이것은 마치 식당에서 주문-조리-서빙처럼, 특정 작업은 반드시 다른 작업보다 먼저 실행되어야 하기 때문입니다.
4. 또한 로직에는 재반복 처리(while 루프)가 존재합니다.
- 재반복의 이유를 살펴보면 스프링이 새로운 빈을 등록하기 위해 BeanDefinitionRegistryPostProcessor를 실행하는 도중에도 새로운 BeanDefinitionRegistryPostProcessor가 등록될 수 있습니다. 이 경우, 한 번의 처리로 끝나지 않고, 새로 추가된 후처리기들도 다시 실행해 줘야만 모든 후처리가 누락 없이 이루어집니다. 예를 들어, ConfigurationClassPostProcessor가 새로운 @Configuration 클래스를 인식하고 내부적으로 또 다른 후처리기를 등록하는 상황이 발생할 수 있습니다.
boolean reiterate = true;
while (reiterate) {
reiterate = false;
// 새로 추가된 BeanDefinitionRegistryPostProcessor 재검색 ...
// 검색된 후처리기들 실행 ...
// 다시 추가된 것이 있으면 reiterate = true ...
}
- 위와 같은 방식으로 “더 이상 새롭게 추가되는 후처리기가 없을 때까지” 재실행을 반복합니다.
이를 통해, 스프링이 "우선순위가 가장 높은 후처리기부터, 등록되었거나 새로 추가된 후처리기들을 모두 실행" 하도록 설계되어 있음을 알 수 있습니다.
마지막으로 위의 메서드가(빈 정의를 등록하거나 수정하는 작업) 끝난 다음 아래의 로직이 호출됩니다.
if (!NativeDetector.inNativeImage()
&& beanFactory.getTempClassLoader() == null
&& beanFactory.containsBean("loadTimeWeaver")) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
NativeDetector.inNativeImage() 메서드
- 그레일 VM(GraalVM) 기반의 네이티브 이미지를 빌드할 때, 특정 기능(리플렉션, 로드 타임 위빙 등)에 제약이 있어 생기는 분기입니다. “네이티브 환경에서 굳이 수행할 필요가 없는 작업은 제외한다”라는 의도가 담겨 있습니다.
LoadTimeWeaverAwareProcessor 설명
- 스프링에서 로드 타임 위빙(Load Time Weaving, LTW)을 지원해 주는 프로세서입니다. 보통 AspectJ를 사용하거나, 클래스 로딩 시점에 바이트코드를 바꿔야 하는 경우 사용합니다. 이 프로세서를 등록해 두고, 만약 loadTimeWeaver 빈이 존재한다면 해당 기능을 자동으로 활성화합니다.
위와 같은 코드는 대부분의 일반적인 웹 애플리케이션 개발에서 자주 보이진 않지만, Spring이 “AOP나 로드 타임 위빙을 기본적으로 지원하기 위해” 이런 체크 로직을 넣어뒀다는 정도로 이해해 주시면 됩니다.
정리하자면 다음과 같이 동작합니다.
[invokeBeanFactoryPostProcessors]
-> [PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors]
-> [BeanDefinitionRegistryPostProcessor(우선순위별 실행)]
-> [BeanFactoryPostProcessor(우선순위별 실행)]
다음으로 registerBeanPostProcessors 메서드를 살펴봅시다.
다음 단계입니다. invokeBeanFactoryPostProcessors 다음에 호출되는 registerBeanPostProcessors 메서드는 "빈 생성 전후에 개입해서 다양한 작업(예: AOP 프록시 생성, 공통 로깅 처리 등)"을 할 수 있는 Processor들을 등록합니다.
- invokeBeanFactoryPostProcessors로 빈 정의를 등록했으니 그 다음으로 registerBeanPostProcessors로 프록시 생성 및 다양한 작업을 담당할 BeanPostProcessor를 등록하는 과정입니다.
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}
- 이번에도 위에서와 동일하게 PostProcessorRegistrationDelegate 클래스에 메서드를 선언해두고 위임받은 Delegate클래스 내부의 registerBeanPostProcessors 메서드가 동작하며 AOP 관련 프로세서를 등록합니다. 이번에도 주석을 통해 흐름을 파악하실 수 있도록 표시해두었습니다.
/**
* 스프링 컨테이너에 등록된 BeanPostProcessor들을 우선순위에 따라 찾아서,
* 순서대로 BeanFactory에 등록해주는 메서드.
*/
public static void registerBeanPostProcessors(
ConfigurableListableBeanFactory beanFactory,
AbstractApplicationContext applicationContext) {
// 1. BeanPostProcessor 타입의 모든 Bean 이름을 조회
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);
// 2. 이미 BeanFactory에 등록된 BeanPostProcessor(기본값들) + 새로 발견된 BeanPostProcessor들의 총 개수 계산
int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
// 3. BeanPostProcessorChecker를 추가하여,
// 아직 모든 BeanPostProcessor가 등록되기 전에 Bean이 생성될 경우 경고 메시지를 띄울 수 있도록 함
beanFactory.addBeanPostProcessor(
new BeanPostProcessorChecker(beanFactory, postProcessorNames, beanProcessorTargetCount)
);
// 각 우선순위별로 BeanPostProcessor를 분류할 컬렉션
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
// 4. 조회된 BeanPostProcessor 이름들을 순회하며,
// PriorityOrdered, Ordered, 그 외(순서 지정 없음)로 분류
// 또한 MergedBeanDefinitionPostProcessor라면 internalPostProcessors에 별도 보관
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
priorityOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}
// 5. PriorityOrdered를 구현한 BeanPostProcessor부터 등록(우선순위가 가장 높음)
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);
// 6. Ordered를 구현한 BeanPostProcessor 등록
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String ppName : orderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(orderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, orderedPostProcessors);
// 7. 나머지(우선순위 미지정) BeanPostProcessor들 등록
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);
// 8. 내부에서 사용되는( MergedBeanDefinitionPostProcessor ) BeanPostProcessor들을 마지막으로 정렬 후 등록
sortPostProcessors(internalPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, internalPostProcessors);
// 9. ApplicationListenerDetector 추가
// (ApplicationListener 인터페이스 구현체를 Bean 생성 시점에 감지해주는 역할)
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
}
BeanPostProcessor 조회
- getBeanNamesForType(BeanPostProcessor.class, true, false)로 BeanPostProcessor를 전부 스캔합니다.
BeanPostProcessorChecker 등록
- 모든 BPP(빈 포스트 프로세서)가 다 등록되기 전에, Bean이 일찍 생성되어 버리면 로그/경고로 알려주도록 하는 체크용 BPP입니다.
우선순위(PriorityOrdered, Ordered) 분류
- PriorityOrdered → Ordered → 우선순위 미지정 순으로 구분합니다. (위에서의 설명과 동일합니다.)
- 후에 순서를 맞춰 등록합니다.
MergedBeanDefinitionPostProcessor는 internalPostProcessors로 별도 보관
- 다른 BPP보다 늦게 등록되어야 하는 내부용 후처리기
등록 순서대로 BeanPostProcessor 등록
- PriorityOrdered 구현체 → Ordered 구현체 → 순서 없는 BPP
- 그 뒤 internalPostProcessors를 등록합니다.
ApplicationListenerDetector 추가
- Bean이 등록될 때마다, 해당 Bean이 ApplicationListener 구현체인지 확인해서, 스프링 이벤트 체계에 자동으로 연결해주는 역할을 담당합니다.
참고사항
위에서 말한 "AOP 관련 프로세서”라는 말은, 사실 일반적으로 BeanPostProcessor 중 AOP 처리를 담당하는 AutoProxyCreator(예: AnnotationAwareAspectJAutoProxyCreator)가 이 로직 속에서 함께 등록·적용될 수 있기 때문입니다. 따라서 “AOP 관련 등록도 여기에 포함될 수 있다.” 정도로 이해하시면 좋습니다.
즉, 이 메서드는 스프링 컨테이너가
- 어떤 순서로 BeanPostProcessor를 등록해야 하고,
- 그중에서 내부적으로 쓰는 특별한 후처리기는 언제 등록해야 하며,
- 최종적으로 Event Listener까지 연결 감지 장치를 달아야 하는지를 한 번에 처리하는 핵심 로직입니다.
마지막으로 finishBeanFactoryInitialization를 살펴봅시다.
마지막 단계입니다. 최종적으로 finishBeanFactoryInitialization 메서드가 호출되어 비-지연 싱글톤 빈(Non-Lazy Singleton Beans)들을 모두 인스턴스화하고 이때 드디어 위에서 등록시킨 BeanPostProcessor(예: AOP 관련)들이 빈 생성 과정에 개입하여 필요한 빈을 프록시로 감싸는 작업을 수행합니다. 즉, 여기서 프록시 객체가 생성됩니다.
/**
* 스프링 컨테이너 초기화 마지막 단계:
* 1) bootstrapExecutor / conversionService 등을 BeanFactory에 연결
* 2) EmbeddedValueResolver(환경 변수 치환) 등록
* 3) BeanFactoryInitializer 실행
* 4) LoadTimeWeaverAware 빈 초기화 (클래스 로더를 통한 로드타임 위빙 관련)
* 5) 임시 클래스 로더 제거
* 6) BeanFactory 설정 '동결'(더 이상 변경 불가)
* 7) preInstantiateSingletons()를 통해 모든 non-lazy 싱글톤 빈들을 즉시 생성/초기화
*/
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// 1. 만약 'bootstrapExecutor' Bean이 존재하고 타입이 Executor라면,
// BeanFactory에 bootstrapExecutor 설정을 등록 (비동기 실행 등 필요 시 사용)
if (beanFactory.containsBean("bootstrapExecutor")
&& beanFactory.isTypeMatch("bootstrapExecutor", Executor.class)) {
beanFactory.setBootstrapExecutor(
(Executor) beanFactory.getBean("bootstrapExecutor", Executor.class)
);
}
// 2. 'conversionService' Bean이 존재하고, 타입이 ConversionService라면
// 빈 팩토리의 ConversionService로 설정
// (스프링 내부에서 타입 변환 등에 사용)
if (beanFactory.containsBean("conversionService")
&& beanFactory.isTypeMatch("conversionService", ConversionService.class)) {
beanFactory.setConversionService(
(ConversionService) beanFactory.getBean("conversionService", ConversionService.class)
);
}
// 3. EmbeddedValueResolver가 아직 등록되지 않았다면 추가
// (환경변수나 placeholder를 문자열에서 찾아 치환해주는 역할)
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(
strVal -> this.getEnvironment().resolvePlaceholders(strVal)
);
}
// 4. BeanFactoryInitializer 타입의 빈들을 모두 찾고, 순회하며 초기화
// (BeanFactory 자체를 커스터마이징하는 초기화 로직 실행 가능)
String[] initializerNames = beanFactory.getBeanNamesForType(BeanFactoryInitializer.class, false, false);
for (String initializerName : initializerNames) {
BeanFactoryInitializer initializer =
beanFactory.getBean(initializerName, BeanFactoryInitializer.class);
initializer.initialize(beanFactory);
}
// 5. LoadTimeWeaverAware 빈들을 찾고, 실제로 getBean()을 호출하여 초기화
// (로드타임 위빙을 사용하여 AOP 로직을 적용할 경우 사용)
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
try {
beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class);
} catch (BeanNotOfRequiredTypeException ex) {
// 만약 타입 불일치가 발생하면 디버그 로그
if (this.logger.isDebugEnabled()) {
this.logger.debug("Failed to initialize LoadTimeWeaverAware bean '"
+ weaverAwareName + "' due to unexpected type mismatch: " + ex.getMessage());
}
}
}
// 6. 임시 ClassLoader 제거 (사용하지 않음으로 null 설정)
beanFactory.setTempClassLoader(null);
// 7. BeanFactory 설정을 동결: 더 이상 BeanDefinition 등을 수정할 수 없게 설정
beanFactory.freezeConfiguration();
// 8. 모든 non-lazy 싱글톤 빈을 즉시 인스턴스화 및 초기화
// (실제 객체가 생성되어 각종 의존관계가 주입됨)
beanFactory.preInstantiateSingletons();
}
bootstrapExecutor / conversionService:
- 빈 팩토리에 등록된 특정 Executor(스레드 풀 등)나 ConversionService를 BeanFactory의 기본 설정으로 연결
EmbeddedValueResolver 등록
- ${}와 같은 형식으로 선언된 플레이스홀더를 실제 Environment 값으로 치환해주는 기능
BeanFactoryInitializer 초기화
- BeanFactoryInitializer들은 BeanFactory 자체를 커스터마이징하는 로직을 담고 있을 수 있음
LoadTimeWeaverAware 초기화
- 로드타임 위빙(AOP) 관련 처리를 위해, 필요한 빈들을 미리 초기화
임시 ClassLoader 제거
- 초기 작업 시 잠시 사용할 수 있는 ClassLoader를 더 이상 사용하지 않음
BeanFactory 설정 동결
- 이후에는 BeanDefinition 등 환경 구성이 변경되지 못하도록 고정
모든 싱글톤 빈 생성(preInstantiateSingletons)
- (lazy-init이 아닌) 싱글톤 빈들을 실제로 한 번에 다 생성해둬서, 런타임 시점에 지연 없이 바로 사용할 수 있게 함
이 과정을 통해 스프링 컨테이너는 최종적인 설정을 마무리 짓고, 빈 초기화를 마친 뒤, 애플리케이션 실행 준비 상태가 됩니다.
스프링 컨텍스트 초기화 과정 요약
1. 초기화 시작 (refresh() 메서드 호출)
- 스프링 컨테이너 준비 작업(락 처리, BeanFactory 생성 등)을 진행합니다.
- 서버 실행 시 단 한 번 호출됩니다.
2. 빈 정의 로딩 (invokeBeanFactoryPostProcessors)
- BeanFactoryPostProcessor / BeanDefinitionRegistryPostProcessor 등을 통해 빈 정의(BeanDefinition)를 추가, 수정, 삭제할 기회를 제공합니다.
- 여기서 AOP 관련 AspectJAutoProxyRegistrar 같은 후처리기도 이 단계에서 등록될 수 있습니다.
- 참고로 BeanDefinitionRegistryPostProcessor는 postProcessBeanDefinitionRegistry() 메서드를 통해 BeanDefinition 자체를 직접 수정·추가·삭제할 수 있습니다. 반면, 일반적인 BeanFactoryPostProcessor는 이미 등록된 빈 정의가 로딩된 뒤, BeanFactory 레벨에서 추가 후처리를 수행합니다.
3. 프로세서 등록 (registerBeanPostProcessors)
- BeanPostProcessor들을 우선순위(PriorityOrdered, Ordered)대로 스캔 및 등록합니다.
- 이 단계에서 등록된 BeanPostProcessor들은, 이후 실제 빈 생성 시점(finishBeanFactoryInitialization 메서드)에서 postProcessBeforeInitialization, postProcessAfterInitialization 등의 메서드를 통해 빈에 대한 후처리를 수행합니다.
- AOP 관련 자동 프록시 생성기(AnnotationAwareAspectJAutoProxyCreator 등)도 여기서 등록 가능합니다.
- 특히 AOP 프록시를 만드는 AutoProxyCreator 계열 BeanPostProcessor가 여기서 등록되면, 뒤에서 빈이 만들어질 때 자동으로 프록시를 씌우게 됩니다. (예를들어 AbstractAutoProxyCreator 클래스가 있습니다.)
4. 빈 초기화 (finishBeanFactoryInitialization)
- conversionService, bootstrapExecutor 설정, EmbeddedValueResolver 등록 등 마무리 작업을 진행합니다.
- preInstantiateSingletons() 메서드 호출로 모든 (non-lazy 싱글톤 빈)들을 한꺼번에 실제 생성 & 초기화
- 이때 (lazy-init)으로 설정된 빈들은 바로 생성되지 않고, 실제로 해당 빈이 필요할 때가 되어서야 초기화가 진행됩니다.
- AOP 프록시도 이 시점에 실제로 적용되어, 스프링 빈들이 프록시로 감싸질 수 있습니다.
즉, invokeBeanFactoryPostProcessors 단계에서 빈 정의를 최종 조정하고, registerBeanPostProcessors에서 AOP 등 다양한 BeanPostProcessor를 등록해 둔 뒤, finishBeanFactoryInitialization 시점에서 non-lazy 빈들을 만들면서 “프록시 적용”까지 마무리되는 구조입니다.
결과적으로, 이 일련의 과정을 통해 스프링은 빈을 정의 -> 후처리 -> 등록 -> 실제 인스턴스화 -> 프록시 적용까지 마친 뒤, 완전히 초기화된 애플리케이션 컨텍스트를 준비하게 됩니다.
아스키 아트로 본 전체 흐름
+------------------------------------------------+
| 1) refresh() |
| - Lock 처리 |
| - BeanFactory 생성 |
+-------------------+----------------------------+
|
v
+------------------------------------------------+
| 2) invokeBeanFactoryPostProcessors |
| - 빈 정의 추가 및 수정 |
| - BeanFactoryPostProcessor / RegistryPost.. |
+-------------------+----------------------------+
|
v
+------------------------------------------------+
| 3) registerBeanPostProcessors |
| - BeanPostProcessor 등록 (AOP 등) |
| - 우선순위(PriorityOrdered, Ordered) 반영 |
+-------------------+----------------------------+
|
v
+------------------------------------------------+
| 4) finishBeanFactoryInitialization |
| - conversionService 등 최종 설정 |
| - preInstantiateSingletons()로 |
| non-lazy 싱글톤 빈 생성·초기화 |
| - AOP 프록시 적용 |
+-------------------+----------------------------+
|
v
+------------------------------------------------+
| **스프링 컨텍스트 초기화 완료** |
| 이제 요청 처리나 의존성 주입 등이 모두 가능. |
+------------------------------------------------+
Tip
lazy-init 된 빈은 finishBeanFactoryInitialization에서 초기화되지 않고, 실제로 빈이 필요할 때까지 생성이 지연됩니다.
AOP 관련 BeanPostProcessor는 빈이 생성될 때 자동으로 프록시를 덧씌워, 메서드 호출에 부가로직을 넣을 수 있습니다.
이처럼 전체 흐름을 살펴보면 스프링이 어떻게 내부적으로 빈 정의를 다루고, 후처리기를 통해 기능을 확장하며, 최종 빈을 만들고 AOP를 적용하는지 한눈에 이해하실 수 있습니다.
마무리하며
새해 첫 포스팅이자 제가 계속 기획만 하고 있던 시리즈였기에 어떻게 해야 이해하기 쉽게 내용을 잘 작성할 수 있을지에 대해서 정말 많이 고민했습니다. 그리고 그 생각하는 과정을 통해 스프링에 대한 제 생각이 확고하게 정리되었습니다. 지금부터 작성할 내용은 제 개인적인 생각을 조금 정리해 본 것이니 읽지 않으셔도 됩니다. 그러니 먼저 인사드리도록 하겠습니다.
긴 글 읽어주셔서 감사합니다 :) 새해에도 모두 즐거운 개발 합시다!
자, 그럼 제 생각을 정리해 보겠습니다. 스프링 프레임워크는 자바, 코틀린 개발자가 보다 빠르고 편하게 서버를 개발할 수 있게 도와주는 프레임워크입니다. 생각해 보면 이게 없었으면 어떻게 개발했을까 싶을 정도입니다. 제가 스프링을 사용한 지는 4년 정도 되었습니다. 저는 아직 주니어 개발자입니다 ㅎㅎㅎ 그럼에도 4년이란 시간 동안 사용하며 정리된 생각은 다음과 같습니다.
제가 생각하는 스프링 프레임워크는 잘 만들어진 붕어빵틀입니다. 붕어빵은 틀 안에 슈크림, 커스터드, 초콜릿 같이 여러 다양한 재료들을 커스텀하게 넣어서 만들 수 있습니다. 즉, 재료만 있다면 사용자의 needs에 맞는 붕어빵을 탄생시킬 수 있는 것입니다. 이와 동일하게 우리 백엔드 개발자들도 스프링이라는 프레임워크에 멋진 재료(비즈니스, 코드 구성 방식, 아키텍처)들을 넣으며 멋지게 커스텀하면서 서버 개발을 하고 있습니다.
이렇게 편안하게 붕어빵틀(프레임워크)을 사용하며 개발하다 보니 어느 순간부터 저는 붕어빵틀을 사용하는 편안함에 절여져서 붕어빵을 만들기 위해 어떤 재료를 넣을지에 대해서만 꾸준히 고민했지 붕어빵틀이 어떻게 만들어졌는지에 대해서는 알려고 하지 않았습니다. 특히 일반적인 IT회사는 붕어빵틀을 만드는 곳이 아닐 것입니다. 그렇기에 더더욱 비즈니스라는 맛난 재료를 부어서 맛있는 붕어빵을 만드는 것을 고민하지 붕어빵틀이 어떻게 구성되었는지를 연구하며 내부적인 것들을 하나하나 생각해 보기에는 한계가 있습니다. (그래도 개발 중에 필요시 프레임워크의 내부적인 구성을 다 확인하며 개발하기에 아예 붕어빵틀 내부 구성을 파악하지 않는다는 말은 아닙니다. 다만 재료(비즈니스)가 더 중요시되기에 이렇게 표현하였습니다.)
그래서 저는 이 2가지(붕어빵 재료, 붕어빵틀 구성)를 잘 분리해서 공부하며 회사에서는 가능한 좋은 품질의 코드와 비즈니스를 멋지게 적용시켜서 사용자들에게 좋은 기능을 제공하고자 노력하고 집에서는 프레임워크가 어떻게 만들어졌는지 그 내부 구성을 파악하고자 노력하고 있습니다.
2025년 신년 목표가 단순히 쟤료를 넣어서 붕어빵을 만드는 사람을 넘어서서 필요시에는 언제든 붕어빵 틀을 고치거나 직접 만들어서 사용할 수도 있는 그런 개발자가 되고자 합니다. 그 시작이 이 포스팅입니다. 지금까지 저는 인프런에서 영한선생님께서 설명해 주신 강의를 열심히 들으며 스프링에 대한 기초를 다졌습니다. 그 과정에서 추상화된 방식으로 빈 등록 과정을 배웠으며 개념에 대해서는 알고 있었습니다. 그러나 조금 전문적인 대화를 할 때 누군가에게 빈 등록 과정을 설명할 수는 없었습니다. 왜냐하면 너무 추상화된 과정만 이해하고 있다 보니 실제로 어떻게 등록되는지는 모르는 것과 마찬가지였기 때문입니다. 그래서 이번 기회에 정말 깊게 공부해 보며 스프링의 빈 초기화과정을 자세히 알게 되어서 기쁩니다. 앞으로도 이렇게 붕어빵틀(프레임워크)을 분석하며 글을 정리해 나가고자 합니다.
그러니 많은 분들께서 제 포스팅을 통해 관련해서 호기심도 가져주시고 빈 생성 방식이나 프레임워크의 구성에 대한 배움도 얻어갈 수 있었으면 좋겠습니다. 많이 부족하고 잘못된 내용도 분명 있겠지만 재미있게 봐주셨으면 좋겠습니다. 언제든 잘못된 부분에 대해서 수정할 내용을 알려주시는 것도 환영합니다!
여기까지 읽어주셔서 감사합니다 :)