namalzi

← 글 목록

Spring 소스 파헤치기 6: 생명주기 콜백과 PostProcessor — 컨테이너에 끼어드는 자리

Spring · 게시

BeanFactoryPostProcessor, BeanPostProcessor와 그 하위 인터페이스들, Aware 계열, @PostConstruct·InitializingBean·init-method, @PreDestroy·DisposableBean·destroy-method, SmartInitializingSingleton, SmartLifecycle이 각각 언제, 어떤 순서로 불리는지 실제 소스와 실행 결과로 확인합니다. 끼어드는 자리가 왜 이렇게 여러 종류인지도 함께 봅니다.

이 글에서 다루는 것

"Spring 소스 파헤치기" 시리즈의 여섯 번째 글입니다. 4편에서 빈이 인스턴스화 → 의존성 주입 → 초기화의 세 단계로 만들어지는 것을 봤고, 5편에서 @Autowired 자리에 넣을 빈을 고르는 과정을 봤습니다. 이번에는 그 흐름의 곳곳에 끼어들 수 있는 자리를 모아서 봅니다. Spring의 많은 기능이 이 자리들에 매달려 있습니다.

답하려는 질문은 세 가지입니다.

항목 내용
기준 소스 Spring Framework 7.1.0-SNAPSHOT
커밋 f447f3c310 (2026-10-01, main 브랜치)
주로 읽는 모듈 spring-beans, spring-context
주로 읽는 클래스 PostProcessorRegistrationDelegate, AbstractAutowireCapableBeanFactory, ApplicationContextAwareProcessor, DisposableBeanAdapter, DefaultLifecycleProcessor

글에 나오는 줄 번호와 세부 동작은 모두 이 커밋 기준입니다. 다른 버전에서는 줄 번호가 다르고 세부 내용도 달라질 수 있습니다. 같은 커밋의 소스를 받는 방법은 1편에 있습니다.

이 글은 Spring을 처음 보는 분이 따라올 수 있는 큰 줄기만 골랐습니다. 빼놓은 것은 글 끝의 "이 글에서 뺀 것"에 이름만 적어 두었습니다.

먼저 알아 둘 말

콜백(callback)은 "이런 일이 생기면 이 메서드를 불러 달라"고 맡겨 두는 메서드입니다. 내 코드가 Spring을 부르는 것이 아니라 Spring이 내 코드를 부릅니다. 이 글에 나오는 자리는 전부 콜백입니다.

후처리기(PostProcessor)는 컨테이너가 무언가를 만든 직후에 끼어들어 손보는 객체입니다. 설계도(BeanDefinition)를 손보는 것이 BeanFactoryPostProcessor, 만들어지는 빈 객체를 손보는 것이 BeanPostProcessor입니다. 이 구분은 2편에서 한 번 나왔습니다.

Aware는 "컨테이너야, 나에게 ○○을 알려 다오"라는 인터페이스들의 공통 이름입니다. BeanNameAware를 구현하면 내 빈 이름을, ApplicationContextAware를 구현하면 컨테이너 자신을 받습니다. 메서드가 하나뿐인 작은 인터페이스들입니다.

초기화·소멸 콜백은 빈이 다 만들어졌을 때와 버려질 때 한 번씩 불리는 메서드입니다. 각각 세 가지 방법이 있습니다. 어노테이션(@PostConstruct·@PreDestroy), 인터페이스(InitializingBean·DisposableBean), 설정(@Bean(initMethod=…)·destroyMethod=…). 왜 셋이나 있는지는 5절에서 봅니다.

**Ordered와 PriorityOrdered**는 "나를 먼저 불러 달라"를 표현하는 인터페이스입니다. getOrder()가 돌려주는 숫자가 작을수록 먼저입니다(Ordered 49·55행). PriorityOrdered는 숫자와 상관없이 평범한 Ordered보다 항상 앞에 서는 특별 대우입니다(PriorityOrdered 20–28행).

phase는 SmartLifecycle이 시작·정지 순서를 정할 때 쓰는 숫자입니다. 6절에서 나옵니다.

1. 끼어드는 자리는 왜 이렇게 많은가 — 한 장의 지도

컨테이너를 켜는 refresh()는 열두 단계로 되어 있습니다(AbstractApplicationContext 582–626행, 단계 하나하나는 7편에서 봅니다). 이 글의 자리들은 그 단계 위에 흩어져 있습니다. 먼저 어디에 무엇이 있는지 지도를 펴 둡니다.

refresh의 흐름 위에 끼어드는 자리를 표시한 그림. prepareBeanFactory에서 ApplicationContextAwareProcessor가 등록되고, invokeBeanFactoryPostProcessors에서 설계도를 고치고, registerBeanPostProcessors에서 후처리기를 줄 세우고, finishBeanFactoryInitialization에서 빈을 만들면서 BPP·Aware·초기화 콜백이 불리고, 그 뒤 afterSingletonsInstantiated, finishRefresh의 SmartLifecycle.start가 이어진다. close()에서는 SmartLifecycle.stop 뒤에 destroySingletons가 소멸 콜백을 부른다. 파란 상자가 전부 끼어드는 자리입니다. 위에서 아래로 시간 순서입니다.

자리가 많은 이유는 다루는 대상과 시점이 다르기 때문입니다. 표로 묶으면 다섯 묶음입니다.

묶음 다루는 것 불리는 때 대표 인터페이스
설계도 후처리 BeanDefinition (아직 객체는 없다) 빈을 만들기 전, 한 번 BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor
빈 후처리 만들어지는 빈 객체 빈 하나마다 생성 흐름의 여러 지점 BeanPostProcessor와 하위 인터페이스 넷
빈 자신의 콜백 그 빈 자신 그 빈이 만들어질 때, 버려질 때 Aware 계열, InitializingBean, DisposableBean, @PostConstruct, @PreDestroy
전체 완료 콜백 싱글톤 전부 싱글톤이 모두 만들어진 뒤, 한 번 SmartInitializingSingleton
시작·정지 오래 도는 작업 (스레드, 연결) 컨테이너가 켜진 뒤와 닫히기 직전 Lifecycle, SmartLifecycle

