예제 코드2 - 직접 만든 포인트컷
포인트컷 요구사항
save() 메서드 -> 어드바이스 로직 적용
find() 메서드 -> 어드바이스 로직을 적용X
Pointcut 관련 인터페이스 (스프링 제공)
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
//..
}
- ClassFilter: 클래스 조건으로 필터링
- MethodFilter: 메서드 조건으로 필터링
둘다 true 로 반환해야 어드바이스를 적용할 수 있다.
일반적으로 스프링이 이미 만들어둔 구현체를 사용하지만 개념 학습 차원에서 간단히 직접 구현해보자.
직접 만든 포인트컷
static class MyPointcut implements Pointcut{
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
@Slf4j
static class MyMethodMatcher implements MethodMatcher {
private String matchName = "save";
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals(matchName);
log.info("포인트컷 호출 method={}, targetClass={}", method.getName(), targetClass);
log.info("포인트컷 결과 result={}", result);
return result;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
MyPointcut
- Pointcut 인터페이스를 구현한다.
- 현재 메서드 기준으로 로직을 적용하면 된다. 클래스 필터는 항상 true 를 반환하도록 했고, 메서드 비교 기능은 MyMethodMatcher 를 사용한다.
MyMethodMatcher
- MethodMatcher 인터페이스를 구현한다.
- matches() : 이 메서드에 method , targetClass 정보가 넘어온다. 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단할 수 있다. 여기서는 메서드 이름이 "save" 인 경우에 true 를 반환하도록 판단 로직을 적용했다.
- isRuntime() , matches(... args) : isRuntime() 이 값이 참이면 matches(... args) 메서드가 대신 호출된다. 동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다. isRuntime() 이 false 인 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만, isRuntime() 이 true 인 경우 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.
포인트컷을 직접 구현할 일은 거의 없다. 참고만 하자.
테스트 코드
...
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
직접 만든 포인트컷을 어드바이저에 넣고 프록시를 통해 save() 와 find() 호출
실행 결과
#save() 호출
AdvisorTest - 포인트컷 호출 method=save targetClass=class
hello.proxy.common.service.ServiceImpl
AdvisorTest - 포인트컷 결과 result=true
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
#find() 호출
AdvisorTest - 포인트컷 호출 method=find targetClass=class
hello.proxy.common.service.ServiceImpl
AdvisorTest - 포인트컷 결과 result=false
ServiceImpl - find 호출
- save() : 어드바이스 적용 (TimeProxy)
- find() : 어드바이스 적용 X
실행 과정

1. 클라이언트 -> 프록시의 save() 호출
2. 포인트컷으로 Service 클래스 save() 메서드에 어드바이스를 적용할지 확인
3. 포인트컷이 true 반환 -> 어드바이스를 호출해서 부가 기능 적용
4. 이후 실제 인스턴스의 save() 호출

1. 클라이언트 -> 프록시의 find() 호출
2. 포인트컷으로 Service 클래스 find() 메서드에 어드바이스를 적용할지 확인
3. 포인트컷이 false 반환 -> 어드바이스 호출X, 부가 기능 적용X
4. 실제 인스턴스의 find() 호출
예제 코드3 - 스프링이 제공하는 포인트컷
스프링은 우리가 필요한 포인트컷을 이미 대부분 제공한다.
NameMatchMethodPointcut 테스트 코드
@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
ServiceImpl target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
NameMatchMethodPointcut 을 생성하고 setMappedNames(...) 으로 메서드 이름을 지정
"save" 지정 -> save() 메서드 호출시 어드바이스 적용
스프링이 제공하는 포인트컷
- NameMatchMethodPointcut : 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils 를 사용한다. 예) *xxx* 허용
- JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
- TruePointcut : 항상 참을 반환한다.
- AnnotationMatchingPointcut : 애노테이션으로 매칭한다.
- AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.
예제 코드4 - 여러 어드바이저 함께 적용
목표 하나의 target 에 여러 어드바이스를 적용하기
여러 프록시
- 하나의 프록시에 하나의 어드바이저를 적용
- 프록시가 다른 프록시를 호출
@Test
@DisplayName("여러 프록시")
void multiAdvisorTest1(){
//client -> proxy2(advisor2) -> proxy1(advisor1) -> target
//프록시1 생성 : target = ServiceImpl
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
//프록시2 생성 : target = proxy1
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();
//실행
proxy2.save();
}
target 이 다르기 때문에 ProxyFactory 를 각각 생성하여 프록시 생성

여러 프록시의 문제
적용해야 하는 어드바이저만큼 프록시를 생성해야 한다.
하나의 프록시, 여러 어드바이저
스프링은 이 문제를 해결하기 위해 하나의 프록시에 여러 어드바이저를 적용할 수 있게 했다.
- 프록시가 어드바이저를 호출
- 어드바이저가 다른 어드바이저를 호출
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2(){
//client -> proxy -> advisor2 -> advisor1 -> target
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
//프록시에 어드바이저를 등록한 순서대로 호출
proxyFactory.addAdvisor(advisor2);
proxyFactory.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
//실행
proxy.save();
}
- 프록시 팩토리에 원하는 만큼 addAdvisor() 를 통해서 어드바이저를 등록한다.
- 등록하는 순서대로 advisor 가 호출된다. ( invocation.proceed() )

정리
결과적으로 여러 프록시를 사용할 때와 비교해서 결과는 같고, 성능은 더 좋다.
중요
스프링의 AOP를 처음 공부하거나 사용하면, AOP 적용 수 만큼 프록시가 생성된다고 착각하게 된다.
스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
정리하면 하나의 target 에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target 마다 하나의 프록시만 생성한다.
프록시 팩토리 - 적용1
인터페이스가 있는 v1 애플리케이션에 LogTrace 기능을 프록시 팩토리를 통해서 프록시를 만들어 적용
LogTraceAdvice
@Slf4j
public class LogTraceAdvice implements MethodInterceptor {
//target 정보 필요 없음
private final LogTrace logTrace;
public LogTraceAdvice(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TraceStatus status = null;
try{
Method method = invocation.getMethod();
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
status = logTrace.begin(message);
//target 호출
Object result = invocation.proceed();
logTrace.end(status);
return result;
} catch (Exception e){
logTrace.exception(status, e);
throw e;
}
}
}
- LogTrace 를 호출하는 부분은 이전과 크게 다르지 않다.
- target 을 호출하는 부분 ( invocation.proceed() ) 과 target 을 따로 필드로 가지지 않는 점이 이전 코드와 다르다.
ProxyFactoryConfigV1 ( 수동 빈 등록 )
@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {
@Bean
public OrderRepositoryV1 orderRepository(LogTrace logTrace){
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
@Bean
public OrderServiceV1 orderService(LogTrace logTrace){
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepository(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderControllerV1 orderController(LogTrace logTrace){
OrderControllerV1 orderController = new OrderControllerV1Impl(orderService(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
private Advisor getAdvisor(LogTrace logTrace) {
//pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
- 포인트컷은 NameMatchMethodPointcut 을 사용한다. 여기에는 심플 매칭 기능이 있어서 * 을 매칭할 수 있다.
- request* , order* , save* : request, order, save 로 시작하는 메서드에 포인트컷은 true 를 반환한다.
- 이렇게 설정한 이유는 noLog() 메서드에는 어드바이스를 적용하지 않기 위해서다.
- 어드바이저는 포인트컷( NameMatchMethodPointcut ), 어드바이스( LogTraceAdvice )를 가지고 있다.
- 프록시 팩토리에 각각의 target 과 advisor 를 등록해서 프록시를 생성한다. 그리고 생성된 프록시를 스프링 빈으로 등록한다. ( return proxy )
애플리케이션 로딩 로그 ( in 스프링 빈 등록 )
ProxyFactory proxy=class com.sun.proxy.$Proxy50, target=class ...v1.OrderRepositoryV1Impl
ProxyFactory proxy=class com.sun.proxy.$Proxy52, target=class ...v1.OrderServiceV1Impl
ProxyFactory proxy=class com.sun.proxy.$Proxy53, target=class ...v1.OrderControllerV1Impl
V1 애플리케이션은 인터페이스가 있기 때문에 프록시 팩토리가 JDK 동적 프록시를 적용한다.
프록시 팩토리 - 적용2
구체 클래스만 있는 v2 애플리케이션에 LogTrace 기능을 프록시 팩토리를 통해서 프록시를 만들어 적용
V1 에 대한 설정에서 인터페이스로 된 참조 변수를 구체 클래스로 변경
애플리케이션 로딩 로그 ( in 스프링 빈 등록 )
ProxyFactory proxy=class hello.proxy.app.v2.OrderRepositoryV2$$EnhancerBySpringCGLIB$$594e4e8,
target=class hello.proxy.app.v2.OrderRepositoryV2
ProxyFactory proxy=class hello.proxy.app.v2.OrderServiceV2$$EnhancerBySpringCGLIB$$59e5130b,
target=class hello.proxy.app.v2.OrderServiceV2
ProxyFactory proxy=class hello.proxy.app.v2.OrderControllerV2$$EnhancerBySpringCGLIB$$79c0b9e,
target=class hello.proxy.app.v2.OrderControllerV2
V2 애플리케이션은 인터페이스가 없고 구체 클래스만 있기 때문에 프록시 팩토리가 CGLIB을 적용한다.
정리
프록시 팩토리 덕분에 개발자는 매우 편리하게 프록시를 생성할 수 있게 되었다.
추가로 어드바이저, 어드바이스, 포인트컷 이라는 개념 덕분에 어떤 부가 기능을 어디에 적용할 지 명확하게 이해할 수 있었다.
남은 문제
너무 많은 설정
ProxyFactoryConfigV1 , ProxyFactoryConfigV2 와 같은 설정 파일이 지나치게 많다.
스프링 빈에 프록시를 적용하기 위해 스프링 빈 별로 설정 코드가 필요하다. 설사 같은 기능이라고 해도 target 이 다르기 때문에 각각 프록시를 생성해서 수동으로 프록시를 적용해야 한다.
최근에는 스프링 빈을 등록하기 귀찮아서 컴포넌트 스캔까지 사용하는데 말이다!
컴포넌트 스캔
애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능하다.
왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.
지금까지 학습한 프록시를 적용하려면, 실제 객체를 스프링 컨테이너에 빈으로 등록하는 것이 아니라 부가 기능이 있는 프록시를 스프링 컨테이너에 등록해야 한다.
두 가지 문제를 한번에 해결하는 방법 => 빈 후처리기
7. 빈 후처리기
빈 후처리기 - 소개

@Bean 이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용한다.
빈 후처리기 ( BeanPostProcessor )
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다. 빈 후처리기를 사용하면 객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다.

1. 생성: 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
3. 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
4. 등록: 빈 후처리기는 빈을 반환한다. 전달 된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.
빈 후처리기 - 예제 코드
빈 후처리기를 사용하여 A 객체를 B 객체로 바꿔치기 하여 스프링 컨테이너에 등록

빈 이름은 그대로, 객체만 바꿔치기 한다.
BeanPostProcessor 인터페이스 - 스프링 제공
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}
- 빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다. postProcessBeforeInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서이다.
- postProcessAfterInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서이다.
AToBPostProcessor ( BeanPostProcessor 구현체 )
@Slf4j
static class AToBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={}, bean={}", beanName, bean);
if(bean instanceof A){
return new B();
}
return bean;
}
}
이 빈 후처리기는 A 객체를 새로운 B 객체로 바꿔치기 한다. 파라미터로 넘어오는 빈( bean ) 객체가 A 의 인스턴스이면 새로운 B 객체를 생성해서 반환한다. 여기서 A 대신에 반환된 값인 B 가 스프링 컨테이너에 등록된다.
BeanPostProcessorConfig ( 수동 빈 등록 )
@Slf4j
@Configuration
static class BeanPostProcessorConfig{
@Bean(name="beanA")
public A a(){
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor(){
return new AToBPostProcessor();
}
}
빈 후처리기가 먼저 등록되어 다른 빈들이 생성되고 스프링 컨테이너에 등록될 때 작동한다.
테스트 코드
@Test
void postProcessorTest() {
//스프링 컨테이너 생성 (스프링 빈 등록)
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
//A를 스프링 빈으로 등록할 때 helloPostProcessor 에 의해 "beanA" 라는 이름으로 B가 등록됨
B b = applicationContext.getBean("beanA", B.class);
b.helloB();
//A는 스프링 빈으로 등록되지 않음
Assertions.assertThrows(NoSuchBeanDefinitionException.class,
() -> applicationContext.getBean(A.class));
}
정리
빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트이다.
이것은 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다.
여기서 조작이라는 것은 해당 객체의 특정 메서드를 호출하는 것을 뜻한다.
일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다.
이 말은 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻이다.
참고: @PostConstruct의 비밀
@PostConstruct 는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다. 그런데 생각해보면 빈의 초기화 라는 것이 단순히 @PostConstruct 애노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다. 쉽게 이야기해서 생성된 빈을 한번 조작하는 것이다.
스프링은 CommonAnnotationBeanPostProcessor 라는 빈 후처리기를 자동으로 등록하는데, 여기에서 @PostConstruct 애노테이션이 붙은 메서드를 호출한다. 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다.
빈 후처리기 - 적용
빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록해보자.
이렇게 하면 수동으로 등록하는 빈은 물론이고, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다.
더 나아가서 설정 파일에 있는 수 많은 프록시 생성 코드도 한번에 제거할 수 있다.

PackageLogTracePostProcessor
- 패키지 단위로 프록시 적용 대상 여부 체크
- LogTrace 어드바이저를 호출할 프록시 객체를 생성하고 실제 객체 대신 반환
@Slf4j
public class PackageLogTracePostProcessor implements BeanPostProcessor {
private final String basePackage;
private final Advisor advisor;
public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
this.basePackage = basePackage;
this.advisor = advisor;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("param beanName={}, bean={}", beanName, bean.getClass());
//프록시 적용 대상 여부 체크
//프록시 적용 대상이 아니면 원본을 그대로 스프링 빈으로 등록
String packageName = bean.getClass().getPackageName();
if(!packageName.startsWith(basePackage)){
return bean;
}
//프록시 적용 대상이면 프록시를 만들어서 반환
ProxyFactory factory = new ProxyFactory(bean);
factory.addAdvisor(advisor);
Object proxy = factory.getProxy();
log.info("create proxy: target={}, proxy={}", bean.getClass(), proxy.getClass());
return proxy;
}
}
- PackageLogTraceProxyPostProcessor 는 원본 객체를 프록시 객체로 변환하는 역할을 한다. 이때 프록시 팩토리를 사용하는데, 프록시 팩토리는 advisor 가 필요하기 때문에 이 부분은 외부에서 주입 받도록 했다.
- 모든 스프링 빈들에 프록시를 적용할 필요는 없다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용한다. 프록시를 적용하지 않을 객체들은 원본 객체를 그대로 반환한다. 이때 반환된 객체가 스프링 컨테이너에 등록된다.
BeanPostProcessorConfig ( 수동 빈 등록 )
@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {
@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace){
return new PackageLogTracePostProcessor("hello.proxy.app", getAdviser(logTrace));
}
private Advisor getAdviser(LogTrace logTrace) {
//Pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//Advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//Advisor
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
- @Import({AppV1Config.class, AppV2Config.class}) : V3는 컴포넌트 스캔으로 자동으로 스프링 빈으로 등록되지만, V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다. ProxyApplication 에서 등록해도 되지만 편의상 여기에 등록하자. 이 설정 파일은 컴포넌트 스캔 패키지에 포함되지 않으므로 ProxyApplication 에서 @Import 하여 적용
- @Bean logTraceProxyPostProcessor() : 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다. 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다. 여기에 프록시를 적용할 패키지 정보( hello.proxy.app )와 어드바이저( getAdvisor(logTrace) )를 넘겨주기 위해 수동 빈 등록을 사용했다.
- 이제 프록시를 생성하는 코드가 설정 파일에는 필요 없다. 순수한 빈 등록만 고민하면 된다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
애플리케이션 로딩 로그
#v1 애플리케이션 프록시 생성 - JDK 동적 프록시
create proxy: target=v1.OrderRepositoryV1Impl proxy=class com.sun.proxy.$Proxy50
create proxy: target=v1.OrderServiceV1Impl proxy=class com.sun.proxy.$Proxy51
create proxy: target=v1.OrderControllerV1Impl proxy=class com.sun.proxy.$Proxy52
#v2 애플리케이션 프록시 생성 - CGLIB
create proxy: target=v2.OrderRepositoryV2 proxy=v2.OrderRepositoryV2$$EnhancerBySpringCGLIB$$x4
create proxy: target=v2.OrderServiceV2 proxy=v2.OrderServiceV2$$EnhancerBySpringCGLIB$$x5
create proxy: target=v2.OrderControllerV2 proxy=v2.OrderControllerV2$$EnhancerBySpringCGLIB$$x6
#v3 애플리케이션 프록시 생성 - CGLIB
create proxy: target=v3.OrderRepositoryV3 proxy=3.OrderRepositoryV3$$EnhancerBySpringCGLIB$$x1
create proxy: target=v3.orderServiceV3 proxy=3.OrderServiceV3$$EnhancerBySpringCGLIB$$x2
create proxy: target=v3.orderControllerV3 proxy=3.orderControllerV3$$EnhancerBySpringCGLIB$$x3
여기서는 생략했지만, 실행해보면 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기를 통과하는 것을 확인할 수 있다.
- v1: 인터페이스가 있으므로 JDK 동적 프록시가 적용된다.
- v2: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
- v3: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
프록시 적용 대상 여부 체크
애플리케이션을 실행해서 로그를 확인해보면 알겠지만, 우리가 직접 등록한 스프링 빈들 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다. 그래서 어떤 빈을 프록시로 만들 것인지 기준이 필요하다. 여기서는 간단히 basePackage 를 사용해서 특정 패키지를 기준으로 해당 패키지와 그 하위 패키지의 빈들을 프록시로 만든다.
스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다. 따라서 모든 객체를 프록시로 만들 경우 오류가 발생한다.
빈 후처리기 - 정리
문제 해결
빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있다. 프록시 팩토리에 의해 인터페이스가 있든, 없든 동일한 방식으로 프록시를 생성하고, 프록시 적용 범위와 어드바이저를 외부에서 받아서 하나의 빈 후처리기로 모든 빈에 대해 프록시 적용 여부를 확인하고 프록시로 바꿔치기 하는 과정을 자동화 했다.
그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다.
덕분에 애플리케이션에 수 많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 된다. 그리고 컴포넌트 스캔을 사용해도 프록시가 모두 적용된다.
스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다.
중요
프록시의 적용 대상 여부를 간단히 패키지를 기준으로 설정했다. 그런데 잘 생각해보면 포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다.
참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다.
스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.
결과적으로 포인트컷은 다음 두 곳에 사용된다.
1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)