프록시 - 빈후처리기

바그다드·2023년 9월 4일
0

지난 포스팅에서는 프록시 팩토리에 대해서 알아보았다.
프록시 팩토리에 어드바이저를 설정해주는 것만으로 프록시를 생성할 수 있게 되었다.

하지만 프록시 팩토리에도 단점이 있는데,

  1. 의존성을 설정해주는게 복잡하다는 것
  2. 컴포넌트 스캔에는 개입할 수 없다는 것

이번 포스팅에서는 이런 문제를 해결할 수 있는 빈 후처리기에 대해 알아보도록 하자.

빈 후처리기

스프링에서는 @Bean이나 @Component로 스프링 빈을 등록하고 이를 컨테이너에 저장한다.
빈 후처리기는 이 객체를 컨테이너에 저장하기 전에 조작하는 기능을 제공한다.

  • 빈 후처리기 동작 과정
  1. 스프링에서 스프링 빈이 되는 객체를 생성한다.
  2. 이 객체는 빈 후처리기에 전달된다.
  3. 빈 후처리기는 객체를 조작하거나 다른 객체로 변경하는 등의 동작을 한다.
  4. 빈 후처리기에서 반환된 객체를 빈 저장소에 동록한다.

바로 코드로 확인해보자.

빈 후처리기 예제

  • 테스트 코드로 먼저 확인해보자.
public class BeanPostProcessorTest {

    // 테스트 코드
    @Test
    void basicConfig() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);

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

        // A는 빈으로 등록되지 않는다.
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean("beanB"));
    }
    
    // 의존성 주입
    @Slf4j
    @Configuration
    static class BeanPostProcessorConfig {
        // beanA만 스프링 빈으로 등록
        @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;
        }
    }

}
  • 빈 후처리기를 생성하려면 BeanPostProcessor를 구현하고 스프링 빈으로 등록해주면 된다.
  • @Configuration 부분을 보면 스프링 빈으로 등록되는 객체는 A의 객체와 AToBPostProcessor(빈 후처리기)뿐이다.
    B는 단순히 클래스만 정의했을 뿐 따로 빈으로 등록하지 않았다.
    다만, AToBPostProcessor에서 파라미터로 넘어온 빈이 A의 인스턴스라면 B의 객체를 반환하는 로직을 짰다.
    테스트 결과를 확인해보면

    우리가 생성한 AToBPostProcessor가 작동하고 직접 등록한 A 대신 B가 스프링 빈으로 등록된 것을 확인할 수 있다.

BeanPostProcessor

빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하면 되는데, BeanPostProcessor 아래의 두 메서드를 디폴트 메서드로 정의하고 있다.

  1. postProcessBeforeInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기
    전에 호출되는 포스트 프로세서이다.
  2. postProcessAfterInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한
    다음에 호출되는 포스트 프로세서이다.

@PostConstruct

객체가 생성되고 난 후 객체를 초기화할 때 사용하는 어노테이션이다.
초기화는 한번만 하면 되므로 @PostConstruct는 한번 수행이되고 만다. 그런데 생각해보면 앞서 확인한 빈 후처리기가 이것과 유사한 역할을 한다.
@PostConstruct는 빈후처리기를 이용한 기능인데, 스프링은 CommonAnnotationBeanPostProcessor라는 빈 후처리기를 자동으로 등록해 여기서 @PostConstruct이 붙은 메서드를 호출하는 것이다.

빈 후처리기 적용

그렇다면 빈 후처리기를 이용한 로그 추적기의 코드는 어떻게 변할까?

빈 후처리기 생성

@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 proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvisor(advisor);

        Object proxy = proxyFactory.getProxy();
        log.info("create proxy: target={} proxy={}", bean.getClass(), proxy.getClass());
        return proxy;
    }
}
  • 빈 후처리기의 적용대상을 한정시키기 위해 메서드의 패키지 경로와 베이스 패키지 경로를 매칭하여 매칭이되지 않을 경우 원본 객체를 그대로 반환한다.
  • 매칭될 경우에는 프록시를 생성해 반환한다.

Config 생성

이제 필요한 객체를 빈을 등록해주자.

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {

    @Bean
    public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
        return new PackageLogTracePostProcessor("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);
    }
}
  • hello.proxy.app과 그 하위 패키지에만 프록시를 생성한다.
  • 이것으로 프록시 생성을 위한 준비는 끝났다.
    지난 포스팅에서 프록시 팩토리만 이용했을 때 해야했던 복잡한 설정 로직들이 깔끔하게 정리된 것을 확인할 수 있다.

이제 어플리케이션을 실행해보면

인터페이스를 이용한 컨트롤러는 JDK동적 프록시로
구현 클래스를 이용한 컨트롤러는 CGLIB로
컴포넌트 스캔을 이용한 컨트롤러는 CGLIB로
구현 기술에 관계없이 프록시를 생성한 것을 확인할 수 있다.

