빈 후처리기

땡글이·2023년 1월 25일
0

스프링 AOP

목록 보기
3/5

프록시 팩토리와 어드바이저를 활용해 프록시 객체를 동적으로 생성할 수 있었다. 하지만, 문제가 있다. 첫 번째는 설정 파일에 동적 프록시 생성 코드를 만들어야 한다는 점, 두 번째로는 컴포넌트 스캔으로 자동으로 스프링 DI 컨테이너에 등록되는 객체들을 앞서 살펴본 동적 프록시 기술을 통해 프록시 객체를 등록할 수 없다는 것이다.

프록시 팩토리와 어드바이저에 대한 포스팅은 해당 포스팅을 참고해주시기 바랍니다.

빈 후처리기 (Bean PostProcessor)

빈 후처리기는 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전 조작하고 싶을 때 사용되는 기술이다. 빈 후처리기는 객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기도 가능하다.

빈 후처리기 작동과정

  • 생성 : 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)
  • 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  • 후처리 작업 : 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔칠 수 있다. (위 그림은 다른 객체로 바꿔치기하는 과정)
  • 등록 : 빈 후처리기는 빈을 반환한다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

빈 후처리기 사용방법

직접 코드를 통해 살펴보자.

    @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(A.class));
    }

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

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

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

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

    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;
        }
    }
...
16:16:10.294 [Test worker] INFO hello.proxy.postprocessor.BeanPostProcessorTest - beanName=beanA bean=hello.proxy.postprocessor.BeanPostProcessorTest$A@2ca47471
16:16:10.345 [Test worker] INFO hello.proxy.postprocessor.BeanPostProcessorTest - hello B

직접 테스트코드로 결과를 확인해보니, beanA 라는 이름의 빈에 A 객체가 아닌, B 객체가 저장되어 있는 것을 확인했다.

  • AToBPostProcessor 클래스
    • 빈 후처리기이다.
    • 인터페이스인 BeanPostProcessor 를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다. 주의할 점은 스프링 컨테이너에 등록되는 모든 스프링 빈들이 해당 후처리기를 거치니까 적절히 분기처리를 해줘야한다는 점이다.

@PostConstruct 어노테이션은 스프링 빈 생성 이후에 빈을 초기화하는 역할을 한다. 즉, 생성된 빈을 한 번 조작하는 것이다. 따라서 빈을 조작하는 빈 후처리기가 필요한데, 스프링에서는 CommonAnnotationBeanPostProcessor 라는 빈 후처리기를 자동으로 등록하고, 여기에서 @PostConstruct 어노테이션이 붙은 메서드를 호출하는 것이다. 따라서 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다.

프록시 팩토리에서는 직접 프록시 객체를 생성하는 코드를 작성하고, 빈으로 등록해줘야 했지만, 빈 후처리기를 활용해 빈으로 등록될 객체를 모두 프록시 객체로 바꿀 수 있게 되었다.

프록시 적용 대상 여부 체크

위의 예제에서는 객체의 클래스 타입으로 프록시 대상 객체를 선정했지만, 패키지 이름으로도 프록시 대상 여부를 적용할 수 있다. 다음의 코드를 보자. scanBasePackage 를 통해, 스프링 부트가 기본으로 등록하는 수많은 빈들이 빈 후처리기를 거치지 않도록 해줄 수 있다. 이렇게 패키지를 설정해줌으로써, 애플리케이션 관련 빈들만 프록시 대상이 되도록 했다.

@Import(BeanPostProcessorConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProxyApplication.class, args);
	}

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

빈 후처리기 애플리케이션 로직에 적용

@Slf4j
@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);
    }
}
@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);

        // 프록시 적용대상 여부 체크
        // 프록시 적용 대상이 아니면 원본을 그대로 진행
        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;

    }
}

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

