본문 바로가기

BeanPostProcessor

빈 후처리기가 초기화된 객체를 프록시로 교체하는 과정을 실행하고 자동 적용·조기 참조·이중 프록시 문제를 검증합니다.

빈마다 ProxyFactory를 호출하면 래퍼 클래스의 반복은 사라져도, 구성 코드의 반복은 남습니다. 빈 후처리기(BeanPostProcessor)는 컨테이너가 만드는 빈을 초기화 전후에 받아 보고, 컨테이너가 최종적으로 노출할 참조를 다른 객체로 바꿀 수 있는 확장 지점입니다.

Spring의 자동 프록시 생성기(auto-proxy creator)는 이 확장 지점을 써서, 등록된 어드바이저와 각 빈을 대조한 뒤 필요한 대상만 프록시로 감쌉니다. 그래서 횡단 관심을 애플리케이션 코드와 빈 연결 코드 모두에서 떼어 낼 수 있습니다. 이 글에서는 학습용 후처리기를 직접 만들어 본 뒤, 자동 생성기가 무엇을 더 해 주는지와 조기 참조·이중 프록시 문제를 확인합니다.

BeanPostProcessor

초기화한 bean 판별 뒤 proxy 또는 원 참조를 선택해 최종 노출하며 target 자체가 proxy로 변하지 않는다.

BEAN LIFECYCLE · CONDITIONAL FINAL REFERENCE

초기화 후 조건에 맞는 빈의 최종 참조를 바꾼다

BeanPostProcessor는 컨테이너 수명주기의 확장점입니다. 일반적인 초기화 후 경로에서 대상이 준비되면 조건을 판정해 프록시 또는 원본을 반환합니다.

사용자 TimingPostProcessor의 표시 검사와 최종 참조 분기일반after-initialization에서초기화된bean이TimedComponent면newProxyFactory로새proxy를반환하고아니면같은bean을반환한다. 최종노출참조를바꾸는것이며원target객체자체를proxy로변환하는것이아니다. 이사용자if는인프라자동생성기의Advisor/Advice/Pointcut제외검사를구현하지않으며중복이미proxy검사도없다. 두branch는최종참조노출에서합류하고조기순환참조는별도다.초기화된 빈TimedComponent에해당하는가?새 프록시 반환같은 빈 반환최종 참조 노출예아니요원 사용자 처리기의 marker 조건 · 중복 포장 검사는 없음
사용자 TimingPostProcessor의 표시 검사와 최종 참조 분기일반after-initialization에서초기화된bean이TimedComponent면newProxyFactory로새proxy를반환하고아니면같은bean을반환한다. 최종노출참조를바꾸는것이며원target객체자체를proxy로변환하는것이아니다. 이사용자if는인프라자동생성기의Advisor/Advice/Pointcut제외검사를구현하지않으며중복이미proxy검사도없다. 두branch는최종참조노출에서합류하고조기순환참조는별도다.초기화된 빈TimedComponent표시가 있는가?새 프록시 반환같은 빈 반환최종 참조 노출예아니요원 사용자 처리기 조건중복 포장 검사는 없음
초기화된 빈의 후처리와 proxy 또는 원본 선택생성, 이 빈의 의존성 주입, 초기화를 마친 대상이 초기화 후 처리기에 들어간다. 적용 대상이면 proxy를 반환하고 불일치 또는 인프라 빈이면 원본을 반환한다. 컨테이너는 최종 참조를 노출한다. 조기 순환 참조는 별도 고려사항이다.준비된 target생성 → 주입 → 초기화초기화 후 처리조건 판정적용 대상Proxy 반환최종 노출 참조불일치 / 인프라원본 반환동일 객체 유지
  1. target 생성 → 주입 → 초기화

    이 빈 자신의 property population 뒤 초기화가 완료됩니다. 일반적인 after-initialization 경로를 보여줍니다.

  2. 처리기 조건 판정 → 최종 참조

    대상이면 proxy, 불일치/인프라면 원본을 반환합니다. 컨테이너가 선택된 최종 참조를 노출합니다.

학습용 처리기와 자동 생성기

원문의 TimingPostProcessor는 TimedComponent 표시만 보고 중복 포장 검사가 없습니다. 인프라 자동 생성기는 적합한 advisor·포인트컷을 찾고 인프라 타입을 제외합니다.