"설계도 vs 객체", "하나마다 vs 전부 끝난 뒤", "만드는 일 vs 돌리는 일"이 각각 다른 자리를 필요로 합니다. 이제 위에서부터 하나씩 내려갑니다.

2. 설계도 단계 — BeanFactoryPostProcessor

refresh()의 다섯 번째 단계 invokeBeanFactoryPostProcessors는 PostProcessorRegistrationDelegate라는 클래스에 일을 넘깁니다(AbstractApplicationContext 795–796행). 이 클래스는 final이고 생성자가 private인, 정적 메서드만 모아 둔 도우미입니다(62–65행).

인터페이스는 둘입니다.

둘을 나눈 이유가 호출 순서에 있습니다. 등록하는 쪽이 먼저 다 돌아야 고치는 쪽이 고칠 설계도가 전부 모여 있기 때문입니다. invokeBeanFactoryPostProcessors의 뼈대는 다음과 같습니다. (// ...는 이 글에서 줄인 곳이고, 나머지는 소스 그대로입니다.)

			// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
			String[] postProcessorNames =
					beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
			for (String ppName : postProcessorNames) {
				if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
					currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
					processedBeans.add(ppName);
				}
			}
			sortPostProcessors(currentRegistryProcessors, beanFactory);
			// ...
			invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
			// ...

			// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
			// ...

			// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
			boolean reiterate = true;
			while (reiterate) {
				// ...
			}

			// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
			invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
			invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);

spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java 107–154행

BeanFactoryPostProcessor가 불리는 순서. 코드로 직접 등록한 것, BDRPP 빈의 PriorityOrdered·Ordered·나머지(새로 나타난 것이 없을 때까지 반복), 그 BDRPP들의 postProcessBeanFactory, BFPP 빈의 PriorityOrdered·Ordered·나머지 순이고 마지막에 clearMetadataCache가 병합된 설계도 캐시를 비운다. 같은 패턴이 두 번 되풀이됩니다. 등록하는 쪽(②~④)이 끝난 뒤에야 고치는 쪽(⑥~⑧)이 돕니다.

줄기만 적으면 이렇습니다.

  1. BeanDefinitionRegistryPostProcessor를 PriorityOrdered → Ordered → 나머지 순으로 찾아서 부릅니다. 매 묶음마다 getBeanNamesForType으로 다시 찾는 것이 눈에 띕니다(108·122·138행). 앞 묶음이 등록한 설계도 가운데 또 후처리기가 있을 수 있기 때문입니다. 마지막 묶음은 새로 나타난 것이 없을 때까지 while로 되풀이합니다(135–150행).
  2. 그렇게 모은 것들의 postProcessBeanFactory를 부릅니다(152–154행).
  3. 이제 평범한 BeanFactoryPostProcessor를 같은 세 묶음으로 부릅니다(164–204행). 1단계에서 이미 처리한 이름은 processedBeans로 걸러 두 번 부르지 않습니다.
  4. 끝으로 clearMetadataCache()로 병합된 설계도의 캐시를 비웁니다(206–208행). 후처리기가 원본 설계도를 고쳤을 수 있으니, 3편에서 본 병합본을 다시 만들게 하는 것입니다.

메서드 첫머리에는 "여러 리스트와 여러 번의 순회가 번거로워 보여도 일부러 그렇게 한 것이니 고치지 말라"는 경고 주석이 붙어 있습니다(71–82행). PriorityOrdered 후처리기가 Ordered 후처리기보다 먼저 만들어지는 것까지 약속이기 때문입니다. 후처리기도 빈이라 getBean()으로 만들어지는데, 그 순서가 바뀌면 어떤 빈이 어떤 후처리기의 손을 타는지가 달라집니다.

예제: 설계도를 등록하고 고치기

두 후처리기를 하나씩 두고, 컨테이너에 greeter라는 설계도를 직접 등록한 뒤 refresh()를 부릅니다.

// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
    @Configuration
    static class Config {

        /** 설계도를 새로 등록하는 쪽. 다른 BeanFactoryPostProcessor보다 먼저 불린다. */
        @Bean static BeanDefinitionRegistryPostProcessor registrar() {
            return registry -> {
                System.out.println("  [BDRPP] postProcessBeanDefinitionRegistry: registering extraGreeter");
                BeanDefinition extra = BeanDefinitionBuilder.genericBeanDefinition(Greeter.class)
                        .addPropertyValue("message", "value written by BDRPP").getBeanDefinition();
                registry.registerBeanDefinition("extraGreeter", extra);
            };
        }

        /** 이미 등록된 설계도를 고치는 쪽. 객체는 아직 하나도 없다. */
        @Bean static BeanFactoryPostProcessor rewriter() {
            return beanFactory -> {
                System.out.println("  [BFPP] postProcessBeanFactory: definitions=" + beanFactory.getBeanDefinitionCount()
                        + ", singletons created so far=" + java.util.Arrays.toString(beanFactory.getSingletonNames()));
                BeanDefinition greeter = beanFactory.getBeanDefinition("greeter");
                System.out.println("  [BFPP] greeter.message before: " + greeter.getPropertyValues().get("message"));
                greeter.getPropertyValues().add("message", "changed by BFPP");
            };
        }
    }

main에서는 context.registerBeanDefinition("greeter", …addPropertyValue("message", "original value")…)로 설계도를 넣고 refresh()한 뒤 두 빈의 message를 찍습니다. 실행 결과입니다.