포인트컷

  • 여기서는 단순히 패키지 경로를 기준으로 프록시를 생성하였는데,
    포인트컷은 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에 포인트컷을 이용하면 더 정밀한 조절이 가능하다.
    포인트컷은 두가지 역할을 하는데,
  1. 프록시 적용 대상 여부 체크
  2. 메서드에 어드바이스 적용 여부 체크

빈 후처리기를 생성해 사용하는 것만으로도 많은 로직이 줄어든 것을 확인할 수 있는데, 스프링에서 제공하는 빈 후처리기를 이용하면 더 쉽게 프록시를 적용할 수 있다.

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

먼저 라이브러리를 추가해주자.

implementation 'org.springframework.boot:spring-boot-starter-aop'

이 라이브러리를 추가하면 스프링 부트는 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 빈으로 등록한다.
이름 그대로 스프링 빈으로 등록된 Advisor를 찾아서 프록시를 적용해준다.

  • Advisor 안에 포인트컷과 어드바이스가 포함되어 있기 때문에 어드바이저만으로 프록시 적용 여부를 알 수 있다.

자동 프록시 생성기의 동작 과정은 다음과 같다.

  1. 스프링이 빈 객체를 생성
  2. 객체를 빈 후처리기에 전달
  3. 빈 후처리기에서 컨테이너에 등록된 모든 Advisor를 조회
  4. Advisor의 포인트컷으로 프록시 적용 여부 확인
    • 클래스 정보 뿐만 아니라 모든 메서드 하나하나 매칭하여, 하나라도 조건이 만족하면 프록시를 적용
  5. 프록시 생성
  6. 컨테이너에 프록시 등록

그럼 코드가 어떻게 바뀌는지 확인해보자.

Config생성

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {

    /*
    * 메서드 이름이 매칭되는 빈은 모두 어드바이스가 적용됨
    * */
    @Bean
    public Advisor advisor1(LogTrace logTrace) {
        // pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        // advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
  • 어드바이저만 스프링 빈으로 등록해줬고, 따로 빈 후처리기를 등록하지는 않았다.
    대신 스프링에서 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 자동으로 등록해준다.
    이제 어플리케이션을 실행해보면

    로그 추적기가 정상적으로 적용된 것을 확인할 수 있다.
  • 다만 위의 코드는 문제가 있는데, 여기서 사용된 포인트컷은 단순히 메서드 이름이 포인트컷과 매칭되는 모든 메서드에 로그 추적기가 적용된다는 문제가 있다.
    여기서 포인트컷은 2가지 역할을 하는데,
  1. 프록시 적용 여부(생성 단계)
    • 클래스와 메서드의 모든 정보를 비교하여 하나라도 조건에 부합하면 프록시를 생성한다.
  2. 어드바이스 적용 여부(사용 단계)
    • 프록시가 호출되었을 때 어드바이스를 적용할지 판단한다.
    • 예를들어 위의 코드에서 request()라는 메서드가 호출된다면 어드바이스가 적용되지만,
      noLog()라는 메서드가 호출된다면 어드바이스는 적용되지 않는다.

앞선 포스팅에서 포인트컷의 종류에 대해 알아볼 때 가장 자주 쓰이는 것은 AspectJExpressionPointcut이라고 하였는데, 이 포인트컷을 이용하면 훨씬 정교한 포인트컷을 적용할 수 있다. 이건 다음에 자세하게 알아보자.

하나의 프록시, 여러 Advisor

하나의 타겟에 조건을 만족하는 Advisor가 2개 이상이라면 프록시는 몇개가 생성이 될까?
프록시는 하나만 생성을 한다.
업로드중..업로드중..
빈 후처리기는 컨테이너에 등록된 모든 어드바이저와 빈의 모든 정보를 매칭하여 하나의 조건이라도 부합하면 프록시를 생성한다.
이렇게 프록시 팩토리가 생성하는 프록시는 내부에 여러 개의 Advisor를 가질 수 있다.
따라서 포인트컷이 하나라도 만족하지 않으면 프록시를 생성하지 않고,
하나라도 만족한다면 프록시를 생성,
여러 어드바이저의 포인트컷을 만족한다면 하나의 프록시에 포인트컷에 만족하는 여러 어드바이저를 포함하게 된다.

정리

이것으로 빈 후처리기에 대해 알아보았다.

빈 후처리기는 스프링 빈을 생성하고, 컨테이너에 등록하는 과정 중간에서 생성된 객체를 조작하는 기능을 제공한다.
빈 후처리기를 생성하려면 BeanPostProcessor 인터페이스를 구현하고 빈으로 등록해주면 된다.

스프링은 빈 후처리기를 제공하는데, 라이브러리를 추가하면 스프링에서는 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 자동으로 등록해준다. 덕분에 어드바이저만 등록하면 자동 프록시 생성기에서 빈과 어드바이저를 매칭하여 프록시를 자동으로 생성해준다.

출처 : 김영한 - 스프링 핵심 원리 고급편

profile
꾸준히 하자!

0개의 댓글