참조와 적용 수 검증

조기 참조/순환 의존으로 원본과 프록시가 나뉘지 않게 구조를 정리합니다. proxy 여부뿐 아니라 advisor 수·순서·대상 호출 수와 이중 래핑을 확인합니다.

새 프록시를 반환해도 원본 target 자체의 자기 호출은 우회됩니다. 사용자 처리기의 조기 빈 조회와 여러 생성기 중복 등록을 피합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 일반 after-initialization에서 초기화된 bean이 TimedComponent면 new ProxyFactory로 새 proxy를 반환하고 아니면 같은 bean을 반환한다. 최종 노출 참조를 바꾸는 것이며 원래 target 객체 자체를 proxy로 변환하는 것이 아니다. 이 사용자 if는 인프라 자동 생성기의 Advisor/Advice/Pointcut 제외 검사를 구현하지 않으며 이미 proxy인지 확인하는 중복 검사도 없다. 두 branch는 최종 참조 노출에서 합류하고 조기 순환 참조는 별도다.
  • 생성, 이 빈의 의존성 주입, 초기화를 마친 대상이 초기화 후 처리기에 들어간다. 적용 대상이면 proxy를 반환하고 불일치 또는 인프라 빈이면 원본을 반환한다. 컨테이너는 최종 참조를 노출한다. 조기 순환 참조는 별도 고려사항이다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1BeanPostProcessor
4BeanPostProcessor는 컨테이너 수명 주기의 확장점입니다. 일반적인 초기화 후 경로에서 대상이 준비되면 조건을 판정해 프록시 또는 원본을 반환합니다.
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는 컨테이너 수명주기의 확장점입니다. 일반적인 초기화 후 경로에서 대상이 준비되면 조건을 판정해 프록시 또는 원본을 반환합니다.

사용자 TimingPostProcessor의 표시 검사와 최종 참조 분기일반after-initialization에서초기화된bean이TimedComponent면newProxyFactory로새proxy를반환하고아니면같은bean을반환한다. 최종노출참조를바꾸는것이며원target객체자체를proxy로변환하는것이아니다. 이사용자if는인프라자동생성기의Advisor/Advice/Pointcut제외검사를구현하지않으며중복이미proxy검사도없다. 두branch는최종참조노출에서합류하고조기순환참조는별도다.초기화된 빈TimedComponent에해당하는가?새 프록시 반환같은 빈 반환최종 참조 노출예아니요원 사용자 처리기의 marker 조건 · 중복 포장 검사는 없음
사용자 TimingPostProcessor의 표시 검사와 최종 참조 분기일반after-initialization에서초기화된bean이TimedComponent면newProxyFactory로새proxy를반환하고아니면같은bean을반환한다. 최종노출참조를바꾸는것이며원target객체자체를proxy로변환하는것이아니다. 이사용자if는인프라자동생성기의Advisor/Advice/Pointcut제외검사를구현하지않으며중복이미proxy검사도없다. 두branch는최종참조노출에서합류하고조기순환참조는별도다.초기화된 빈TimedComponent표시가 있는가?새 프록시 반환같은 빈 반환최종 참조 노출예아니요원 사용자 처리기 조건중복 포장 검사는 없음
초기화된 빈의 후처리와 proxy 또는 원본 선택생성, 이 빈의 의존성 주입, 초기화를 마친 대상이 초기화 후 처리기에 들어간다. 적용 대상이면 proxy를 반환하고 불일치 또는 인프라 빈이면 원본을 반환한다. 컨테이너는 최종 참조를 노출한다. 조기 순환 참조는 별도 고려사항이다.준비된 target생성 → 주입 → 초기화초기화 후 처리조건 판정적용 대상Proxy 반환최종 노출 참조불일치 / 인프라원본 반환동일 객체 유지
  1. target 생성 → 주입 → 초기화

    이 빈 자신의 property population 뒤 초기화가 완료됩니다. 일반적인 after-initialization 경로를 보여줍니다.

  2. 처리기 조건 판정 → 최종 참조

    대상이면 proxy, 불일치/인프라면 원본을 반환합니다. 컨테이너가 선택된 최종 참조를 노출합니다.