[refresh]
  [BDRPP] postProcessBeanDefinitionRegistry: registering extraGreeter
  [BFPP] postProcessBeanFactory: definitions=11, singletons created so far=[environment, systemProperties, systemEnvironment, applicationStartup, org.springframework.context.annotation.internalConfigurationAnnotationProcessor, org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry, registrar, org.springframework.context.event.internalEventListenerProcessor, rewriter, org.springframework.context.event.internalEventListenerFactory]
  [BFPP] greeter.message before: original value
  Greeter constructor (the bean is actually created now)
  Greeter constructor (the bean is actually created now)
[getBean]
  greeter.message = changed by BFPP
  extraGreeter.message = value written by BDRPP

이 예제는 릴리스 버전인 Spring Framework 7.0.9로 실행해 위 출력을 확인했습니다(이 시리즈가 읽는 7.1.0-SNAPSHOT을 직접 빌드해 돌린 것은 아닙니다). 전체 코드는 ex/u04/FactoryPostProcessorExample.java입니다.

헷갈리기 쉬운 곳

3. 후처리기를 줄 세우기 — registerBeanPostProcessors

여섯 번째 단계 registerBeanPostProcessors는 BeanPostProcessor 빈들을 찾아 컨테이너의 후처리기 목록에 넣어 두기만 합니다. 실제로 불리는 것은 4절에서 빈을 만들 때입니다. 같은 클래스의 두 번째 정적 메서드입니다.

		// First, register the BeanPostProcessors that implement PriorityOrdered.
		sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

		// Next, register the BeanPostProcessors that implement Ordered.
		// ...
		sortPostProcessors(orderedPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, orderedPostProcessors);

		// Now, register all regular BeanPostProcessors.
		// ...
		registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

		// Finally, re-register all internal BeanPostProcessors.
		sortPostProcessors(internalPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, internalPostProcessors);

		// Re-register post-processor for detecting inner beans as ApplicationListeners,
		// moving it to the end of the processor chain (for picking up proxies etc).
		beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
	}

spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java 258–292행

2절과 같은 PriorityOrdered → Ordered → 나머지 순서인데, 끝에 두 줄이 더 있습니다. "내부 후처리기"를 다시 등록하고, ApplicationListenerDetector를 다시 등록합니다. 내부 후처리기란 MergedBeanDefinitionPostProcessor를 구현한 것들입니다(246–248행). @PostConstruct를 처리하는 CommonAnnotationBeanPostProcessor와 @Autowired를 처리하는 AutowiredAnnotationBeanPostProcessor가 둘 다 여기 들어갑니다(각각 InitDestroyAnnotationBeanPostProcessor 87–88행, AutowiredAnnotationBeanPostProcessor 158–159행).

"다시 등록"이 무슨 뜻인지는 목록에 넣는 메서드를 보면 압니다.

	@Override
	public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
		Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null");
		synchronized (this.beanPostProcessors) {
			// Remove from old position, if any
			this.beanPostProcessors.remove(beanPostProcessor);
			// Add to end of list
			this.beanPostProcessors.add(beanPostProcessor);
		}
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java 980–989행

이미 있던 자리에서 빼서 맨 뒤에 다시 넣습니다. 그래서 최종 목록은 다음과 같아집니다.

AnnotationConfigApplicationContext의 최종 BeanPostProcessor 목록. ApplicationContextAwareProcessor, BeanPostProcessorChecker, PriorityOrdered 빈(내부 후처리기는 뒤로 옮겨짐), Ordered 빈(TracerA 10 → TracerB 20), 순서 없는 빈, 다시 등록된 내부 후처리기(CommonAnnotation, AutowiredAnnotation), 맨 끝의 ApplicationListenerDetector 순이다. 빨간 상자가 함정입니다. @PostConstruct를 부르는 후처리기는 PriorityOrdered인데도 내가 만든 Ordered 후처리기보다 뒤에 섭니다.

맨 앞의 ApplicationContextAwareProcessor는 이 단계가 아니라 그보다 앞 단계 prepareBeanFactory에서 addBeanPostProcessor로 직접 넣은 것입니다(AbstractApplicationContext 737행). 그래서 어떤 빈 후처리기보다 앞에 있고, 5절에서 보듯 ApplicationContextAware가 다른 후처리기보다 먼저 처리되는 이유가 됩니다.

BeanPostProcessorChecker — 너무 일찍 만들어진 빈을 경고

이 단계가 시작할 때 BeanPostProcessorChecker라는 후처리기를 하나 끼웁니다(229–234행). 등록될 후처리기의 총 개수를 미리 세어 두고, 어떤 빈이 초기화를 마쳤는데 등록된 후처리기 수가 아직 그 총수에 못 미치면 "이 빈은 모든 BeanPostProcessor의 처리를 받을 수 없다(예: 자동 프록시 대상이 되지 못한다)"는 경고를 남깁니다(415–446행). 후처리기 A를 만드는 데 빈 B가 필요하면 B가 A보다 먼저 만들어지고, 그 B는 A 뒤에 등록될 후처리기의 손을 타지 못합니다. 2절의 static 권고가 피하려는 상황이 이것입니다(429–433행).

헷갈리기 쉬운 곳: 반환 타입이 순서를 정한다

이 글의 예제를 만들면서 실제로 걸린 곳입니다. Ordered를 구현한 Tracer 클래스를 @Bean static BeanPostProcessor tracerA()처럼 반환 타입을 BeanPostProcessor로 적었더니, order 값이 20인 쪽이 10인 쪽보다 먼저 불렸습니다. 반환 타입을 Tracer로 바꾸자 10 → 20 순서가 됐습니다.

