[고급편] 빈 후처리기
포스트
취소

[고급편] 빈 후처리기

빈 후처리기 - 소개

일반적인 스프링 빈으로 등록

  • @Bean 이나 컴포넌트 스캔으로 스프링 빈을 등록하면,
    스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다.
  • 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.

빈 후처리기 (Bean PostProcessor)

  • 빈 후처리기는 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.
  • 스프링이 빈 저장소에 객체를 전달 및 등록하는 과정 중간에 빈 후처리기를 거쳐서 추가 처리를 진행하고, 그 다음에 빈 저장소에 전달된다.
  • 빈 후처리기는 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
    • postProcessBeforeInitialization
      • 객체 생성 이후에 @PostConstruct같은 초기화가 발생하기 전에 호출되는 포스트 프로세서
    • postProcessAfterInitialization
      • 객체 생성 이후에 @PostConstruct같은 초기화가 발생한 다음에 호출되는 포스트 프로세서
public interface BeanPostProcessor {
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

빈 후처리기 기능

  • 객체를 조작할 수도 있다.
  • 단순한 조작이 아닌 아예 다른 객체로 바꿔치기할 수도 있다.

빈 후처리기 동작 과정

  1. 생성
    • 스프링 빈 대상이 되는 객체를 생성한다.
    • @Bean 애노테이션이나 컴포넌트 스캔 모두 포함한다.
  2. 전달
    • 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 후 처리 작업
    • 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
  4. 등록
    • 빈 후처리기는 빈을 반환한다.
    • 전달 된 빈을 그대로 반환하면 해당 빈이 등록되고,
      바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

빈 후처리기 - 예제 코드1

  • 확실한 이해를 위해 우선 일반적인 스프링 빈 등록 과정을 확인해보자.

테스트 생성 및 실행

  • 간단한 스프링 빈 등록 과정이다.
  • new AnnotationConfigApplicationContext(BasicConfig.class)
    • 스프링 컨테이너를 생성하면서 BasicConfig.class를 넘겨주었다.
    • BasicConfig.class 설정 파일은 스프링 빈으로 등록된다.
package com.example.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BasicTest {
    @Test
    void basicConfig() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);

        //A는 빈으로 등록된다.
        A a = applicationContext.getBean("beanA", A.class);
        a.exampleA();

        //B는 빈으로 등록되지 않는다.
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(B.class));
    }

    @Slf4j
    @Configuration
    static class BasicConfig {
        @Bean(name = "beanA")
        public A a() {
            return new A();
        }
    }

    @Slf4j
    static class A {
        public void exampleA() {
            log.info("Excute exampleA");
        }
    }

    @Slf4j
    static class B {
        public void exampleB() {
            log.info("Excute exampleB");
        }
    }
}

빈 후처리기 - 예제 코드2

  • 이번에는 실제로 빈 후처리기를 사용해보자.

테스트 생성 및 실행

  • 첫번째 예제처럼 내부 클래스 A와 B가 있다.
  • 첫번째 예제와의 차이점은 이번에는 만약 빈으로 저장하려는 객체가 A라면 그걸 B로 치환시키는 빈 후처리기를 등록했다.
  • 인스턴스가 B가 되는 것이지 빈 이름은 처음에 설정한 빈 이름 그대로 저장된다.
package com.example.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import
        org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanPostProcessorTest {
    @Test
    void postProcessor() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);

        //beanA 이름으로 B 객체가 빈으로 등록된다.
        B b = applicationContext.getBean("beanA", B.class);
        b.helloB();

        //A는 빈으로 등록되지 않는다.
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.class));
    }

    @Slf4j
    @Configuration
    static class BeanPostProcessorConfig {
        @Bean(name = "beanA")
        public A a() {
            return new A();
        }

        /**
         * 빈 후처리기도 스프링 빈으로 등록해야 한다.
         */
        @Bean
        public AToBPostProcessor helloPostProcessor() {
            return new AToBPostProcessor();
        }
    }

    @Slf4j
    static class A {
        public void helloA() {
            log.info("hello A");
        }
    }

    @Slf4j
    static class B {
        public void helloB() {
            log.info("hello B");
        }
    }

    @Slf4j
    static class AToBPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            log.info("beanName={} bean={}", beanName, bean);
            //빈 저장소에 등록하려고한 클래스가 A라면 B로 치환한다.
            if (bean instanceof A) {
                return new B();
            }
            return bean;
        }
    }
}