학습용 처리기와 자동 생성기

원문의 TimingPostProcessor는 TimedComponent 표시만 보고 중복 포장 검사가 없습니다. 인프라 자동 생성기는 적합한 advisor·포인트컷을 찾고 인프라 타입을 제외합니다.

참조와 적용 수 검증

조기 참조/순환 의존으로 원본과 프록시가 나뉘지 않게 구조를 정리합니다. proxy 여부뿐 아니라 advisor 수·순서·대상 호출 수와 이중 래핑을 확인합니다.

새 프록시를 반환해도 원본 target 자체의 자기 호출은 우회됩니다. 사용자 처리기의 조기 빈 조회와 여러 생성기 중복 등록을 피합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 일반 after-initialization에서 초기화된 bean이 TimedComponent면 new ProxyFactory로 새 proxy를 반환하고 아니면 같은 bean을 반환한다. 최종 노출 참조를 바꾸는 것이며 원래 target 객체 자체를 proxy로 변환하는 것이 아니다. 이 사용자 if는 인프라 자동 생성기의 Advisor/Advice/Pointcut 제외 검사를 구현하지 않으며 이미 proxy인지 확인하는 중복 검사도 없다. 두 branch는 최종 참조 노출에서 합류하고 조기 순환 참조는 별도다.
  • 생성, 이 빈의 의존성 주입, 초기화를 마친 대상이 초기화 후 처리기에 들어간다. 적용 대상이면 proxy를 반환하고 불일치 또는 인프라 빈이면 원본을 반환한다. 컨테이너는 최종 참조를 노출한다. 조기 순환 참조는 별도 고려사항이다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1BeanPostProcessor
4BeanPostProcessor는 컨테이너 수명 주기의 확장점입니다. 일반적인 초기화 후 경로에서 대상이 준비되면 조건을 판정해 프록시 또는 원본을 반환합니다.
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 참조 노출을 구분합니다.

그림은 일반적인 초기화 후(after-initialization) 단계에서, 초기화를 마친 빈이 TimedComponent 표시를 가졌는지 판정하는 흐름입니다. 표시가 있으면 new ProxyFactory로 만든 새 프록시를, 없으면 같은 빈을 반환하고, 두 갈래는 컨테이너가 최종 참조를 노출하는 칸에서 합류합니다. 바뀌는 것은 노출되는 참조이지 원래 대상 객체 자체가 프록시로 변하는 것은 아닙니다. 이 사용자 처리기는 인프라 자동 생성기가 하는 어드바이저·어드바이스·포인트컷 제외 검사나, 이미 감싼 빈인지 확인하는 검사를 구현하지 않았으며, 순환 의존의 조기 참조는 별도의 경로입니다.


빈 후처리기의 확장 지점

컨테이너는 인스턴스 생성, 의존성 주입, Aware 콜백, 초기화 전 처리기, 초기화 콜백, 초기화 후 처리기를 거쳐 빈 참조를 노출합니다. 프록시는 보통 초기화 후 단계에서, 완성된 대상을 감싸 반환하는 방식으로 만들어집니다.

모든 빈을 무조건 프록시로 만들면 구성 클래스, 어드바이저, 후처리기 자신에게까지 재귀적으로 적용될 수 있습니다. 인프라 타입이나 포인트컷과 맞지 않는 빈은 원본을 그대로 반환합니다. 중요한 보안 범위를 빈 이름 문자열만으로 정하지도 않습니다.

src/main/java/board/autoproxy/TimingPostProcessor.java
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 { }
}

표시 인터페이스(marker interface)는 학습을 위한 선택 기준입니다. 운영 코드에서는 어드바이저의 포인트컷과 인프라의 자동 프록시 생성기를 써서, 타입·애노테이션·메서드 조건을 따로 표현합니다.

이 TimingPostProcessor의 if는 표시 인터페이스만 검사합니다. 반환 참조가 바뀌어도 원래 대상의 메모리 객체가 프록시로 바뀌는 것은 아니므로, 컨테이너에서 받은 최종 참조로 호출해야 부가 기능을 거칩니다. 따로 보관해 둔 원래 대상 참조로 부르면 대상이 바로 실행됩니다.