이유는 위 소스의 beanFactory.isTypeMatch(ppName, Ordered.class)에 있습니다(243·250행). 아직 만들어지지 않은 빈의 타입을 설계도만 보고 판단하는데, @Bean 메서드로 만든 설계도에서 알 수 있는 타입은 메서드의 반환 타입입니다. 반환 타입이 BeanPostProcessor면 Ordered인지 알 수 없으니 "순서 없는 빈" 묶음으로 가서 등록된 순서대로 불립니다. 순서를 주고 싶다면 구체 타입을 반환 타입에 적어야 합니다.

4. 빈 하나를 만드는 동안의 훅 — BeanPostProcessor 가족

여기부터는 빈 하나마다 되풀이되는 자리입니다. 4편에서 initializeBean() 앞뒤의 훅 둘을 봤는데, 실제로는 생성 흐름 전체에 더 있습니다.

빈 하나가 만들어지는 동안 불리는 훅의 순서. postProcessBeforeInstantiation(지름길), 인스턴스화(determineCandidateConstructors), postProcessMergedBeanDefinition, populateBean의 postProcessAfterInstantiation과 postProcessProperties, invokeAwareMethods, postProcessBeforeInitialization(ApplicationContextAware, @PostConstruct), afterPropertiesSet과 init-method, postProcessAfterInitialization(프록시), registerDisposableBeanIfNecessary 순이다. 파란 상자가 후처리기가 끼어드는 자리, 노란 상자는 소멸을 위해 적어 두는 곳입니다.

BeanPostProcessor 자체는 메서드가 둘뿐이고 둘 다 default라 필요한 것만 구현하면 됩니다(67–111행). 나머지 자리는 이것을 상속한 하위 인터페이스 넷이 맡습니다.

인터페이스 더해지는 메서드 불리는 곳 (AbstractAutowireCapableBeanFactory) 쓰는 쪽의 예
InstantiationAwareBeanPostProcessor (44행) postProcessBeforeInstantiation createBean → resolveBeforeInstantiation (513행, 1123–1139행) 인스턴스화를 건너뛰고 프록시를 내놓기
postProcessAfterInstantiation populateBean 첫머리 (1415–1421행) false를 돌려주면 주입을 통째로 건너뛴다
postProcessProperties populateBean 가운데 (1438–1449행) @Autowired 필드·메서드 주입 (AutowiredAnnotationBeanPostProcessor 490행)
SmartInstantiationAwareBeanPostProcessor (37행) determineCandidateConstructors 인스턴스화 직전 (1318행) @Autowired 생성자 고르기 (5편)
getEarlyBeanReference 조기 참조 람다 안 (969행) 순환 참조 때 프록시를 미리 만들기 (4편 5절)
MergedBeanDefinitionPostProcessor (38행) postProcessMergedBeanDefinition 인스턴스화 직후, 빈마다 한 번 (573–584행) 클래스를 훑어 @Autowired·@PostConstruct 자리를 미리 찾아 두기
DestructionAwareBeanPostProcessor (30행) postProcessBeforeDestruction, requiresDestruction 소멸 때 (DisposableBeanAdapter 198–202행) @PreDestroy 부르기

첫 번째 자리만 코드로 봅니다. createBean()이 doCreateBean()에 들어가기 전에 묻는 질문입니다.

	protected @Nullable Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
		Object bean = null;
		if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
			// Make sure bean class is actually resolved at this point.
			if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
				Class<?> targetType = determineTargetType(beanName, mbd);
				if (targetType != null) {
					bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
					if (bean != null) {
						bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
					}
				}
			}
			mbd.beforeInstantiationResolved = (bean != null);
		}
		return bean;
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java 1123–1139행

후처리기가 여기서 null이 아닌 객체를 돌려주면 createBean()은 그 객체를 그대로 빈으로 삼고 끝냅니다(513–516행). 생성자도, 주입도, 초기화 메서드도 돌지 않고 "초기화 후" 훅만 한 번 거칩니다. 설명 주석대로 "기본 인스턴스화를 억누르고 특수한 프록시를 내놓는" 지름길입니다(InstantiationAwareBeanPostProcessor 46–53행).

왜 하나로 합치지 않았나

하위 인터페이스 둘의 설명 주석에는 같은 말이 적혀 있습니다. "이 인터페이스는 주로 프레임워크 내부용이다. 가능한 한 평범한 BeanPostProcessor를 구현하라"(InstantiationAwareBeanPostProcessor 34–36행, SmartInstantiationAwareBeanPostProcessor 29–32행). 사용자에게 보여 주는 문은 초기화 앞뒤 둘로 작게 두고, 인스턴스화 자체를 바꾸거나 주입에 손대는 위험한 자리는 따로 떼어 "안다면 쓰라"고 한 것입니다.

나눠 둔 덕에 컨테이너 쪽도 이득을 봅니다. AbstractBeanFactory는 등록된 후처리기 목록을 종류별로 미리 걸러 캐시해 둡니다(1024–1047행, 캐시 클래스는 2116–2125행). 위 표의 호출 지점들은 모두 전체 목록이 아니라 getBeanPostProcessorCache().instantiationAware 같은 걸러진 목록만 돕니다. 빈 하나마다 되풀이되는 자리라 instanceof 검사를 매번 하지 않으려는 것입니다.

5. 빈 자신이 받는 콜백 — Aware, 초기화, 그리고 실제 순서

4절의 훅은 남(후처리기)이 빈을 손보는 자리였고, 이 절은 빈 자신이 받는 콜백입니다. 모두 initializeBean() 안에서 일어납니다(1809–1834행, 본문은 4편 3절에 있습니다).

Aware는 두 곳에서 처리됩니다