위의 예제 코드에서는 패지지 이름, 객체 타입으로 프록시 적용 대상 여부를 체크했지만 포인트컷(Pointcut) 을 이용하면 조금 더 깔끔할 것이다. 포인트컷은 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다.
참고로, 어드바이저는 포인트컷을 포함하고 있기에 어드바이저를 통해 적용 대상 여부를 판단할 수 있다.
스프링 AOP도 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.

포인트컷은 다음 두 곳에서 사용된다.

  • 자동 프록시 생성 : 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다
  • 프록시 내부 : 프록시의 어떤 메서드가 호출되었을 때 어드바이스를 적용할 지 판단한다.

스프링이 제공하는 빈 후처리기를 사용하려면 build.gradle 파일에 다음을 꼭 추가해주어야 한다.

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

이 라이브러리를 추가해주면 aspectjweaver 라는 aspectJ 관련 라이브러를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다.

자동 프록시 생성기 - AutoProxyCreator

스프링부트는 자동으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈에 자동으로 등록된다. 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.
이 빈 후처리기는 스프링 빈으로 등록된 Advisor 들을 자동으로 찾아서, 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.

  • Advisor 안에는 PointcutAdvice 가 이미 모두 포함되어 있다. 따라서 Advisor 만 알고 있으면 그 안에 있는 Pointcut 으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다. 그리고 Advice로 부가 기능을 적용하면 된다.

AnnotationAwareAspectJAutoProxyCreator@AspectJ와 관련된 AOP 기능도 자동으로 찾아서 처리해준다. Advisor 는 물론이고, @Aspect 도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다. @Aspect 에 대한 자세한 내용은 다음 포스팅에서 다룬다.

자동 프록시의 작동과정

자동 프록시 생성기(빈 후처리기)는 스프링 컨테이너에서 모든 Advisor를 조회한다. 그리고, 조회한 Advisor 에 포함된 Pointcut을 이용해서 프록시를 적용할 대상인지 아닌지 판단한다.

프록시 적용 대상여부를 체크할 때, 객체의 클래스 정보, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 대칭해본다. 조건이 하나라도 만족하면 프록시 적용 대상이 된다.

만약 만족하는 어드바이저가 여러 개라면 하나의 프록시에 여러 개의 어드바이저를 포함시켜 스프링 컨테이너에 등록한다.

자동 프록시 사용방법

@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);
    }
}
import org.aopalliance.intercept.MethodInterceptor;

public class LogTraceAdvice implements MethodInterceptor {

    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);

            // 로직 호출
            Object result = invocation.proceed();

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

위처럼 자동 프록시를 등록해서 사용할 수 있다. 하지만, 위처럼 Pointcut을 메서드 이름으로 매칭하게 된다면 스프링부트가 자동으로 등록하는 빈들의 메서드와 매칭되어 어드바이스를 적용하게 된다. 그러므로, 더 정밀한 포인트컷을 사용해야 한다. 실무에서는 매우 정밀한 AspectJExpressionPointcut 포인트컷을 사용한다.

    @Bean
    public Advisor advisor2(LogTrace logTrace) {
        // pointcut
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* hello.proxy.app..*(..))");

        // advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        return new DefaultPointcutAdvisor(pointcut, advice);
    }

pointcut.setExpression("execution(* hello.proxy.app..*(..))"); 의미

  • * : 모든 반환 타입
  • hello.proxy.app.. : hello.proxy.app 패키지와 하위 패키지로 패키지 범위를 지정
  • *(..) : * 는 모든 메서드 이름을, (..)는 파라미터는 상관 없다는 것을 의미합니다.

정리하자면, hello.proxy.app 패키지와 그 하위 패키지의 모든 메서드는 포인트컷의 매칭 대상이 된다.

만약, 프록시를 적용하고 싶지 않은 대상이 있다면 다음과 같이 작성하면 된다.

    @Bean
    public Advisor advisor3(LogTrace logTrace) {
        // pointcut
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");

        // advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
  • && : 모두 만족
  • ! : 반대

Reference

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글