BeanPostProcessor — 애노테이션이 실제 동작으로 바뀌는 곳
스프링에서 @Autowired를 붙이면 의존성이 채워지고, @EventListener를
붙이면 이벤트를 받고, @Transactional을 붙이면 트랜잭션이 걸린다. 우리는
이 "붙이면 저절로"에 너무 익숙해서, 그게 누가 해 주는 일인지는 잘
생각하지 않는다.
답은 하나로 모인다: BeanPostProcessor. 빈이 만들어지는 파이프라인 중간에 끼어들어, 애노테이션을 읽고 실제 동작으로 바꿔 주는 확장 지점이다. 스프링의 "마법"이라 부르는 것들의 상당수가 여기서 벌어진다.
BeanPostProcessor는 빈 생성 파이프라인의 한 지점에 끼어드는 장치다. 그 파이프라인 전체가 궁금하다면 Bean 생성 과정 글을 먼저 보고 오면 이 글이 훨씬 선명해진다.
1. BeanPostProcessor란 무엇인가
이름을 그대로 풀면 "빈(Bean)을 후처리(PostProcess)하는 것"이다. 인터페이스 자체는 놀랄 만큼 단순하다. 메서드가 딱 두 개다.
public interface BeanPostProcessor {
// 초기화(@PostConstruct 등) '전'에 각 빈마다 호출
Object postProcessBeforeInitialization(Object bean, String beanName);
// 초기화 '후'에 각 빈마다 호출
Object postProcessAfterInitialization(Object bean, String beanName);
}핵심은 이 두 메서드가 컨테이너가 만드는 모든 빈 하나하나에 대해 호출된다는 점이다. 그리고 결정적으로, 반환값이 다시 그 빈으로 쓰인다. 즉 후처리기는 빈을 들여다보기만 하는 게 아니라, 다른 객체로 바꿔치기할 수도 있다.
postProcessAfterInitialization이 원본 대신
프록시 객체를 반환하면, 컨테이너에는 그 프록시가
등록된다. @Transactional·@Cacheable이
동작하는 원리가 정확히 이것이다 —
AOP와 Dynamic Proxy 글 참고.
2. 파이프라인의 어디에 끼어드나
빈 생성 순서 위에 후처리기를 겹쳐 보면, 정확히 어느 틈에 개입하는지가 보인다.
생성자 호출, @Autowired 채우기
각 후처리기가 초기화 전에 개입
초기화 콜백
여기서 프록시로 교체 가능
원본 또는 프록시가 컨테이너에 등록
여기서 재밌는 사실. @Autowired를 채우는 것도, @EventListener를 등록하는
것도 각각 별개의 후처리기가 한다. 스프링은 우리가 아는 애노테이션 기능들을
대부분 "내장 BeanPostProcessor"로 구현해 두었다.
- ·@Autowired / @Value 로 주입
- ·@PostConstruct / @PreDestroy 콜백
- ·@EventListener 로 이벤트 구독
- ·@Transactional 로 트랜잭션
- ·@Async / @Cacheable 등
- ·AutowiredAnnotationBeanPostProcessor
- ·CommonAnnotationBeanPostProcessor
- ·EventListenerMethodProcessor 계열
- ·AnnotationAwareAspectJAutoProxyCreator (AOP)
- ·각 기능마다 전담 후처리기가 있다
후처리기들의 긴 클래스 이름을 외울 필요는 없다. 중요한 건 "애노테이션 = 그걸 해석하는 후처리기가 뒤에 있다"는 구조 감각이다.
3. 왜 이 구조로 만들었나
스프링이 굳이 이런 확장 지점을 둔 이유는, 애노테이션 기능을 컨테이너 본체에서 떼어내기 위해서다. 이게 왜 좋은지 세 가지로 정리된다.
- 1관심사가 분리된다
컨테이너는 '빈을 만든다'만 하고, '@Autowired를 해석한다'는 별도 후처리기가 맡는다.
컨테이너 코어 코드를 건드리지 않고도, 후처리기를 추가하는 것만으로 새로운 애노테이션 기능을 얹을 수 있다.
- 2우리가 직접 만들 수도 있다
커스텀 애노테이션을 만들고, 그걸 해석하는 후처리기를 등록하면 나만의 '마법'을 만들 수 있다.
예를 들어
@MaskLog같은 애노테이션을 정의하고, 후처리기에서 그게 붙은 빈을 프록시로 감싸 로그를 가리게 할 수 있다. 스프링이 하는 일을 똑같이 할 수 있다는 뜻이다. - 3다른 후처리기보다 먼저 등록된다
BeanPostProcessor 자신도 빈이지만, 일반 빈보다 먼저 만들어져야 한다.
후처리기가 일반 빈을 처리하려면, 그 일반 빈보다 먼저 준비돼야 한다. 그래서 컨테이너는 부팅 초반에 후처리기부터 등록한다. 이 순서 보장은 ApplicationContext가 알아서 해 준다.
이름이 비슷한 BeanFactoryPostProcessor는 다른 것이다.
이건 빈이 만들어지기 전, 설계도(BeanDefinition) 단계에
개입한다. 예: @Value의 ${...} 플레이스홀더를
실제 값으로 치환하는 일. 요약하면 — BeanFactoryPostProcessor는
설계도를 손보고, BeanPostProcessor는 완성 직전의 객체를 손본다.
4. @EventListener는 어떻게 붙나 (예시)
@EventListener를 예로 흐름을 구체화해 보자. 우리는 그저 메서드에
애노테이션을 붙였을 뿐인데, 그게 어떻게 "이벤트를 받는 리스너"가 되는가.
- 1부팅 시 스캔
전용 후처리기가 모든 빈의 메서드를 훑어 @EventListener를 찾는다.
- 2리스너로 등록
찾은 메서드를 이벤트 종류별로 컨테이너의 이벤트 멀티캐스터에 등록한다.
- 3이벤트 발행
누군가 applicationEventPublisher.publishEvent()를 호출하면,
- 4매칭 호출
그 이벤트 타입에 맞게 등록해 둔 메서드들이 불린다.
즉 @EventListener도 결국 "애노테이션을 스캔해 실제 등록 동작으로
바꿔 주는 후처리기" 덕에 동작한다. 앞서 본 @Autowired,
@Transactional과 완전히 같은 패턴이다. 애노테이션이 마법처럼 보였던 건,
그 뒤에서 조용히 도는 후처리기를 못 봤기 때문일 뿐이다.
한 장 요약
- 1정체
@Autowired·@EventListener·@Transactional 등 '붙이면 저절로'의 실행 주체. 빈 생성 파이프라인에 끼어드는 확장 지점.
- 2인터페이스
postProcessBeforeInitialization / AfterInitialization 두 메서드. 모든 빈마다 호출되고, 반환값이 그 빈이 된다.
- 3핵심 능력
반환값을 바꿀 수 있어 원본을 프록시로 교체 가능 — @Transactional·@Cacheable의 토대.
- 4내장 후처리기
@Autowired·@PostConstruct·@EventListener·AOP는 각각 전담 내장 후처리기가 처리한다.
- 5vs BeanFactoryPostProcessor
후자는 '설계도(BeanDefinition)'를 손보고, BeanPostProcessor는 '완성 직전 객체'를 손본다.
- 6다음 글
후처리기가 빈을 프록시로 감싸 @Transactional을 거는 원리 — AOP와 Dynamic Proxy 편.