본문 바로가기

Spring

빈 후처리기, BeanPostProcessor

빈 후처리기

남아있는 문제

  • 이전 포스팅에서 프록시 팩토리를 사용해서 스프링이 지원하는 프록시를 만들고 어드바이저로 부가기능을 생성해보았다.
  • 스프링 빈에서 프록시 객체로 등록하기 위해서 해당 스프링 빈 대신에 프록시 객체를 넣고 프록시 객체 내부에서 부가기능과 함께 실제 객체를 호출하는 식으로 작성을 했다.
  • 이렇게 할 때 문제는 복잡한 빈 등록 설정을 해줘야 한다는 것인데 빈 등록을 할 때 프록시 객체를 넘겨서 설정을 해주는 과정이 번거롭다. 게다가 빈을 컴포넌트 스캔으로 자동 등록할 때에는 이런식으로 수동으로 등록하기가 어렵다.
  • 위와 같은 문제를 해결하기 위해 빈 후처리기를 사용한다.

빈 후처리기 (BeanPostProcessor)

  • 객체를 스프링 빈 저장소에 등록하기 전에 조작을 할 수 있게 해주는 인터페이스다.
  • 객체를 조작하거나 아예 바꿔서 반환할 수 있다.

  • 스프링 빈 등록과정
    1. 생성 : 빈 대상이 되는 객체를 생성한다.
    2. 전달 : 생성한 객체를 빈 후처리기에 전달한다.
    3. 후 처리 작업 : 빈 후처리기에서는 객체를 조작하거나 다른 객체로 변경한다.
    4. 등록 : 후 처리 작업이 끝난 객체를 빈 저장소에 등록한다.
  • 위의 등록과정을 살펴보면 후 처리 작업에서 다른 객체로 변경을 하면 해당 객체가 빈 저장소에 등록됨을 확인할 수 있다.
    • 이를 그림으로 살펴보면 다음과 같다.

다른 객체로 바꿔치는 빈 후처리기 예제

  • 먼저 빈 후처리기를 만들어서 빈으로 등록해야 한다.
  • 빈 후처리기를 만들기 위해서는 BeanPostProcessor 인터페이스를 구현해줘야 한다.
    • BeanPostProcessor는 두 가지 디폴트 메서드가 존재한다.
    • postProcessBeforeInitialization, postProcessAfterInitialization, 각각 Initialization(@PostConstruct) 전과 후에 적용이 된다고 보면 된다.
  • 다음은 A객체를 B객체로 변경하는 후처리기이다.
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;
    }
}

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

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

  • 위의 코드에서 Config 클래스를 살펴보면 beanA라는 이름으로 A객체를 생성하여 등록하고있다.
  • 하지만 후처리기에서 A객체의 인스턴스이면 B 객체를 생성하여 객체를 변경하고있다.
  • 아래의 테스트코드 결과에서도 볼 수 있듯이 실제로 beanA 로 등록된 객체는 B.class 타입이고 A.class 타입의 객체는 존재하지 않음을 확인할 수 있다.
@Test
void basicConfig(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
    //beanA 이름으로 B는 빈으로 등록된다.
    B a = applicationContext.getBean("beanA", B.class);
    a.helloB();
    //A는 빈으로 등록되지 않는다.
    assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.class));
}

프록시와 빈 후처리기

  • 다시 한 번 기존의 프록시의 구현 방식을 생각해보자.
  • 빈을 등록할 때 직접 프록시 객체를 생성해서 등록해줬었는데 이를 빈 후처리기를 이용하면 훨씬 간결한 코드로 프록시를 만들 수 있다. 

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("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;
    }
}

  • 먼저 BeanPostProcessor 를 구현한 클래스를 생성하고 postProcessAfterInitialization 메서드를 재정의한다.
  • 해당 메서드에서는 프록시를 생성해서 반환한다.
    • 위의 코드에서는 패키지를 주입받아서 해당 패키지의 대상일 경우에만 프록시를 생성하여 반환한다.
    • 어드바이저도 주입받아서 프록시를 생성할 때 추가해준다.
  • 다음은 위의 후처리기를 빈으로 등록하는 과정이다.