또 예제의 ReportTarget은 ReportService와 TimedComponent를 모두 구현합니다. new ProxyFactory(bean)이 만든 인터페이스 프록시에도 이 표시가 남으므로, 같은 처리기를 두 번 적용하면 한 번 감싼 프록시를 또 감쌉니다. 한 번 감쌌다는 이유로 이 if가 거짓이 되리라 기대하지 않습니다.


최종 프록시 주입

다음 컨텍스트는 사용자 정의 후처리기를 정적 빈으로 먼저 등록하고, 표시가 있는 서비스를 프록시로 바꿉니다. 클라이언트는 프록시가 만들어졌다는 사실을 몰라도, 인터페이스 호출이 측정됩니다.

src/main/java/board/autoproxy/PostProcessorDemo.java
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));
        }
    }
}

getBean(ReportService.class)가 돌려준 참조는 프록시이므로, 호출하면 대상이 반환된 뒤 finally가 측정 줄을 먼저 출력하고, 이어서 main이 결과와 프록시 여부를 출력합니다. 경과 나노초는 실행마다 다르므로 값 대신 조건으로 적었습니다.

PostProcessorDemo 관찰
timed=weeklyPostCount nanos=(0 이상의 값)
postCount=12
proxy=true

후처리기의 @Bean 팩토리 메서드를 static으로 두면, 후처리기를 만들려고 구성 클래스의 인스턴스를 너무 일찍 생성하는 일을 피할 수 있습니다. 사용자 정의 처리기가 다른 빈을 조회하면 그 빈이 일찍 초기화되어 다른 후처리기의 처리를 받지 못하거나 순환 의존이 생길 수 있으므로, 처리기의 의존성은 최소로 둡니다.

더 깊이: 후처리기가 일찍 만든 빈은 왜 프록시가 되지 않을까

Spring 레퍼런스의 컨테이너 확장 지점은 그 이유를 시작 순서로 설명합니다. ApplicationContext는 시작할 때 특별한 단계를 두어 모든 BeanPostProcessor와 그것이 직접 참조하는 빈을 먼저 만들고, 그다음 후처리기들을 정렬해 등록한 뒤 나머지 빈에 적용합니다. 자동 프록시 생성도 후처리기 하나로 구현되어 있으므로, 이 이른 단계에서 만들어진 빈에는 아직 등록되지 않은 후처리기가 적용될 수 없습니다. 그래서 후처리기가 주입받은 서비스는 @Transactional이나 애스펙트가 붙어 있어도 프록시가 되지 않습니다.

이런 빈이 생기면 Spring Framework 7.0의 컨테이너는 WARN 수준으로 Bean 'someBean' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 같은 로그를 남깁니다. 시작 로그에서 이 문장이 보이면, 트랜잭션이나 보안 어드바이스가 빠진 빈이 있다는 신호로 읽고 그 빈을 일찍 끌어당긴 후처리기의 의존성을 찾습니다. 같은 문서는 @Bean으로 후처리기를 선언할 때 반환 타입을 구현 클래스나 최소한 BeanPostProcessor 인터페이스로 두라고도 합니다. 반환 타입이 그보다 넓으면 컨테이너가 빈을 끝까지 만들기 전에는 후처리기인지 알아볼 수 없기 때문입니다.


자동 프록시 생성

Spring의 인프라는 BeanFactory에서 어드바이저를 조회하고, 각 빈의 클래스와 메서드에 포인트컷이 맞는지 검사합니다. 하나라도 맞으면 프록시를 만들고, 적용할 수 있는 어드바이저를 순서대로 체인에 넣습니다. 하나도 맞지 않으면 객체의 동일성과 시작 비용을 지키기 위해 원본을 반환합니다.

DefaultAdvisorAutoProxyCreator는 어드바이저 기반으로 동작을 익히기에 좋고, AspectJ 애노테이션을 처리하는 생성기는 애스펙트의 메타데이터를 어드바이저로 바꿔 같은 방식으로 적용합니다. 여러 자동 프록시 생성기를 임의로 함께 등록하지 말고, 필요한 기능에 맞는 생성기로 프레임워크가 올려 쓰도록 둡니다.