initializeBean()의 첫 줄 invokeAwareMethods가 처리하는 것은 셋뿐입니다.

	private void invokeAwareMethods(String beanName, Object bean) {
		if (bean instanceof Aware) {
			if (bean instanceof BeanNameAware beanNameAware) {
				beanNameAware.setBeanName(beanName);
			}
			if (bean instanceof BeanClassLoaderAware beanClassLoaderAware) {
				ClassLoader bcl = getBeanClassLoader();
				if (bcl != null) {
					beanClassLoaderAware.setBeanClassLoader(bcl);
				}
			}
			if (bean instanceof BeanFactoryAware beanFactoryAware) {
				beanFactoryAware.setBeanFactory(AbstractAutowireCapableBeanFactory.this);
			}
		}
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java 1836–1851행

ApplicationContextAware, EnvironmentAware 같은 나머지는 여기 없습니다. spring-beans 모듈은 ApplicationContext가 무엇인지 모르기 때문입니다(2편의 층 구조). 그래서 spring-context는 그것들을 BeanPostProcessor로 처리합니다. 3절에서 맨 앞에 등록된다고 한 ApplicationContextAwareProcessor입니다.

	@Override
	public @Nullable Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		if (bean instanceof Aware) {
			invokeAwareInterfaces(bean);
		}
		return bean;
	}

spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java 82–88행

invokeAwareInterfaces는 EnvironmentAware부터 ApplicationContextAware까지 일곱 가지를 instanceof로 확인해 값을 넣어 줍니다(90–112행). Aware의 설명 주석도 "Aware를 구현하는 것만으로는 아무 일도 일어나지 않는다. 처리는 예를 들어 BeanPostProcessor에서 명시적으로 해야 한다"고 적어 두었습니다(25–29행). 즉 Aware는 표시일 뿐이고, 누가 그 표시를 보고 값을 넣어 주느냐는 모듈마다 다릅니다. 일곱 가지를 처리하는 순서는 if의 순서 그대로이고, 클래스 설명 주석에도 그 순서가 적혀 있습니다(39–47행).

초기화 메서드 셋의 순서

