관리 메뉴

나구리의 개발공부기록

빈 후처리기(소개/예제/적용/정리), 스프링이 제공하는 빈 후처리기, 하나의 프록시/여러 Advisor 적용 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 고급편

빈 후처리기(소개/예제/적용/정리), 스프링이 제공하는 빈 후처리기, 하나의 프록시/여러 Advisor 적용

소소한나구리 2024. 11. 11. 16:42

출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 빈 후처리기

1) 소개

(1) 일반적인 스프링 빈 등록

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

(2) 빈 후처리기 - BeanPostProcessor

  • 스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶을 때 빈 후처리기를 사용
  • 빈 포스트 프로세서(BeanPostProcessor)를 번역하여 빈 후처리기라고 하며, 빈을 생성한 후에 이미지의 2번(등록)의 상황에서 무언가 처리하는 용도로 사용함
  • 빈 후처리의 기능은 매우 막강한데 객체를 조작하는 것을 넘어서 완전히 다른 객체로 바꿔치기 하는 것도 가능함

(3) 다른 객체로 바꿔치는 빈 후처리기

  • 1. 생성: 스프링 빈 대상이 되는 객체를 생성함(@Bean, @ComponentScan모두 포함)
  • 2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달
  • 3. 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기 할 수 있음
  • 4. 등록: 빈 후처리기는 빈을 반환함, 전달된 빈을 그대로 반환하면 빈이 등록되고 바꿔치기하면 다른 객체가 빈 저장소에 등록됨

좌) 일반적인 빈 후처리 작업 / 우) 객체 자체를 바꾸는 빈 후처리 작업

2) 예제1 - 일반적인 스프링빈 등록 과정

(1) BasicTest

  • test하위에 postprocessor패키지를 생성 후 작성
  • new AnnotationConfigApplicationContext(BasicConfig.class)로 스프링 컨테이너를 생성하면서 BasicConfig.class를 넘겨주었기 때문에 설정 파일이 스프링 빈으로 등록됨
  • 아래의 테스트 케이스를 작성하면 너무도 당연하게도 A는 스프링 빈으로 등록되어 메서드를 출력할 수 있지만, B는 스프링 빈으로 등록되지 않았기 때문에 NoSuchBeanDefinitionException 예외가 발생함
package hello.proxy.postprocessor;

public class BasicTest {

    @Test
    void basicConfig() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(BasicConfig.class);
        // A는 빈으로 등록됨
        A a = ac.getBean("beanA", A.class);
        a.helloA();

        // B는 빈으로 등록안됨
        assertThatThrownBy(() -> ac.getBean(B.class)).isInstanceOf(NoSuchBeanDefinitionException.class);
    }

    @Slf4j
    @Configuration
    static class BasicConfig {

        @Bean(name = "beanA")
        public A a() {
            return new A();
        }
    }

    @Slf4j
    static class A {

        public void helloA() {
            log.info("helloA");
        }
    }

    @Slf4j
    static class B {

        public void helloB() {
            log.info("helloB");
        }
    }
}

3) 예제2 - 빈 후처리기 사용

  • 빈 후처리기를 통해서 A객체를 B객체로 바꾸기

(1) BeanPostProcessor 인터페이스 - 스프링 제공

  • 빈 후처리기를 사용하려면 BeanPostProcessor인터페이스를 구현하고 스프링 빈으로 등록하면되며 라이프 사이클에 따라 다르게 호출되는 두가지 메서드가 있음
  • postProcessBeforeInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서
  • postProcessAfterInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 이후에 호출되는 포스트 프로세서
package org.springframework.beans.factory.config;

public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

 

(2) BeanPostProcessorTest 

  • AToBPostProcessor: BeanPostProcessor인터페이스를 구현한 빈 후처리기, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작함
  • 파라미터로 넘어온 bean객체가 A객체이면 새로운 B객체로 바꿔서 반환되도록 동작하여 B가 스프링 컨테이너에 등록됨
  • 테스트를 실행해보면 AtoBPostProcessor로 넘어온빈의 이름은 beanA, Bean은 A가 넘어가지만 후처리기에서 A가 B로 변경되어 최종적으로는 B가 반환되었고 A는 스프링빈으로 등록조차 되지 않음
package hello.proxy.postprocessor;

public class BeanPostProcessorTest {