자동 생성기는 Advisor, Advice, Pointcut, AopInfrastructureBean 타입의 빈을 다시 프록시하지 않습니다. 사용자 정의 생성기를 만들 때도 이 제외 규칙을 지키지 않으면 무한히 감싸거나 시작에 실패할 수 있습니다. 다만 이 인프라 제외 규칙이 모든 @Configuration 클래스를 자동으로 제외한다는 뜻은 아닙니다. 구성 빈까지 프록시하지 않으려면 포인트컷의 패키지·타입·애노테이션 조건이 구성 빈과 맞지 않는지 확인합니다. 앞의 TimingPostProcessor에는 인프라 타입 제외가 구현돼 있지 않다는 점에서도 운영용 자동 생성기와 다릅니다. 빈 후처리기 확장 지점 문서는 처리기가 다루는 인스턴스의 범위와, 후처리기를 일찍 등록하는 규칙을 설명합니다.


조기 참조와 순환 의존성

순환 의존성을 풀기 위해 대상의 조기 참조(early reference)가 다른 빈에 먼저 주입되고 프록시가 나중에 만들어지면, 어떤 호출자는 원본을, 어떤 호출자는 프록시를 보게 될 수 있습니다. Spring의 자동 프록시 생성기는 조기 프록시 참조를 다루는 훅을 갖고 있지만, 그것이 순환 구조를 그대로 둘 이유는 아닙니다.

생성자 주입과 역할 분리로 순환을 없앱니다. 지연 주입으로 순환을 감추면 문제가 드러나는 시점이 런타임의 첫 호출로 미뤄질 수 있습니다. 원본 참조로 들어온 호출에서 트랜잭션이나 보안 어드바이스가 빠지는 것은 기능의 정확성에 관한 결함입니다.

이중 프록시도 확인합니다. 서로 다른 처리기가 같은 빈을 연달아 감싸면 어드바이저의 순서와 대상 클래스 조회가 복잡해집니다. 진단할 때는 AopUtils.isAopProxy, Advised.getAdvisors, 최종 대상 클래스를 확인하되, 애플리케이션 로직이 프록시 내부를 뜯어보게 만들지는 않습니다.


시작과 실행 범위 검증

컨텍스트 테스트는 적용되어야 할 빈이 프록시인지, 적용되지 않아야 할 빈은 원본인지, 어드바이저의 수와 순서가 맞는지 확인합니다. 동작 테스트는 메서드 호출에서 어드바이스와 대상이 각각 한 번씩 실행되는지, 예외의 동일성과 반환값이 유지되는지 봅니다.

운영 환경의 시작 로그에 모든 빈 이름을 쏟아 내지 말고, 예상한 프록시 개수와 핵심 보안 빈의 적용 실패만 상태 확인이나 시작 검증으로 드러냅니다. 프록시 생성 시간이 시작 시간 예산을 넘는다면 포인트컷의 클래스 필터를 좁히고 런타임 매처를 줄입니다.


정리

  • 빈 후처리기는 초기화된 빈을 받아 컨테이너가 노출할 참조를 바꿀 수 있으며, 프록시는 주로 초기화 후 단계에서 만들어집니다.
  • 참조만 바뀔 뿐 원래 대상 객체는 그대로이므로, 컨테이너에서 받은 최종 참조로 호출해야 부가 기능을 거칩니다.
  • 자동 프록시 생성기는 어드바이저와 빈을 대조해 맞는 빈만 감싸고, 인프라 타입은 제외합니다.
  • 조기 참조와 이중 프록시는 원본과 프록시가 섞이거나 부가 기능이 두 번 실행되게 하므로, 구조를 정리하고 호출 수로 검증합니다.

연습 문제

1. @Measured 메서드의 자동 프록시

애노테이션 기반 어드바이저와 DefaultAdvisorAutoProxyCreator를 등록해, 애플리케이션 패키지의 @Measured 메서드만 자동 프록시로 만드세요. 어드바이저 빈과 구성 빈은 프록시되지 않고, 대상 서비스는 한 번만 감싸지는지 검증하세요.

해설 보기