invokeInitMethods는 인터페이스 방식을 먼저, 설정 방식을 다음에 부릅니다.

	protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd)
			throws Throwable {

		boolean isInitializingBean = (bean instanceof InitializingBean);
		if (isInitializingBean && (mbd == null || !mbd.hasAnyExternallyManagedInitMethod("afterPropertiesSet"))) {
			// ...
			((InitializingBean) bean).afterPropertiesSet();
		}

		if (mbd != null && bean.getClass() != NullBean.class) {
			String[] initMethodNames = mbd.getInitMethodNames();
			if (initMethodNames != null) {
				for (String initMethodName : initMethodNames) {
					if (StringUtils.hasLength(initMethodName) &&
							!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
							!mbd.hasAnyExternallyManagedInitMethod(initMethodName)) {
						invokeCustomInitMethod(beanName, bean, mbd, initMethodName);
					}
				}
			}
		}
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java 1866–1889행

어노테이션 방식 @PostConstruct는 이 메서드에 없습니다. 그것은 CommonAnnotationBeanPostProcessor가 "초기화 전" 훅에서 부릅니다(InitDestroyAnnotationBeanPostProcessor 214–226행). CommonAnnotationBeanPostProcessor는 이 클래스를 상속한 것으로, 생성자에서 jakarta.annotation.PostConstruct와 PreDestroy를 초기화·소멸 어노테이션으로 등록합니다(190–195행). 그래서 @PostConstruct는 afterPropertiesSet보다 앞입니다.

위 코드의 hasAnyExternallyManagedInitMethod는 같은 메서드를 두 번 부르지 않기 위한 장치입니다. @PostConstruct가 붙은 메서드는 postProcessMergedBeanDefinition 단계에서 "바깥(후처리기)이 관리하는 초기화 메서드"로 설계도에 적힙니다(361–372행). 같은 메서드가 initMethod로도 지정되어 있으면 invokeInitMethods가 건너뜁니다.

예제: 전부 구현하고 순서를 찍어 보기

빈 하나가 이 글의 콜백을 전부 구현하고, Ordered 값이 다른 후처리기 둘(DestructionAwareBeanPostProcessor이기도 합니다)을 등록했습니다. 핵심만 보입니다.

// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
    static class Talkative implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware,
            ApplicationContextAware, InitializingBean, DisposableBean, SmartInitializingSingleton {

        Talkative() { System.out.println("  constructor"); }

        @Autowired
        void setHelper(Helper helper) { System.out.println("  @Autowired setter (dependency injection)"); }

        // ... Aware 메서드 넷, @PostConstruct, afterPropertiesSet, customInit,
        //     afterSingletonsInstantiated, @PreDestroy, destroy, customDestroy도 한 줄씩 찍는다
    }

    @Configuration
    static class Config {
        // 반환 타입을 Tracer로 적어야 컨테이너가 Ordered를 알아본다 (3절 "헷갈리기 쉬운 곳")
        @Bean static Tracer tracerB() { return new Tracer("TracerB(order=20)", 20); }  // 등록은 먼저
        @Bean static Tracer tracerA() { return new Tracer("TracerA(order=10)", 10); }  // order는 작다
        @Bean Helper helper() { return new Helper(); }
        @Bean(initMethod = "customInit", destroyMethod = "customDestroy")
        Talkative talkative() { return new Talkative(); }
    }

Tracer는 talkative 빈에 대해서만 postProcessBeforeInitialization, postProcessAfterInitialization, postProcessBeforeDestruction에서 자기 이름을 찍습니다. 컨테이너를 만들고 바로 close()한 결과입니다.

[refresh]
  constructor
  @Autowired setter (dependency injection)
  BeanNameAware.setBeanName(talkative)
  BeanClassLoaderAware.setBeanClassLoader
  BeanFactoryAware.setBeanFactory
  ApplicationContextAware.setApplicationContext
  TracerA(order=10).postProcessBeforeInitialization
  TracerB(order=20).postProcessBeforeInitialization
  @PostConstruct
  InitializingBean.afterPropertiesSet
  @Bean(initMethod) customInit
  TracerA(order=10).postProcessAfterInitialization
  TracerB(order=20).postProcessAfterInitialization
  SmartInitializingSingleton.afterSingletonsInstantiated
[close]
  TracerA(order=10).postProcessBeforeDestruction
  TracerB(order=20).postProcessBeforeDestruction
  @PreDestroy
  DisposableBean.destroy
  @Bean(destroyMethod) customDestroy

이 예제는 릴리스 버전인 Spring Framework 7.0.9로 실행해 위 출력을 확인했습니다(이 시리즈가 읽는 7.1.0-SNAPSHOT을 직접 빌드해 돌린 것은 아닙니다). 전체 코드는 ex/u04/LifecycleOrderExample.java입니다.

출력을 소스와 맞춰 보면 이렇습니다.

  1. 생성자 → @Autowired 세터. 주입(populateBean)이 initializeBean보다 앞이라는 4편의 순서 그대로입니다.
  2. BeanNameAware → BeanClassLoaderAware → BeanFactoryAware. invokeAwareMethods의 if 순서입니다.
  3. ApplicationContextAware. "초기화 전" 훅의 첫 번째 후처리기인 ApplicationContextAwareProcessor가 넣어 줍니다.
  4. TracerA → TracerB의 "초기화 전" 훅. 그다음이 @PostConstruct입니다. 3절 그림에서 본 대로 CommonAnnotationBeanPostProcessor는 내부 후처리기라 맨 뒤로 옮겨졌기 때문입니다.
  5. afterPropertiesSet → customInit. invokeInitMethods의 순서입니다.
  6. "초기화 후" 훅도 TracerA → TracerB.
  7. afterSingletonsInstantiated는 모든 싱글톤이 만들어진 뒤에 따로 불립니다(아래).
  8. 소멸은 postProcessBeforeDestruction(Tracer 둘 → @PreDestroy) → destroy() → customDestroy. 6절에서 봅니다.

예제 출력의 [refresh] 부분을 누가 부르는지로 나눈 그림. 주입까지(4편): constructor → @Autowired setter. invokeAwareMethods(spring-beans 가 직접, 셋뿐): BeanNameAware → BeanClassLoaderAware → BeanFactoryAware. 초기화 전 훅은 후처리기 차례대로 ① spring-context 의 ApplicationContextAwareProcessor 가 EnvironmentAware 부터 ApplicationContextAware 까지 일곱 가지를 넣는데 예제에서는 ApplicationContextAware, ② TracerA(order=10) → TracerB(order=20), ③ 맨 뒤로 옮겨진 내부 후처리기 CommonAnnotationBeanPostProcessor 가 @PostConstruct 를 부른다. 다음 invokeInitMethods: afterPropertiesSet → @Bean(initMethod) customInit. 초기화 후 훅: TracerA → TracerB. 마지막으로 모든 싱글톤을 만든 뒤 preInstantiateSingletons 가 SmartInitializingSingleton.afterSingletonsInstantiated 를 부른다 위 목록 1~7을 한 줄로 세운 것입니다. 빨간 칸의 @PostConstruct가 내 Ordered 후처리기보다 뒤에 있는 것을 눈여겨보세요. 순서는 예제의 실제 출력 그대로입니다.

BeanFactory 인터페이스의 설명 주석에 이 순서가 "표준 순서"로 정리되어 있습니다(70–98행). 위 실행 결과와 일치합니다.

헷갈리기 쉬운 곳: @PostConstruct는 "첫 번째 초기화 훅"이 아닙니다

흔히 "@PostConstruct는 afterPropertiesSet보다 먼저"라고만 기억하는데, 위 출력이 보여 주듯 내가 등록한 Ordered 후처리기의 "초기화 전" 훅보다는 뒤입니다. 내 후처리기에서 "이미 @PostConstruct가 끝난 빈"을 기대하면 틀립니다. 반대로 @PostConstruct 안에서 ApplicationContextAware로 받은 컨테이너는 쓸 수 있습니다. 그쪽은 맨 앞에 서 있으니까요.

모든 싱글톤이 만들어진 뒤 — SmartInitializingSingleton

preInstantiateSingletons()는 싱글톤을 전부 만든 뒤 한 번 더 돌면서 SmartInitializingSingleton을 찾습니다.

		// Trigger post-initialization callback for all applicable beans...
		for (String beanName : beanNames) {
			Object singletonInstance = getSingleton(beanName, false);
			if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
				// ...
				smartSingleton.afterSingletonsInstantiated();
				// ...
			}
		}

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java 1141–1150행

afterPropertiesSet과 다른 점은 시점입니다. afterPropertiesSet은 그 빈 하나가 끝났을 때라 다른 빈은 아직 없을 수 있지만, 이 콜백은 "모든 평범한 싱글톤이 이미 만들어졌음을 보장"합니다. 설명 주석은 그래서 getBeansOfType 같은 호출을 여기서 해도 "뜻하지 않은 조기 초기화"가 없다고 적습니다(47–50행). 같은 주석의 NOTE대로 지연 초기화(lazy) 싱글톤이나 다른 스코프의 빈에는 불리지 않습니다(51–54행).

6. 닫힐 때 — 소멸 순서와 Lifecycle

close()가 하는 일

close()는 doClose()에 일을 넘깁니다(1146–1171행). registerShutdownHook()을 불러 두었다면 JVM이 끝날 때 같은 doClose()가 스레드 하나에서 불립니다(1069–1088행). 그래서 close()는 성공하면 그 훅을 다시 떼어 냅니다(1158–1165행).

