@PostConstruct는 컨테이너 어디쯤에서 불리나 — Spring BeanPostProcessor

seonwoo_jung·2026년 5월 26일

1. 도입

@PostConstruct가 컨테이너 어디쯤에서 호출되는지, AOP 프록시는 어디서 끼어드는지 헷갈렸다. 답은 결국 BeanPostProcessor(이하 BPP)에 있었다. Spring 컨테이너는 빈을 만들 때 생성자만 부르고 끝내는 게 아니라, 인스턴스 생성 → 의존성 주입 → 초기화 콜백 사이사이에 BPP 훅이 끼어들어 빈을 가공한다.

이 글은 BPP가 무엇이고 컨테이너 초기화의 어느 시점에 어떤 메서드가 불리는지, Spring Reference §1.8.1을 따라가며 정리한 노트다.

2. 핵심 개념

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 계열로 구현돼 있다.

3. 내부 동작

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() 호출 시 어드바이스가 안 탐)가 여기서 나온다.

자주 헷갈리는 지점

  • BPP 자기 자신은 컨테이너 부팅 시 일반 빈보다 먼저 등록된다(§1.8.1). BPP에 일반 빈을 @Autowired로 받으면 그 빈이 너무 일찍 만들어져 다른 BPP의 가공을 못 받을 수 있다.
  • 여러 BPP의 순서는 Ordered@Order로 제어한다.

4. 예시 / 코드

가장 작은 형태의 커스텀 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$$...로 찍히면 이 단계를 통과한 결과다.

5. 정리

BPP는 "빈을 만들고 컨테이너에 넘기기 전, 마지막 가공 라인"이다.

  • before/after는 초기화 콜백(@PostConstruct 등)을 감싼다.
  • @Autowired는 그보다 앞서, AOP 프록시는 after 시점에 끼어든다.
  • 반환값을 바꾸면 컨테이너에 등록되는 객체가 바뀐다 — 프록시 패턴의 정석.

더 파고들 만한 주제: InstantiationAwareBeanPostProcessor(더 이른 훅), BeanFactoryPostProcessor vs BeanPostProcessor(BeanDefinition 단계 vs 인스턴스 단계).

참고 자료

  • Spring Framework Reference §1.8.1 "Customizing Beans by Using a BeanPostProcessor"
  • Spring Framework Reference §1.6 "Customizing the Nature of a Bean"
  • org.springframework.beans.factory.config.BeanPostProcessor Javadoc

0개의 댓글