빈 후처리기 정리

  • 빈 후처리기는 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다.
  • 여기서 조작은 해당 객체의 특정 메서드를 호출하는 것을 의미한다.
  • 일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없다.
  • 하지만 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다.
  • 모든 빈을 중간에 조작할 수 있다는 것은 무려 빈 객체를 프록시로 교체하는 것도 가능하다는 것을 의미한다.

@PostConstruct

  • @PostConstruct는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다.
    • 그저 @PostConstruct 애노테이션이 붙은 어노테이션을 호출하는 것만으로도 쉽게 생성된 빈을 한 번 조작할 수 있다.
  • 스프링은 CommonAnnotationBeanPostProcessor라는 빈 후처리기를 자동으로 등록한다.
    • 여기에서 @PostConstruct 애노테이션이 붙은 메서드를 호출한다.
    • 이것은 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다는 것을 의미한다.

빈 후처리기 - 적용

  • 이번에는 실제로 애플리케이션에 적용해보자.
  • 드디어 우리는 빈 후처리기를 통해 프록시를 생성하는 코드를 설정 파일에 집어넣을 필요가 없어졌다.
    • 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.

빈 후처리기

  • 원본 객체를 프록시 객체로 변환하는 역할의 빈 후처리기다.
  • postProcessAfterInitialization의 반환 값을 보면 원본 객체 대신에 프록시 객체를 반환한다.
    • 원본 객체는 스프링 빈으로 등록되지 않고, 프록시 객체가 스프링 빈으로 등록된다.
package com.example.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

@Slf4j
public class PackageLogTraceProxyPostProcessor implements BeanPostProcessor {
    private final String basePackage;
    private final Advisor advisor;

    public PackageLogTraceProxyPostProcessor(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 proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvisor(advisor);
        Object proxy = proxyFactory.getProxy();

        log.info("create proxy: target={} proxy={}", bean.getClass(), proxy.getClass());
        
        return proxy;
    }
}

환경설정

  • @Import({AppV1Config.class, AppV2Config.class})
    • v3는 컴포넌트 스캔을 통해서 자동으로 스프링 빈으로 등록된다.
    • 하지만 v1, v2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다.
    • Chapter2Application에서 등록해도 되지만 편의상 여기에 등록하자.
  • @Bean logTraceProxyPostProcessor()
    • 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다.
    • 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다.
package com.example.config;

import com.example.advice.LogTraceAdvice;
import com.example.postprocessor.PackageLogTraceProxyPostProcessor;
import com.example.trace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {
    @Bean
    public PackageLogTraceProxyPostProcessor logTraceProxyPostProcessor(LogTrace logTrace) {
        return new PackageLogTraceProxyPostProcessor("com.example.app", getAdvisor(logTrace));
    }

    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);
    }
}
package com.example;

import com.example.config.*;
import com.example.trace.LogTrace;
import com.example.trace.ThreadLocalLogTrace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

//@Import({AppV1Config.class, AppV2Config.class})
//@Import(InterfaceProxyConfig.class)
//@Import(ConcreteProxyConfig.class)
//@Import(DynamicProxyBasicConfig.class)
//@Import(DynamicProxyFilterConfig.class)
//@Import(ProxyFactoryConfigV1.class)
//@Import(ProxyFactoryConfigV2.class)
@Import(BeanPostProcessorConfig.class)
@SpringBootApplication(scanBasePackages = "com.example.app.v3") //컨트롤러때문에 생기는 충돌 방지
public class Chapter2Application {
    public static void main(String[] args) {
        SpringApplication.run(Chapter2Application.class, args);
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }
}

테스트

  • 드디어 떄가 왔다. 차례대로 v1, v2, v3를 실행해보자.
  • v1은 인터페이스가 있으므로 JDK 동적 프록시가 적용된다.
  • v2와 v3는 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
v1 (인터페이스 O)
  • localhost:8082/v1/request?itemId=test로 접속해보자.

    [ad3dd7b4] OrderControllerV1.request()
    [ad3dd7b4] |–>OrderServiceV1.orderItem()
    [ad3dd7b4] | |–>OrderRepositoryV1.save()
    [ad3dd7b4] | |<–OrderRepositoryV1.save() time=1014ms
    [ad3dd7b4] |<–OrderServiceV1.orderItem() time=1014ms
    [ad3dd7b4] OrderControllerV1.request() time=1014ms