close()의 흐름. ContextClosedEvent 발행, LifecycleProcessor.onClose로 SmartLifecycle을 phase 큰 것부터 정지, destroySingletons로 등록 역순·나를 쓰는 빈 먼저 소멸, 빈 하나마다 DisposableBeanAdapter가 postProcessBeforeDestruction(@PreDestroy), DisposableBean.destroy, destroy-method 또는 close를 차례로 부른 뒤 closeBeanFactory로 끝난다. 초록 상자가 끝. 돌고 있는 것(Lifecycle)을 먼저 세우고 나서 객체를 정리합니다.

doClose()가 하는 일 가운데 이 글과 관련된 것은 네 가지입니다(1182–1233행). ContextClosedEvent를 발행하고(1191행), LifecycleProcessor.onClose()로 돌고 있는 것을 세우고(1200행), destroyBeans()로 싱글톤을 버리고(1208행), closeBeanFactory()로 컨테이너 자신을 닫습니다(1211행). 소스 주석은 Lifecycle을 먼저 세우는 이유를 "개별 소멸 중의 지연을 피하기 위해"라고 적습니다(1197행).

destroyBeans()는 destroySingletons()로 이어지고(1246–1248행), 거기서는 소멸이 필요한 빈의 이름을 등록된 역순으로 돌며 하나씩 버립니다(DefaultSingletonBeanRegistry 699–705행). 빈 하나를 버릴 때는 그 빈에 기대는 빈들을 먼저 버립니다(776–790행). 만든 순서의 반대입니다.

빈 하나의 소멸 — DisposableBeanAdapter

빈을 만들 때 doCreateBean()의 끝에서 registerDisposableBeanIfNecessary가 불립니다(640행). 소멸 콜백이 하나라도 있는 싱글톤이면 DisposableBeanAdapter로 감싸 disposableBeans 맵에 적어 둡니다(AbstractBeanFactory 1935–1943행). 소멸 콜백 세 방식의 순서가 이 어댑터의 destroy()에 있습니다.

	@Override
	public void destroy() {
		if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
			for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
				processor.postProcessBeforeDestruction(this.bean, this.beanName);
			}
		}

		if (this.invokeDisposableBean) {
			// ...
				((DisposableBean) this.bean).destroy();
			// ...
		}

		if (this.invokeAutoCloseable) {
			// ...
				((AutoCloseable) this.bean).close();
			// ...
		}
		else if (this.destroyMethods != null) {
			// ... invokeCustomDestroyMethod(destroyMethod)
		}
		// ...
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java 196–259행

초기화와 거울처럼 대칭입니다. 후처리기(@PreDestroy는 InitDestroyAnnotationBeanPostProcessor.postProcessBeforeDestruction, 234–253행) → 인터페이스(DisposableBean.destroy) → 설정(destroy-method). 5절 예제의 [close] 아래 다섯 줄이 이 순서입니다. Tracer 둘이 @PreDestroy보다 먼저인 것도 3절의 "내부 후처리기는 맨 뒤" 때문입니다.

destroyMethod는 지정하지 않아도 추론됩니다. @Bean의 destroyMethod 기본값은 "(inferred)"입니다(Bean 346행, 상수는 AbstractBeanDefinition 166행). 이 값이면 어댑터가 빈이 DisposableBean이 아닐 때에 한해 ExecutorService면 shutdown, 그 밖의 AutoCloseable이면 close, 아니면 close·shutdown이라는 이름의 public 메서드를 찾아 소멸 메서드로 삼습니다(407–459행). @Bean으로 등록한 DataSource가 따로 설정하지 않아도 닫히는 이유입니다. 반면 initMethod의 기본값은 빈 문자열이라 초기화 쪽에는 추론이 없습니다(317행).

오래 도는 것을 세우기 — Lifecycle과 SmartLifecycle

Lifecycle은 start()·stop()·isRunning() 셋뿐인 인터페이스입니다(50–84행). 스레드 풀이나 메시지 리스너처럼 만들어진 뒤에도 계속 도는 것을 위한 것이라, "만들기"와 "돌리기"를 따로 둡니다. 설명 주석의 NOTE가 중요합니다. "이 인터페이스는 자동 시작을 뜻하지 않는다. 그러려면 SmartLifecycle을 구현하라"(22–23행).

SmartLifecycle은 Lifecycle에 Phased를 더한 것으로(67행), isAutoStartup()이 기본 true이고(100–102행) getPhase()의 기본값은 Integer.MAX_VALUE입니다(82행, 158–160행).

이것을 돌리는 쪽이 DefaultLifecycleProcessor입니다. refresh()의 마지막 단계 finishRefresh()가 onRefresh()를(AbstractApplicationContext 1011–1014행), 위의 doClose()가 onClose()를 부릅니다. onRefresh()는 startBeans(true), 곧 자동 시작 대상만 시작합니다(DefaultLifecycleProcessor 304–307행, 대상 판정은 383–387행). 시작과 정지의 순서는 두 메서드의 TreeMap에 있습니다.

	private void startBeans(boolean autoStartupOnly) {
		Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
		Map<Integer, LifecycleGroup> phases = new TreeMap<>();
		// ...
	}

	// ...

	private void stopBeans(boolean pauseableOnly) {
		Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
		Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
		// ...
	}

spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java 365–450행

phase마다 묶음(LifecycleGroup)을 만들어 TreeMap에 넣으니, 시작은 phase가 작은 것부터, 정지는 reverseOrder()라 큰 것부터입니다. Phased가 아닌 평범한 Lifecycle은 phase 0으로 칩니다(562–564행). SmartLifecycle의 기본 phase가 Integer.MAX_VALUE인 이유가 여기서 보입니다. 자동 시작되는 것들은 가장 늦게 시작하고 가장 먼저 멈추는 쪽에 두는 것입니다(SmartLifecycle 70–74행).