    @Test
    void basicConfig() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
        // beanA의 이름으로 B 객체가 빈으로 등록됨
        B b = ac.getBean("beanA", B.class);
        b.helloB();

        // A는 빈으로 등록안됨
        assertThatThrownBy(() -> ac.getBean(A.class)).isInstanceOf(NoSuchBeanDefinitionException.class);
    }

    @Slf4j
    @Configuration
    static class BeanPostProcessorConfig {

        @Bean(name = "beanA")
        public A a() {
            return new A();
        }

        @Bean
        public AToBPostProcessor helloPostProcessor() {
            return new AToBPostProcessor();
        }
    }

    // classA, classB 코드 동일

    @Slf4j
    static class AToBPostProcessor implements BeanPostProcessor {

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            log.info("beanName={} bean={}", beanName, bean);

            // bean에 A가 들어오면 B를 생성해서 반환
            if (bean instanceof A) {
                return new B();
            }
            return bean;
        }
    }
}

/*
10:57:02.075 [Test worker] WARN org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker -- Bean 'beanPostProcessorTest.BeanPostProcessorConfig' of type [hello.proxy.postprocessor.BeanPostProcessorTest$BeanPostProcessorConfig$$SpringCGLIB$$0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). The currently created BeanPostProcessor [helloPostProcessor] is declared through a non-static factory method on that class; consider declaring it as static instead.
10:57:02.082 [Test worker] INFO hello.proxy.postprocessor.BeanPostProcessorTest$AToBPostProcessor -- beanName=beanA bean=hello.proxy.postprocessor.BeanPostProcessorTest$A@6865c751
10:57:02.091 [Test worker] INFO hello.proxy.postprocessor.BeanPostProcessorTest$B -- helloB
*/

 

(3) 정리

  • 빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트이며 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있음
  • 여기서 조작이라는 것은 해당 객체의 특정 메서드를 호출하는 것을 뜻함
  • 일반적으로 스프링컨테이너가 등록하는(특히 컴포넌트 스캔의 대상이 되는)빈들은 중간에 조작할 방법이 없는데 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있음
  • 즉, 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻임

** 참고 - @PostConstruct의 비밀

  • @PostConstruct는 스프링 빈 생성 이후에 빈을 초기화하는 역할을 하는데 빈의 초기화라는 것은 단순히 @PostConstruct 애노테이션이 붙은 초기화메서드를 한번 호출만 하면됨
  • 쉽게 말해 생성된 빈을 한번 조작하는 것이며, 빈을 조작하는 적절한 빈 후처리기가 있다는 뜻임
  • 스프링은 CommonAnnotationBeanPostProcessor라는 빈 후처리기를 자동으로 등록하는데 여기에서 @PostConstruct애노테이션이 붙은 메서드를 호출함
  • 즉, 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용함

4) 빈 후처리기 - 적용

(1) 적용 그림

  • 빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링빈으로 등록하여 수동으로 등록하는 빈과 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용
  • 더 나아가서 설정 파일에 있는 수많은 프록시 생성코드도 한번에 제거가 가능함

 

(2) PackageLogTraceProxyPostProcessor - 빈후처리기 생성

  • config하위 패키지에 v4_postprocessor.postprocessor 패키지를 생성하여 코드를 작성
  • 원본 객체를 프록시 객체로 바꿔주는 역할을 하는 후처리기이며, 프록시 생성은 프록시 팩토리를 사용하는데 프록시 팩토리는 advisor가 필요하기 때문에 advisor를 외부에서 주입받도록 작성
  • 빈이 모두 생성된 이후에 후처리기가 동작하도록 postProcessAfterInitialization()를 구현
  • 모든 스프링 빈들에 프록시를 적용할 필요는 없으므로 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용할 수 있도록 basePackage 변수를 선언하여 파라미터로 넘어온 bean의 패키지이름과 외부 주입받은 basePackage가 다르면 프록시가 적용되지 않고 원본 객체를 그대로 반환하도록 작성
  • 프록시 적용 대상의 반환값은 프록시 팩토리로 생성된 프록시를 반환하여 스프링 컨테이너에 원본 객체 대신에 프록시 객체가 스프링빈으로 등록되며 원본 객체는 스프링 빈으로 등록되지 않음
package hello.proxy.config.v4_postprocessor.postprocessor;