v2 (인터페이스 X)
  • localhost:8082/v2/request?itemId=test로 접속해보자.

    [88baf58f] OrderControllerV2.request()
    [88baf58f] |–>OrderServiceV2.orderItem()
    [88baf58f] | |–>OrderRepositoryV2.save()
    [88baf58f] | |<–OrderRepositoryV2.save() time=1013ms
    [88baf58f] |<–OrderServiceV2.orderItem() time=1013ms
    [88baf58f] OrderControllerV2.request() time=1013ms

v3 (컴포넌트 스캔)
  • localhost:8082/v3/request?itemId=test로 접속해보자.

    [81355978] OrderControllerV3.request()
    [81355978] |–>OrderServiceV3.orderItem()
    [81355978] | |–>OrderRepositoryV3.save()
    [81355978] | |<–OrderRepositoryV3.save() time=1014ms
    [81355978] |<–OrderServiceV3.orderItem() time=1014ms
    [81355978] OrderControllerV3.request() time=1014ms

프록시 적용 대상 여부 체크

애플리케이션을 실행해서 로그를 확인해보면 알겠지만, 우리가

  • 직접 등록한 스프링 빈들 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다.
    • 애플리케이션을 실행해서 로그를 확인해보면 알 수 있다.
    • 이 점때문에 어떤 빈을 프록시로 만들 것인지 기준이 필요하다.
  • 스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다.
    • 그래서 모든 객체를 프록시로 만들 경우 오류가 발생한다.
  • 물론 스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공하고 있다.

스프링이 제공하는 빈 후처리기1

build.gradle

  • 이 라이브러리를 추가하면 aspectjweaver라는 aspectJ 관련 라이브러리를 등록하고,
    스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다.
  • 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy 를 직접 사용해야 했는데,
    이 부분을 스프링 부트가 자동으로 처리해준다.
implementation 'org.springframework.boot:spring-boot-starter-aop'

자동 프록시 생성기

  • 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.
  • 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.
  • 이 빈 후처리기는 스프링 빈으로 등록된 어드바이저들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
  • 어드바이저 안에는 이미 포인트컷과 어드바이스가 포함되어 있다.
    • 어드바이저만 알고 있으면 그 안에 있는 포인트컷으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다.
    • 어드바이스로 부가 기능을 적용하기만 하면 된다.
  • AnnotationAwareAspectJAutoProxyCreator는 @AspectJ와 관련된 AOP 기능도 자동으로 찾아서 처리해준다.
  • 어드바이저는 물론이고 @Aspect 애노테이션도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다.

자동 프록시 생성기의 작동 과정

  • 자동 프록시 생성기를 통해 생성된 프록시는 내부에 어드바이저와 실제 호출해야할 대상 객체(target)을 알고 있다.
  1. 생성
    • 스프링이 스프링 빈 대상이 되는 객체를 생성한다.
    • @Bean과 컴포넌트 스캔 모두 포함
  2. 전달
    • 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 모든 Advisor 빈 조회
    • 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회한다.
  4. 프록시 적용 대상 체크
    • 앞서 조회한 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다.
    • 이 때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다.
    • 조건이 하나라도 만족하면 프록시 적용 대상이 된다.
      • 즉, 조건이 10개가 있든지 100개가 있던지 그 중 하나만 만족해도 프록시 적용 대상이 된다.
  5. 프록시 생성
    • 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다.
    • 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
  6. 빈 등록
    • 반환된 객체는 스프링 빈으로 등록된다.

환경설정

  • 실제로 적용해보자.
  • AutoProxyConfig 코드를 보면 advisor1이라는 어드바이저 하나만 등록했다.
  • 빈 후처리기는 이제 등록하지 않아도 된다.
    • 스프링은 자동 프록시 생성기라는 빈 후처리기를 자동으로 등록해준다.
package com.example.config;