@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
    return new PackageLogTracePostProcessor("hello.proxy.app", getAdvisor(logTrace));
}

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

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

  • 이렇게 빈 등록까지 마치면 정상적으로 빈 후처리기를 통해서 프록시 등록이 완성된다.

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

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

  • 위와 같이 spring-boot-starter-aop 의존성을 추가하면 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기를 자동으로 등록해준다.
  • 이 빈 후처리기는 또 다시 빈으로 등록된 Advisor를 찾아서 자동으로 프록시를 생성해준다.
    • Advisor는 Pointcut 과 Advice가 포함되어있기 때문에 Pointcut이 적용될 대상에 대해 프록시를 만들고 실제 프록시가 호출될 때 Pointcut으로 다시 한 번 대상 메서드임을 검증하고 Advice의 부가기능을 추가해준다.

 

  1. 생성 : 스프링 빈 대상이 되는 객체를 생성한다.
  2. 전달 : 빈 후처리기에 생성된 객체를 전달한다.
  3. 모든 Advisor 빈 조회 : 빈 후처리기는 빈으로 등록된 모든 Advisor를 조회한다.
  4. 프록시 적용 대상 체크 : 대상 객체가 Advisor의 포인트컷의 적용 대상인지 확인한다. 모든 어드바이저의 포인트컷을 확인해서 하나라도 대상이 되면 프록시를 생성한다.
  5. 프록시 생성 : 프록시 적용 대상이면 프록시를 생성하여 반환하므로 원래 객체 대신 프록시가 빈으로 등록된다. 대상이 아니면 원래 객체가 빈으로 등록된다.
  6. 빈 등록 : 반환된 객체를 빈으로 등록한다.

 

  • 클라이언트가 타겟을 호출하면 타겟 대신 프록시를 호출한다. 프록시는 호출 요청이 포인트컷에 부합하는 메서드를 호출하는지 확인하고 대상이면 부가기능을 추가하고 아니면 부가기능 없이 타겟을 호출하게된다.

포인트컷의 두 가지 사용

  1. 프록시 적용 여부 판단
    • 프록시를 생성할 때 생성할 필요가 있는지 없는지를 판단한다.
    • 여러 어드바이저의 포인트컷 요건에 하나라도 맞으면 프록시를 생성한다.
  2. 어드바이스 적용 여부 판단
    • 프록시가 호출되었을 때 부가기능을 적용할 지 말지를 판단한다.

어드바이저 적용

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

  • 다음과 같이 어드바이저를 생성하여 빈으로 지정하면 자동 프록시 생성기가 advisor에 pointcut으로 빈으로 등록된 객체중에서 어드바이스 적용 대상인지 확인한다.
    • 맞으면 프록시를 생성하고 아니면 본래 객체를 그대로 반환한다.
@Aspect
public class LogTraceAspect {

    private final LogTrace logTrace;

    public LogTraceAspect(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Around("execution(* hello.proxy.app..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        TraceStatus status = null;
        try {
            String message = joinPoint.getSignature().toShortString();
            status = logTrace.begin(message);
            Object result = joinPoint.proceed();
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

@Configuration
public class AopConfig {

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

  • 위와같이 Aspect 어노테이션을 사용해서 어드바이저를 만들 수 있다.
  • @Around 어노테이션에서 들어가는 값이 pointcut이라고 볼 수 있고 execute 메서드 안의 내용은 Advice(부가기능) 이라고 생각할 수 잇다.
  • 위의 객체를 빈으로 등록해줘도 자동 프록시 생성 빈 후처리기가 프록시를 생성하는 작업을 해준다.

 

AnnotationAwareAspectJAutoProxyCreator

  • 어노테이션 기반의 자동 프록시 생성기인데 말 그대로 @Aspect 어노테이션이 붙어있는 스프링 빈을 찾아서 자동으로 프록시를 만들어 주는 역할을 한다.

  • 이 자동 프록시 생성기는 두 가지 역할을 한다.
    1. @Aspect 어노테이션을 보고 어드바이저를 생성해준다.
      • 이 때 BeanFactoryAspectJAdvisorsBuilder 클래스가 사용되는데, 포인트컷, 어드바이스, 어드바이저 등을 생성해준다.
    2. 어드바이저를 기반으로 프록시를 생성한다.

 

Reference

  • 스프링 핵심 원리 - 고급편, 김영한

'Spring' 카테고리의 다른 글

스프링 AOP 내부호출 문제와 한계  (0) 2022.06.16
스프링 AOP  (0) 2022.06.12
스프링이 제공하는 프록시, 프록시 팩토리  (0) 2022.06.09
Servlet ?  (0) 2022.01.05
Fetch join, N+1 문제  (0) 2021.11.19