@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().getPackage().getName();

        // 지정한 패키지의 bean이 아니면 bean을 그대로 반환
        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;
    }
}

 

(3) BeanPostProcessorConfig - 설정파일 생성

  • @Import()로 V1과 V2를 수동 빈 등록하는 설정파일들을 빈등록, V3는 컴포넌트 스캔으로 자동으로 스프링 빈으로 등록되기 때문에 설정파일이 없어도 됨
  • ProxyApplication에서 @Import로 한꺼번에 등록해도 되며 예제에서는 설정파일의 구분을 위해서 편의상 여기에 등록하였음
  • @Bean logTraceProxyPostProcessor : 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록하고, 빈 후처리기를 생성할 때 프록시를 적용할 패키지 정보(hello.proxy.app)과 어드바이저(getAdvisor(logTrace))를 넘겨줌
  • getAdvisor()메서드에서 포인트컷과 어드바이스를 정의하여 어드바이저를 반환
  • 설정파일을 보면 프록시를 생성하는 코드가 설정파일에서 사라져 순수한 빈 등록만 고민하면 됨
  • 프록시를 생성하고 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해줌

 

package hello.proxy.config.v4_postprocessor;

@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class}) // v3는 ComponentScan 사용으로 설정등록을 안해도 됨
public class BeanPostProcessorConfig {

    @Bean
    public PackageLogTraceProxyPostProcessor logTraceProxyPostProcessor(LogTrace logTrace) {
        return new PackageLogTraceProxyPostProcessor("hello.proxy.app", getAdvisor(logTrace));
    }

    private Advisor getAdvisor(LogTrace logTrace) {
        // pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");

        // advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

 

(4) ProxyApplication 수정 및 실행

  • 애플리케이션에서 최종적으로 설정파일을 @Import하여 애플리케이션을 실행하면 스프링부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기를 통과하는 로그를 확인할 수 있음
  • 여기에 모두 프록시를 적용하는것은 올바르지 않기 때문에 필요한 곳에 프록시를 적용하도록 basePackage 변수를 활용해서 v1 ~ v3 애플리케이션 관련 빈들만 프록시 적용 대상이 되도록 했음
  • 로그를 확인해보면 v1은 인터페이스가 있으므로 JDK 동적 프록시가 적용되고, v2, v3는 구체 클래스만 있어 CGLIB 프록시가 적용되었음
  • v1,v2,v3의 매핑된 url로 요청 파라미터를 보내보면 수동으로 등록한 빈 뿐만아니라 컴포넌트 스캔을 통해 등록한 v3 빈들도 프록시가 적용되어 정상적으로 로그 추적기가 동작하는 로그를 확인할 수 있음
@Import(BeanPostProcessorConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
    // 기존 코드 생략
}

좌) v1 ~ v3 애플리케이션이 프록시로 생성된 로그 / 우) v1 ~ v3 애플리케이션 모두 로그추적기가 정상 동작하는 로그

 

(5) 프록시 적용 대상 여부 체크

  • 애플리케이션을 실행한 로그를 확인해보면 알겠지만 직접 등록한 스프링 빈들 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어옴
  • 그래서 어떤 빈을 프록시로 만들 것인지 기준이 필요하며 여기서는 간단히 basePackage를 사용하여 특정 패키지를 기준으로 해당 패키지와 그 하위 패키지의 빈들을 프록시를 만듦
  • 스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있기에 모든 객체를 프록시를 만들 경우 오류가 발생하여 꼭 프록시를 만들 대상을 구분해주어야 하며, 스프링은 이를 훨씬 편리하게 해결하는 기능을 제공함

5) 빈 후처리기 - 정리

(1) 문제 해결

  • 빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있음
  • 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있음
  • 덕분에 애플리케이션에 수 많은 스프링빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도되어 많았던 설정 코드가 줄어듦
  • 그리고 컴포넌트 스캔을 사용해도 프록시가 모두 적용됨

** 중요

  • 프록시의 적용 대상 여부를 여기서는 간단히 패키지를 기준으로 설정했는데 포인트컷을 사용하는 것이 더 깔끔하게 적용할 수 있음
  • 포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에 프록시 적용 대상 여부를 정밀하게 설정할 수 있음
  • 어드바이저는 포인트것을 이미 가지고 있기 때문에 어드바이저를 통해 포인트컷을 확인할 수 있으며 이후에 배울 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크함
  • 결과적으로 포인트컷은 아래의 두 곳에 사용됨
  • 1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용 (빈 후처리기 - 자동 프록시 생성)
  • 2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단 (프록시 내부)

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