import com.example.advice.LogTraceAdvice;
import com.example.trace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
    @Bean
    public Advisor advisor1(LogTrace logTrace) {
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
package com.example;

import com.example.config.*;
import com.example.trace.LogTrace;
import com.example.trace.ThreadLocalLogTrace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

//@Import({AppV1Config.class, AppV2Config.class})
//@Import(InterfaceProxyConfig.class)
//@Import(ConcreteProxyConfig.class)
//@Import(DynamicProxyBasicConfig.class)
//@Import(DynamicProxyFilterConfig.class)
//@Import(ProxyFactoryConfigV1.class)
//@Import(ProxyFactoryConfigV2.class)
//@Import(BeanPostProcessorConfig.class)
@Import(AutoProxyConfig.class)
@SpringBootApplication(scanBasePackages = "com.example.app.v3") //컨트롤러때문에 생기는 충돌 방지
public class Chapter2Application {
    public static void main(String[] args) {
        SpringApplication.run(Chapter2Application.class, args);
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }
}

테스트

  • 아래 주소들에 접속해보면 실제로 잘 동작하는 것을 알 수 있다.
    • localhost:8082/v1/request?itemId=test
    • localhost:8082/v2/request?itemId=test
    • localhost:8082/v3/request?itemId=test

포인트컷의 사용 용도

  • 프록시 적용 여부 판단
    • 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
    • 클래스 + 메서드 조건을 모두 비교한다.
    • 이 때 ,모든 메서드를 체크하는데 포인트컷 조건에 하나하나 매칭해본다.
    • 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다.
  • 어드바이스 적용 여부 판단
    • 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다.

프록시도 결국 자원이다.

  • 프록시를 모든 곳에 생성하는 것은 비용 낭비이다.
  • 꼭 필요한 곳에 최소한의 프록시를 적용해야 한다.
  • 그래서 자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 것이 아니라,
    포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다.

스프링이 제공하는 빈 후처리기2

  • 직전에 만든 advisor1은 이름만 비교하기 때문에 원치 않은 로그가 올라올 때가 있다.
    • 스프링 내부에서 사용하는 빈에도 메소드명에 request라는 단어만 들어가있으면 프록시가 만들어지고 어드바이스도 적용된다.
  • 즉, 패키지에 메소드명까지 함께 지정할 수 있는 매우 정밀한 포인트컷이 필요하다.

AspectJExpressionPointcut

  • AspectJ라는 AOP에 특화된 포인트컷 표현식을 통해 이를 해결할 수 있다.
  • 아까 만든 AutoProxyConfig에 이번에는 패키지까지 필터링할 수 있는 어드바이저를 만들어보자.
    • 어드바이저가 중복되니 advisor1의 @Bean은 주석 처리하자.

어드바이저 개선 1

  • http://localhost:8082/v1/request?itemId=test로 접속해보면 패키지 필터링이 되는 것을 확인할 수 있다.
  • 문제는 http://localhost:8082/v1/no-log에서는 여전히 로그가 출력된다.
    • 단순히 package 를 기준으로 포인트컷 매칭을 했기 때문이다.
package com.example.config;

import com.example.advice.LogTraceAdvice;
import com.example.trace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
    //@Bean
    public Advisor advisor1(LogTrace logTrace) {
        System.out.println("Advisor 1");
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

    @Bean
    public Advisor advisor2(LogTrace logTrace) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* com.example.app..*(..))");
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

어드바이저 개선 2

  • 이번에는 패키지에 메소드명까지 필터링할 수 있는 어드바이저를 만들어보자.
    • 어드바이저가 중복되니 advisor2의 @Bean은 주석 처리하자.
package com.example.config;

import com.example.advice.LogTraceAdvice;
import com.example.trace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
    //@Bean
    public Advisor advisor1(LogTrace logTrace) {
        System.out.println("Advisor 1");
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

    //@Bean
    public Advisor advisor2(LogTrace logTrace) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* com.example.app..*(..))");
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

    @Bean
    public Advisor advisor3(LogTrace logTrace) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* com.example.app..*(..)) && !execution(* com.example.app..noLog(..))");
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        
        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

하나의 프록시, 여러 Advisor 적용

  • 만약 어드바이저가 10개가 있는데 어떤 스프링 빈이 모든 어드바이저의 포인트컷의 조건을 모두 만족하면 프록시 자동 생성기는 프록시를 몇 개 생성할까?
  • 정답은 1개다.
    • 프록시 자동 생성기는 프록시를 하나만 생성한다.
    • 왜냐하면 프록시 팩토리가 생성하는 프록시는 내부에 여러 어드바이저들을 포함할 수 있기 때문이다.
    • 따라서 프록시를 여러 개 생성해서 비용을 낭비할 이유가 없다.
  • 상황별 정리
    • 어드바이저1의 포인트컷만 만족
      • 프록시1개 생성, 프록시에 어드바이저1만 포함
    • 어드바이저1과 어드바이저2의 포인트컷을 모두 만족
      • 프록시1개 생성, 프록시에 어드바이저1과 어드바이저2 모두 포함
    • 어드바이저1과 어드바이저2의 포인트컷을 모두 만족하지 않음
      • 프록시가 생성되지 않음

출처

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.