BeanPostProcessor
빈 후처리기가 초기화된 객체를 프록시로 교체하는 과정을 실행하고 자동 적용·조기 참조·이중 프록시 문제를 검증합니다.
ProxyFactory를 빈마다 호출하면 래퍼 클래스 반복은 사라져도 구성 반복은 남습니다.
BeanPostProcessor는 컨테이너가 만드는 빈을 초기화 전후에 관찰하고 최종 참조를 다른 객체로 바꿀 수 있습니다.
Spring의 자동 프록시 생성기는 등록된 어드바이저와 빈을 대조해 필요한 대상만 프록시로 감싸므로 애플리케이션 코드와 연결에서 횡단 관심을 분리합니다.
BeanPostProcessor
초기화한 bean 판별 뒤 proxy 또는 원 참조를 선택해 최종 노출하며 target 자체가 proxy로 변하지 않는다.
BEAN LIFECYCLE · CONDITIONAL FINAL REFERENCE
초기화 후 조건에 맞는 빈의 최종 참조를 바꾼다
BeanPostProcessor는 컨테이너 수명주기의 확장점입니다. 일반적인 초기화 후 경로에서 대상이 준비되면 조건을 판정해 프록시 또는 원본을 반환합니다.
- target 생성 → 주입 → 초기화
이 빈 자신의 property population 뒤 초기화가 완료됩니다. 일반적인 after-initialization 경로를 보여줍니다.
- 처리기 조건 판정 → 최종 참조
대상이면 proxy, 불일치/인프라면 원본을 반환합니다. 컨테이너가 선택된 최종 참조를 노출합니다.
학습용 처리기와 자동 생성기
원문의 TimingPostProcessor는 TimedComponent 표시만 보고 중복 포장 검사가 없습니다. 인프라 자동 생성기는 적합한 advisor·포인트컷을 찾고 인프라 타입을 제외합니다.
참조와 적용 수 검증
조기 참조/순환 의존으로 원본과 프록시가 나뉘지 않게 구조를 정리합니다. proxy 여부뿐 아니라 advisor 수·순서·대상 호출 수와 이중 래핑을 확인합니다.
새 프록시를 반환해도 원본 target 자체의 자기 호출은 우회됩니다. 사용자 처리기의 조기 빈 조회와 여러 생성기 중복 등록을 피합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 일반 after-initialization에서 초기화된 bean이 TimedComponent면 new ProxyFactory로 새 proxy를 반환하고 아니면 같은 bean을 반환한다. 최종 노출 참조를 바꾸는 것이며 원래 target 객체 자체를 proxy로 변환하는 것이 아니다. 이 사용자 if는 인프라 자동 생성기의 Advisor/Advice/Pointcut 제외 검사를 구현하지 않으며 이미 proxy인지 확인하는 중복 검사도 없다. 두 branch는 최종 참조 노출에서 합류하고 조기 순환 참조는 별도다.
- 생성, 이 빈의 의존성 주입, 초기화를 마친 대상이 초기화 후 처리기에 들어간다. 적용 대상이면 proxy를 반환하고 불일치 또는 인프라 빈이면 원본을 반환한다. 컨테이너는 최종 참조를 노출한다. 조기 순환 참조는 별도 고려사항이다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | BeanPostProcessor |
| 4 | BeanPostProcessor는 컨테이너 수명 주기의 확장점입니다. 일반적인 초기화 후 경로에서 대상이 준비되면 조건을 판정해 프록시 또는 원본을 반환합니다. |
| 6 | 일반 after-initialization에서 초기화된 bean이 TimedComponent면 new ProxyFactory로 새 proxy를 반환하고 아니면 같은 bean을 반환한다. 최종 노출 참조를 바꾸는 것이며 원래 target 객체 자체를 proxy로 변환하는 것이 아니다. 이 사용자 if는 인프라 자동 생성기의 Advisor/Advice/Pointcut 제외 검사를 구현하지 않으며 이미 proxy인지 확인하는 중복 검사도 없다. 두 branch는 최종 참조 노출에서 합류하고 조기 순환 참조는 별도다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 초기화된 bean 일반 after-initialization에서 TimedComponent인지 확인합니다. | 입력: 초기화 bean · 판정: TimedComponent | 최종 노출 참조를 선택합니다. |
| 새 proxy 또는 원 참조 맞으면 ProxyFactory의 새 proxy를 반환하고 아니면 같은 bean입니다. | 일치: 새 proxy · 불일치: 원 bean | 반환 참조가 조건에 따라 달라집니다. |
| 최종 참조 노출 선택한 참조를 컨테이너가 노출합니다. | target 객체: 그대로 · 이미 proxy 검사: 현재 없음 | 원 target 변환과 proxy 참조 노출을 구분합니다. |
BeanPostProcessor
초기화한 bean 판별 뒤 proxy 또는 원 참조를 선택해 최종 노출하며 target 자체가 proxy로 변하지 않는다.
BEAN LIFECYCLE · CONDITIONAL FINAL REFERENCE
초기화 후 조건에 맞는 빈의 최종 참조를 바꾼다
BeanPostProcessor는 컨테이너 수명주기의 확장점입니다. 일반적인 초기화 후 경로에서 대상이 준비되면 조건을 판정해 프록시 또는 원본을 반환합니다.
- target 생성 → 주입 → 초기화
이 빈 자신의 property population 뒤 초기화가 완료됩니다. 일반적인 after-initialization 경로를 보여줍니다.
- 처리기 조건 판정 → 최종 참조
대상이면 proxy, 불일치/인프라면 원본을 반환합니다. 컨테이너가 선택된 최종 참조를 노출합니다.
학습용 처리기와 자동 생성기
원문의 TimingPostProcessor는 TimedComponent 표시만 보고 중복 포장 검사가 없습니다. 인프라 자동 생성기는 적합한 advisor·포인트컷을 찾고 인프라 타입을 제외합니다.
참조와 적용 수 검증
조기 참조/순환 의존으로 원본과 프록시가 나뉘지 않게 구조를 정리합니다. proxy 여부뿐 아니라 advisor 수·순서·대상 호출 수와 이중 래핑을 확인합니다.
새 프록시를 반환해도 원본 target 자체의 자기 호출은 우회됩니다. 사용자 처리기의 조기 빈 조회와 여러 생성기 중복 등록을 피합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 일반 after-initialization에서 초기화된 bean이 TimedComponent면 new ProxyFactory로 새 proxy를 반환하고 아니면 같은 bean을 반환한다. 최종 노출 참조를 바꾸는 것이며 원래 target 객체 자체를 proxy로 변환하는 것이 아니다. 이 사용자 if는 인프라 자동 생성기의 Advisor/Advice/Pointcut 제외 검사를 구현하지 않으며 이미 proxy인지 확인하는 중복 검사도 없다. 두 branch는 최종 참조 노출에서 합류하고 조기 순환 참조는 별도다.
- 생성, 이 빈의 의존성 주입, 초기화를 마친 대상이 초기화 후 처리기에 들어간다. 적용 대상이면 proxy를 반환하고 불일치 또는 인프라 빈이면 원본을 반환한다. 컨테이너는 최종 참조를 노출한다. 조기 순환 참조는 별도 고려사항이다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | BeanPostProcessor |
| 4 | BeanPostProcessor는 컨테이너 수명 주기의 확장점입니다. 일반적인 초기화 후 경로에서 대상이 준비되면 조건을 판정해 프록시 또는 원본을 반환합니다. |
| 6 | 일반 after-initialization에서 초기화된 bean이 TimedComponent면 new ProxyFactory로 새 proxy를 반환하고 아니면 같은 bean을 반환한다. 최종 노출 참조를 바꾸는 것이며 원래 target 객체 자체를 proxy로 변환하는 것이 아니다. 이 사용자 if는 인프라 자동 생성기의 Advisor/Advice/Pointcut 제외 검사를 구현하지 않으며 이미 proxy인지 확인하는 중복 검사도 없다. 두 branch는 최종 참조 노출에서 합류하고 조기 순환 참조는 별도다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 초기화된 bean 일반 after-initialization에서 TimedComponent인지 확인합니다. | 입력: 초기화 bean · 판정: TimedComponent | 최종 노출 참조를 선택합니다. |
| 새 proxy 또는 원 참조 맞으면 ProxyFactory의 새 proxy를 반환하고 아니면 같은 bean입니다. | 일치: 새 proxy · 불일치: 원 bean | 반환 참조가 조건에 따라 달라집니다. |
| 최종 참조 노출 선택한 참조를 컨테이너가 노출합니다. | target 객체: 그대로 · 이미 proxy 검사: 현재 없음 | 원 target 변환과 proxy 참조 노출을 구분합니다. |
빈 후처리기 확장 지점
컨테이너는 인스턴스 생성, 의존성 주입, 인식 콜백, 초기화 전 처리기, 초기화 콜백, 초기화 후 처리기를 거쳐 빈 참조를 노출합니다.
프록시는 흔히 초기화 후 단계에서 완성된 대상을 감싸 반환됩니다.
모든 빈을 무조건 프록시로 만들면 구성 클래스, 어드바이저, 후처리기 자체까지 재귀 적용될 수 있습니다.
인프라 타입과 포인트컷 불일치 빈은 원본을 반환합니다.
빈 이름 문자열만으로 중요한 보안 범위를 정하지 않습니다.
package board.autoproxy;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public final class TimingPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (!(bean instanceof TimedComponent)) {
return bean;
}
var factory = new ProxyFactory(bean);
factory.addAdvice((MethodInterceptor) invocation -> {
long started = System.nanoTime();
try {
return invocation.proceed();
} finally {
System.out.println("timed=" + invocation.getMethod().getName()
+ " nanos=" + (System.nanoTime() - started));
}
});
return factory.getProxy();
}
public interface TimedComponent { }
}표시 인터페이스는 학습용 선택 기준입니다.
운영 환경에서는 어드바이저 포인트컷과 인프라 자동 프록시 생성기를 사용해 타입·애노테이션·메서드 조건을 분리합니다.
이 TimingPostProcessor의 if는 표시 인터페이스만 검사합니다. 반환 참조가 바뀌어도 원 target의 메모리 객체 자체가 프록시 객체로 변하는 것은 아닙니다. 컨테이너에서 받은 최종 참조로 호출해야 추가 기능을 통과하며, 별도로 보관한 원 target 참조는 그대로 대상을 호출합니다.
원 ReportTarget은 ReportService와 TimedComponent를 모두 구현합니다. 새 ProxyFactory(bean)의 인터페이스 프록시에도 그 표시가 남을 수 있어, 같은 사용자 처리기를 두 번 적용하는 것이 자동으로 멱등하지 않습니다. 한 번 감싼 proxy라는 이유로 이 if가 거짓이 된다고 기대하지 않습니다.
최종 프록시 주입
다음 컨텍스트는 사용자 정의 후처리기를 정적 빈으로 먼저 등록하고 표시 서비스를 프록시로 바꿉니다.
클라이언트는 생성 사실을 몰라도 인터페이스 호출이 측정됩니다.
package board.autoproxy;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public final class PostProcessorDemo {
interface ReportService {
int weeklyPostCount(long authorId);
}
static final class ReportTarget
implements ReportService, TimingPostProcessor.TimedComponent {
@Override
public int weeklyPostCount(long authorId) {
return authorId == 41L ? 12 : 0;
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
static TimingPostProcessor timingPostProcessor() {
return new TimingPostProcessor();
}
@Bean
ReportService reportService() {
return new ReportTarget();
}
}
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(Config.class)) {
ReportService report = context.getBean(ReportService.class);
System.out.println("postCount=" + report.weeklyPostCount(41L));
System.out.println("proxy=" + AopUtils.isAopProxy(report));
}
}
}timed=weeklyPostCount nanos=(0 이상의 값)
postCount=12
proxy=true후처리기 빈 팩토리 메서드를 정적으로 두면 구성 인스턴스를 너무 일찍 생성하는 생명 주기 경고를 피할 수 있습니다.
사용자 정의 처리기가 다른 빈을 조회하면 조기 초기화와 순환 의존이 생길 수 있어 의존성을 최소화합니다.
자동 프록시 생성
Spring 인프라는 BeanFactory의 어드바이저를 조회하고 각 빈 클래스와 메서드에 포인트컷이 맞는지 검사합니다.
하나라도 맞으면 프록시를 만들고 적용 가능한 어드바이저를 순서에 따라 체인에 넣습니다.
맞지 않으면 객체 동일성과 시작 비용을 보존하기 위해 원본을 반환합니다.
DefaultAdvisorAutoProxyCreator는 어드바이저 기반 학습에 유용하고 AspectJ 애노테이션을 처리하는 생성기는 애스펙트 메타데이터를 어드바이저로 변환합니다.
여러 자동 프록시 생성기를 임의로 동시에 등록하지 않고 프레임워크가 적절한 인프라를 승격하도록 둡니다.
어드바이저 자체, Advice, Pointcut, AopInfrastructureBean은 다시 프록시하지 않습니다.
사용자 정의 생성기를 만들 때도 이 제외 규칙을 지키지 않으면 무한 래핑이나 시작 실패가 생깁니다.
이 인프라 제외 설명을 모든 @Configuration 클래스가 자동으로 같은 방식으로 제외된다는 규칙으로 넓히지는 않습니다. 연습에서 구성 빈까지 프록시하지 않으려면 애플리케이션 메서드의 실제 패키지·타입·애노테이션 조건이 구성 빈과 맞지 않도록 확인합니다. 사용자 TimingPostProcessor에는 인프라 타입 제외가 직접 구현돼 있지 않으므로 운영 자동 생성기와 구별합니다. 빈 후처리기 확장점은 인스턴스 처리와 이른 후처리기 등록 범위를 설명합니다.
조기 참조와 순환 의존성
순환 의존성을 해결하려고 대상의 조기 참조가 다른 빈에 먼저 주입되고 나중에 프록시가 만들어지면 일부 호출자는 원본, 일부는 프록시를 볼 수 있습니다.
Spring 자동 프록시 생성기는 조기 프록시 참조를 다루는 훅을 갖지만 순환 구조를 정당화하는 이유는 아닙니다.
생성자 주입과 책임 분리로 순환을 제거합니다.
지연 주입으로 숨기면 문제가 드러나는 시점이 런타임의 첫 호출로 옮겨질 수 있습니다.
트랜잭션·보안 어드바이스가 원본 참조에서 빠지는 것은 정확성 결함입니다.
이중 프록시도 관찰합니다.
서로 다른 처리기가 같은 빈을 연속으로 감싸면 어드바이저 순서와 대상 클래스 조회가 복잡해집니다.
AopUtils.isAopProxy, Advised.getAdvisors, 최종 대상 클래스를 진단할 때 확인하되 애플리케이션 로직은 프록시 내부를 뜯지 않습니다.
시작·실행 범위 검증
컨텍스트 테스트는 긍정 빈이 프록시인지, 부정 빈은 원본인지, 어드바이저 수와 순서가 맞는지 확인합니다.
동작 테스트는 메서드 호출에서 어드바이스와 대상이 각각 한 번인지, 예외 동일성과 반환 값이 유지되는지 봅니다.
운영 환경 시작 로그에 모든 빈 이름을 쏟지 않고 예상 프록시 개수와 핵심 보안 빈 적용 실패를 상태 확인 또는 시작 검증으로 제한합니다.
프록시 생성 시간이 시작 예산을 넘는다면 포인트컷 클래스 필터를 좁히고 런타임 매처를 줄입니다.
연습 문제
애노테이션 기반 어드바이저와 DefaultAdvisorAutoProxyCreator를 등록해 애플리케이션 패키지의 @Measured 메서드만 자동 프록시로 만드세요.
어드바이저 빈과 구성 빈이 프록시되지 않고 대상 서비스는 한 번만 감싸지는지 검증하세요.
해설 보기
생성기와 어드바이저는 인프라 역할로 구분하고 포인트컷을 구체 애플리케이션 패키지로 좁힙니다.
아래 measuredAdvisor는 12-7에서 만든 board.advisor.MeasuredPointcut 빈이 같은 컨텍스트에 등록되어 있다는 전제입니다. @Bean 팩토리 메서드에서 new board.advisor.MeasuredPointcut()을 반환하도록 먼저 등록하세요. 해당 빈이 없으면 생성기 동작을 확인하기 전에 어드바이저의 의존성 등록부터 해결해야 합니다.
package board.autoproxy;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class AutoProxyConfiguration {
@Bean
static DefaultAdvisorAutoProxyCreator autoProxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
@Bean
Advisor measuredAdvisor(board.advisor.MeasuredPointcut pointcut) {
MethodInterceptor advice = invocation -> invocation.proceed();
return new DefaultPointcutAdvisor(pointcut, advice);
}
}실제 테스트에서는 Advised로 어드바이저 수를 읽되 운영 환경 코드가 이 형 변환에 의존하지 않게 합니다.
예시 ReportTarget T를 원 처리기로 한 번 감싸 P1을, P1을 다시 같은 처리기로 감싸 P2를 만들었다고 정합니다. P2.weeklyPostCount(41)의 호출 진입은 P2의 어드바이스 → P1의 어드바이스 → T 순서입니다. timed 출력은 대상 반환 뒤 finally가 풀리는 안쪽 P1, 바깥 P2 순서로 이어져 기록 2회·업무 대상 호출 1회입니다. 두 번 프록시했다는 것과 대상을 두 번 실행했다는 것은 다릅니다. 바깥 P2에서 getAdvisors만 보면 그 팩토리가 넣은 어드바이저 하나이고, 그것만으로 안쪽 P1이 없다고 증명하지는 못합니다.
독립 확인: 위 P2와 처음 보관한 T를 각각 한 번 호출합니다. 원 처리기의 표시는 그대로 있고 각 어드바이스는 proceed를 한 번만 부릅니다. 전체 기록 횟수와 대상 횟수를 구하세요. 표시 인터페이스가 없는 다른 빈 U가 후처리기에 들어가면 반환 객체의 동일성은 어떻게 되나요?
풀이 확인
P2 호출은 기록 2회·대상 1회, T 직접 호출은 기록 0회·대상 1회이므로 합계는 기록 2회·대상 2회입니다. U는 if의 조건이 거짓이어서 같은 U 객체를 반환합니다. proxy=true 한 값만으로 모든 호출의 적용 수나 이중 포장 여부까지 알 수 없으므로, 최종 참조·부정 빈 동일성·개별 호출 수를 따로 검증합니다. 순환 의존의 조기 참조는 이 정상 after-initialization 추적과 별도의 수명 주기 문제입니다.