1) 설정 및 설명

(1) build.gradle 설정 추가

  • 해당 라이브러리를 추가하면 aspectjweaver라는 AspectJ관련 라이브러리를 등록하고 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록함
  • 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy를 직접 상용해야 했는데 이 부분을 스프링 부트가 자동으로 처리해줌
  • 스프링부트가 활성화하는 빈은 AopAutoConfiguration을 참고하면 되며 AspectJ는 뒤에서 설명함
implementation 'org.springframework.boot:spring-boot-starter-aop'

 

(2) 자동 프록시 생성기 - AutoProxyCreator

  • 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기가 스프링 빈에 자동 등록되며 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기임
  • 해당 빈 후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해줌
  • Advisor안에는 Pointcut과 Advice가 이미 모두 포함되어있으므로 Advisor만 알고 있으면 그 안에있는 Pointcut으로 어떤 스프링 빈에 프록시를 적용해야할지 알 수 있음

** 참고

  • AnnotationAwareAspectJAutoProxyCreator는 @AspectJ와 관련된 AOP 기능도 자동으로 찾아서 처리해줌
  • Advisor는 물론 @Aspect도 자동으로 인식해서 프록시를 만들고 AOP를 적용해줌, @Aspect에 대한 내용도 뒤에서 다룸

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

  1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성
  2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처라기에 전달
  3. 모든 Advisor빈 조회: 자동 프록시 생성기 - 빈후처리기가 스프링 컨테이너에서 모든 Advisor를 조회함
  4. 프록시 적용 대상 체크: 조회한 Advisor에 포함되어있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단, 이때 객체의 클래스 정보와 해당 객체의 모든 메서드를 포인트컷에 하나씩 매칭해보고 조건이 하나라도 만족하면 프록시 적용 대상이 됨, 즉 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이됨
  5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록하고 프록시 적용 대상이 아니라면 원본 객체를 반환하여 원본 객체를 스프링 빈으로 등록함
  6. 빈 등록: 반환된 객체는 스프링 빈으로 등록됨

좌) 자동 프록시 생성기 동작과정 / 우) 생성된 프록시 구조

2) 적용

(1) AutoProxyConfig

  • config패키지 하위에 v5_autoproxy 패키지를 생성 후 코드 작성
  • 이미 라이브러리를 추가하여 스프링이 제공하는 빈 후처리기를 스프링 빈으로 등록했기때문에 설정파일에서는 어드바이저만 등록해주면 설정이 끝남
package hello.proxy.config.v5_autoproxy;

@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);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

 

(2) ProxyApplication 수정 및 실행

  • @Import로 위에서 작성한 설정파일을 등록한 후 실행 후 매핑된 URL로 요청 파라미터를 보내보면 정상적으로 로그 추적기가 동작하는 것을 확인할 수 있음
@Import(AutoProxyConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
    // 기존 코드 생략
}

3) 중요 : 포인트 컷은 2가지에 사용됨 - 반복 강조

(1) 프록시 적용 여부 판단 - 생성 단계

  • 자동 프록시 생성기는 포인트 컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크
  • 클래스와 메서드 조건을 포인트컷 조건 하나하나와 매칭하여 조건에 맞는 것이 하나라도 있으면 프록시를 생성함
  • 만약 조건에 맞는 것이 하나도 없으면 프록시를 생성할 필요가 없으므로 프록시를 생성하지 않음
  • 예제에서 orderControllerV1은 request(), noLog()가 있는데 request()가 조건에 만족하므로 프록시를 생성함

(2) 어드바이스 적용 여부 판단 - 사용 단계

  • 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단함
  • 예제에서 orderControllerV1은 이미 프록시가 걸려있고 request()는 포인트컷 조건에 만족하므로 프록시는 어드바이스를 먼저 호출하고 target을 호출함
  • 그러나 noLog()는 포인트컷 조건에 만족하지 않으므로 어드바이스를 호출하지 않고 target만 호출함

** 참고

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

4) 적용2

