@PostConstruct가 컨테이너 어디쯤에서 호출되는지, AOP 프록시는 어디서 끼어드는지 헷갈렸다. 답은 결국 BeanPostProcessor(이하 BPP)에 있었다. Spring 컨테이너는 빈을 만들 때 생성자만 부르고 끝내는 게 아니라, 인스턴스 생성 → 의존성 주입 → 초기화 콜백 사이사이에 BPP 훅이 끼어들어 빈을 가공한다.
이 글은 BPP가 무엇이고 컨테이너 초기화의 어느 시점에 어떤 메서드가 불리는지, Spring Reference §1.8.1을 따라가며 정리한 노트다.
BPP는 컨테이너가 빈을 초기화하기 직전과 직후에 끼어들 수 있는 인터페이스다.
Spring Reference §1.8.1에 따르면 인터페이스는 두 메서드만 정의한다.
public interface BeanPostProcessor {
// 초기화 콜백(@PostConstruct, afterPropertiesSet, init-method) "직전"
Object postProcessBeforeInitialization(Object bean, String beanName);
// 초기화 콜백 "직후"
Object postProcessAfterInitialization(Object bean, String beanName);
}
두 메서드 모두 Object를 반환한다. 반환된 객체가 다음 단계로 넘어가는 빈이다. 원본을 그대로 돌려줄 수도 있고, 프록시로 감싼 새 객체를 돌려줄 수도 있다. Spring AOP가 정확히 이 자리에서 동작한다.
이 훅이 필요한 이유는 싱글톤 등록 직전 빈을 가공할 공식 확장 지점이 있어야 해서다. @Autowired, AOP 프록시, *Aware 콜백이 모두 BPP 계열로 구현돼 있다.
Spring Reference §1.6.1과 §1.8을 합치면 싱글톤 빈의 생애는 대략 이렇다.
1) 인스턴스화 (생성자 호출)
2) 의존성 주입 (@Autowired, setter, 필드 주입)
3) *Aware 콜백 (BeanNameAware, BeanFactoryAware …)
4) BPP.postProcessBeforeInitialization
5) 초기화 콜백 (@PostConstruct → afterPropertiesSet → init-method)
6) BPP.postProcessAfterInitialization
7) (사용)
8) 소멸 콜백 (@PreDestroy → destroy → destroy-method)
4번과 6번이 BPP의 자리다. @PostConstruct는 그 사이(5번)에 있다. 그래서 before에서 본 객체와 after에서 본 객체는 서로 다를 수 있다 — 중간 초기화 콜백이 상태를 바꿔놓거나, BPP가 반환값을 교체할 수도 있다.
@Autowired와 AOP는 어디서?@Autowired도 BPP로 처리되지만, AutowiredAnnotationBeanPostProcessor는 더 이른 시점의 하위 인터페이스(InstantiationAwareBeanPostProcessor)를 구현해 2번 단계에 끼어든다. 주입은 before보다 앞에서 끝나는 셈이다.
AOP 쪽 AnnotationAwareAspectJAutoProxyCreator도 BPP다. 이 친구는 postProcessAfterInitialization에서 프록시를 만들어 반환한다. 컨테이너에 최종 등록되는 건 원본 빈이 아니라 그 빈을 감싼 프록시다. AOP의 self-invocation 문제(클래스 내부에서 this.method() 호출 시 어드바이스가 안 탐)가 여기서 나온다.
@Autowired로 받으면 그 빈이 너무 일찍 만들어져 다른 BPP의 가공을 못 받을 수 있다.Ordered나 @Order로 제어한다.가장 작은 형태의 커스텀 BPP — 모든 빈 이름과 실제 타입을 찍어본다.
@Component
public class NameLoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// 초기화 콜백(@PostConstruct) 직전
System.out.println("[before] " + beanName + " : " + bean.getClass().getSimpleName());
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 초기화 콜백 직후 — AOP 프록시였다면 여기서 프록시로 교체될 수 있음
System.out.println("[after] " + beanName + " : " + bean.getClass().getSimpleName());
return bean;
}
}
엣지 케이스: after에서 다른 객체를 반환하면 그 이후 컨테이너가 추적하는 빈은 그 객체다. 디버깅 중 빈 타입이 MyService$$EnhancerBySpringCGLIB$$...로 찍히면 이 단계를 통과한 결과다.
BPP는 "빈을 만들고 컨테이너에 넘기기 전, 마지막 가공 라인"이다.
before/after는 초기화 콜백(@PostConstruct 등)을 감싼다.@Autowired는 그보다 앞서, AOP 프록시는 after 시점에 끼어든다.더 파고들 만한 주제: InstantiationAwareBeanPostProcessor(더 이른 훅), BeanFactoryPostProcessor vs BeanPostProcessor(BeanDefinition 단계 vs 인스턴스 단계).
org.springframework.beans.factory.config.BeanPostProcessor Javadoc