생성기와 어드바이저는 인프라 역할로 구분하고, 포인트컷은 구체적인 애플리케이션 패키지로 좁힙니다. 아래 measuredAdvisor는 ProxyFactory 구성에서 만든 board.advisor.MeasuredPointcut이 같은 컨텍스트에 빈으로 등록되어 있다고 전제합니다. @Bean 팩토리 메서드에서 new board.advisor.MeasuredPointcut()을 반환하도록 먼저 등록하고, 그 빈이 없으면 생성기의 동작보다 어드바이저의 의존성부터 해결합니다. 다만 MeasuredPointcut은 상위 클래스 StaticMethodMatcherPointcut의 기본 클래스 필터 ClassFilter.TRUE를 그대로 써서 패키지를 제한하지 않습니다. 앞 글 연습 1의 해설이 말한 대로 클래스 필터를 애플리케이션 패키지로 좁혀야 이 연습의 조건을 채우며, 예를 들어 @Bean 메서드에서 만든 인스턴스에 setClassFilter로 board와 그 하위 패키지만 받는 필터를 지정한 뒤 반환할 수 있습니다.

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로 형 변환해 어드바이저 수를 읽을 수 있지만, 운영 코드가 이 형 변환에 의존하지 않게 합니다. 어드바이저 빈과 구성 빈은 AopUtils.isAopProxy가 거짓인지, 대상 서비스는 참이면서 어드바이저가 하나인지 확인합니다.

2. 같은 처리기로 두 번 감싸면

ReportTarget T를 TimingPostProcessor로 한 번 감싸 P1을 만들고, P1을 같은 처리기로 다시 감싸 P2를 만들었습니다. P2.weeklyPostCount(41)을 한 번 호출할 때 어드바이스와 대상에 들어가는 순서, timed 줄이 출력되는 순서와 횟수, 대상 호출 횟수를 구하세요. P2에서 getAdvisors로 읽은 어드바이저 수만 보고 이중 포장이 없다고 판단할 수 있는지도 답하세요.

해설 보기

호출은 P2의 어드바이스, P1의 어드바이스, T 순서로 들어갑니다. 대상이 반환되면 finally가 안쪽부터 풀리므로 P1의 timed 줄, P2의 timed 줄 순서로 출력되어 기록은 2회, 업무 대상 호출은 1회입니다. 두 번 감쌌다는 것과 대상을 두 번 실행했다는 것은 다른 이야기입니다.

P2의 getAdvisors는 P2를 만든 팩토리가 넣은 어드바이저 하나만 보여 줍니다. 그러니 그 값만으로 안쪽에 P1이 없다고 판단할 수 없으며, 대상을 따라 들어가며 확인하거나 기록 횟수를 세어야 합니다.

3. 원래 참조와 표시 없는 빈

연습 2의 P2와, 처음에 따로 보관한 T를 각각 한 번 호출합니다. 처리기의 표시 조건은 그대로이고 각 어드바이스는 proceed()를 한 번만 부릅니다. 전체 기록 횟수와 대상 호출 횟수를 구하세요. 표시 인터페이스가 없는 다른 빈 U가 후처리기에 들어가면, 반환되는 객체는 U와 어떤 관계인가요?

해설 보기

P2 호출은 기록 2회와 대상 1회, T를 직접 부른 호출은 기록 0회와 대상 1회이므로 합계는 기록 2회와 대상 2회입니다. U는 if 조건이 거짓이어서 같은 U 객체가 그대로 반환됩니다.

proxy=true 같은 값 하나로는 모든 호출의 적용 횟수나 이중 포장 여부까지 알 수 없습니다. 최종 참조, 적용되지 않아야 할 빈의 동일성, 호출별 횟수를 따로 검증합니다. 순환 의존의 조기 참조는 이 정상적인 초기화 후 경로와는 별개의 생명 주기 문제입니다.

이것으로 12장을 마칩니다. 12장에서는 싱글톤의 공유 필드가 동시 요청에서 섞이는 문제와 ThreadLocal의 수명 관리에서 시작해, 반복되는 트레이스 코드를 템플릿 메서드와 전략·콜백으로 모으고, 프록시와 데코레이터, 동적 프록시, ProxyFactory, 빈 후처리기로 대상 코드를 고치지 않고 부가 기능을 끼워 넣는 단계까지 살펴봤습니다. 다음 장의 횡단 관심과 AOP부터는 이 프록시 위에서 동작하는 Spring AOP의 용어와 @Aspect 구성, 포인트컷 표현식을 차례로 다룹니다.