(1) 애플리케이션 로딩 로그 확인

  • 방금 적용된 애플리케이션의 로딩 로그를 보면 스프링이 초기화 되면서 기대하지 않은 로그들이 함께 출력됨
  • 그 이유는 지금 사용한 포인트컷이 단순히 메서드 이름에 request*, order*, save*만 포함되어있으면 매칭 된다고 판단하기 때문임
  • 스프링 내부에서 사용하는 빈에도 메서드 이름에 request, order, save라는 단어가 들어가있으면 프록시가 만들어지고 어드바이스가 적용이 되고있는 상황임
  • 즉, 패키지에 메서드 이름까지 함께 지정할 수 있는 정밀한 포인트컷이 필요함

(2) AspectJExpressionPointcut

  • AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있으며 실무에서는 이 포인트컷만 사용함
  • 지금은 특별한 표현식으로 복잡한 포인트 컷을 만들 수 있다는 정도로만 이해하고 AspectJ포인트컷 표현식과 AOP에 대한 내용은 뒤에서 자세히 설명함

(3) AutoProxyConfig - advisor2 추가

  • 기존에 등록한 advisor1의 @Bean은 주석처리하고 AspectJExpressionPointcut 포인트컷을 사용하는 advisor2를 스프링 빈으로 등록
  • AspectJ포인트컷 표현식을 적용할 수 있으며 맨처음 *은 모든 반환타입, hello.proxy.app.. 은 해당 패키지와 그 하위 패키지, *(..)에서 *은 모든 메서드 이름, (..)은 파라미터는 상관없다는 뜻임
  • 쉽게 말해 hello.proxy.app 패키지와 그 하위 패키지의 모든 메서드는 포인트컷의 매칭 대상이 된다는 뜻임
@Bean
public Advisor advisor2(LogTrace logTrace) {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(* hello.proxy.app..*(..))");

    LogTraceAdvice advice = new LogTraceAdvice(logTrace);
    return new DefaultPointcutAdvisor(pointcut, advice);
}

 

(4) 실행 결과

  • 해당 어드바이저를 사용하니 기대하지 않았던 로그들은 사라졌으나 /v1/no-log로 접속했을 때에도 로그가 출력됨
  • advisor2에서 단순히 package를 기준으로 포인트 컷을 매칭했기 때문임

(5) AutoProxyConfig - advisor3 추가

  • AspectJ 포인트컷 표현식을 &&로 조합하여 noLog는 포함되지 않도록 작성하여 적용
  • advisor1과 advisor2에 있는 @Bean을 주석처리하고 애플리케이션을 실행 후 요청파라미터로 요청을 보내보면 no-log는 로그가 나오지 않는 것을 확인할 수 있음 
@Bean
public Advisor advisor3(LogTrace logTrace) {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(* hello.proxy.app..*(..)) " +
            "&& !execution(* hello.proxy.app..noLog(..))");

    LogTraceAdvice advice = new LogTraceAdvice(logTrace);
    return new DefaultPointcutAdvisor(pointcut, advice);
}

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

(1) 프록시 팩토리가 생성하는 프록시는 프록시를 하나만 생성함

  • 스프링 빈이 advisor1, advisor2가 제공하는 포인트컷의 조건을 모두 만족하면 프록시 자동 생성기는 프록시를 하나만 생성함
  • 프록시 팩토리가 생성하는 프록시는 내부에 여러 advisor들을 포함할 수 있기 때문에 프록시를 여러개 생성해서 낭비할 이유가 없음

(2) 프록시 자동 생성기 상황별 정리

  • advisor1의 포인트컷만 만족 -> 프록시 1개 생성, 프록시에 advisor1만 포함
  • advisor1, advisor2의 포인트컷을 모두 만족 -> 프록시 1개 생성, 프록시에 advisor1, advisor2 모두 포함
  • advisor1, advisor2의 포인트컷을 모두 만족하지 않음 -> 프록시가 생성되지 않음
  • 이후에 설명할 스프링 AOP도 위와 동일한 방식으로 동작함

(3) 정리

  • 자동 프록시 생성기인 AnnotationAwareAspectJAutoProxyCreator 덕분에 개발자는 매우 편리하게 프록시를 적용할 수 있어 Advisor만 스프링 빈으로 등록하면 됨
  • @Aspect 애노테이션을 사용하면 더 편리하게 포인트컷과 어드바이스를 만들고 프록시를 적용할 수 있음(다음강의)