SmartLifecycle의 phase 순서. onRefresh에서 phase 10의 early.start, phase 20의 late.start, 기본값 Integer.MAX_VALUE 순으로 시작하고, close에서는 그 반대 순서로 stop한 뒤 destroySingletons로 간다. 평범한 Lifecycle은 phase 0이지만 자동 시작되지 않는다는 참고 상자가 있다. 시작은 작은 phase부터, 정지는 큰 phase부터. 회색 상자는 예제의 plain 빈입니다.

예제: phase 두 개

// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
    @Configuration
    static class Config {
        @Bean Worker late() { return new Worker("late", 20); }    // 등록은 먼저, phase는 크다
        @Bean Worker early() { return new Worker("early", 10); }  // 등록은 나중, phase는 작다
        @Bean PlainWorker plain() { return new PlainWorker(); }
    }

Worker는 SmartLifecycle을 구현하고 start()·stop()에서 이름과 phase를 찍습니다. PlainWorker는 평범한 Lifecycle입니다. 실행 결과입니다.

[refresh]
  start early (phase 10)
  start late (phase 20)
  plain.isRunning() = false
[close]
  stop  late (phase 20)
  stop  early (phase 10)

이 예제는 릴리스 버전인 Spring Framework 7.0.9로 실행해 위 출력을 확인했습니다(이 시리즈가 읽는 7.1.0-SNAPSHOT을 직접 빌드해 돌린 것은 아닙니다). 전체 코드는 ex/u04/SmartLifecycleExample.java입니다. 등록 순서와 무관하게 phase 10이 먼저 시작해 나중에 멈췄고, 평범한 Lifecycle인 plain은 start()도 stop()도 불리지 않았습니다. 정지 쪽에서 stop()이 아예 찍히지 않은 것은 doStop이 isRunning()이 true인 빈만 세우기 때문입니다(467–468행).

헷갈리기 쉬운 곳

이 글에서 뺀 것

이름과 위치만 적어 둡니다. BeanDefinitionRegistryPostProcessor의 대표인 ConfigurationClassPostProcessor의 속(8편에서 봅니다). 빈을 만들기 전에 모든 설계도에 MergedBeanDefinitionPostProcessor를 미리 적용하는 AOT용 경로 invokeMergedBeanDefinitionPostProcessors(PostProcessorRegistrationDelegate 321–323행). ApplicationListener인 빈을 찾아 등록하는 ApplicationListenerDetector(48행부터, 이벤트는 12편). 싱글톤이 아닌 스코프 빈의 소멸 콜백 등록(AbstractBeanFactory 1944–1952행). 소멸 메서드가 리액티브 타입을 돌려줄 때 끝나기를 기다리는 처리(DisposableBeanAdapter 501–513행). 7.0에 더해진 컨테이너의 일시 정지·재시작(onPause·onRestart, LifecycleProcessor 36–54행)과 phase별 동시 시작·정지 타임아웃(DefaultLifecycleProcessor 603–631행). 그리고 refresh() 도중 예외가 나면 이미 시작한 Lifecycle을 멈추고 만든 싱글톤을 버리는 되돌리기(AbstractApplicationContext 628–652행).

정리

개념 한 줄 요약
BeanFactoryPostProcessor 빈을 만들기 전에 설계도를 읽고 고친다. 빈 인스턴스는 건드리지 않는다
BeanDefinitionRegistryPostProcessor 설계도를 새로 등록한다. 고치는 쪽보다 먼저, 새로 나타난 것이 없을 때까지 되풀이해 불린다
PriorityOrdered → Ordered → 나머지 설계도 후처리기와 빈 후처리기 모두 이 세 묶음 순서. 인터페이스로 판정하므로 @Order는 통하지 않는다
후처리기 반환 타입 @Bean 메서드의 반환 타입이 곧 컨테이너가 아는 타입. Ordered가 보이게 적어야 순서가 먹는다
내부 후처리기 다시 등록 @PostConstruct·@Autowired 처리기는 목록의 맨 뒤로 옮겨진다. 내 Ordered 후처리기가 그보다 앞이다
BeanPostProcessor 가족 초기화 앞뒤는 누구나, 인스턴스화·주입·소멸에 끼는 하위 인터페이스 넷은 "주로 프레임워크 내부용"
postProcessBeforeInstantiation 객체를 돌려주면 생성 흐름을 통째로 건너뛰는 지름길
Aware 표시일 뿐. spring-beans의 셋은 invokeAwareMethods가, 나머지는 ApplicationContextAwareProcessor가 넣어 준다
초기화 순서 Aware → 초기화 전 훅(ApplicationContextAware, …, @PostConstruct) → afterPropertiesSet → init-method → 초기화 후 훅
SmartInitializingSingleton 모든 싱글톤이 만들어진 뒤 한 번. 다른 빈을 안전하게 찾을 수 있는 자리
소멸 순서 등록 역순, 기대는 빈 먼저. 빈 하나는 postProcessBeforeDestruction(@PreDestroy) → destroy() → destroy-method/close()
destroyMethod 추론 기본값 (inferred). ExecutorService면 shutdown, AutoCloseable이면 close, 아니면 close·shutdown 이름을 찾는다
SmartLifecycle 만든 뒤 돌리고 닫기 전에 세운다. 시작은 phase 작은 것부터, 정지는 큰 것부터. 기본 phase는 Integer.MAX_VALUE

다음에 볼 것

이 글은 refresh()의 단계들 위에 자리를 표시하며 지나갔습니다. 다음 7편: refresh() 12단계 — 컨테이너가 켜지는 순서에서는 그 열두 단계를 처음부터 끝까지 차례로 따라갑니다. prepareBeanFactory가 무엇을 준비하는지, 메시지 소스와 이벤트 멀티캐스터가 왜 그 자리에서 만들어지는지, 그리고 이 글의 후처리기들이 그 안에서 어떤 단계에 속하는지가 거기서 한 줄로 이어집니다.

다른 글