Spring

[스프링] 빈 후처리기란?

문승주 2024. 5. 11. 23:30
반응형

본 내용은 인프런의 김영한 님의 강의 "스프링 핵심 원리 - 고급편" 내용을 바탕으로 정리한 내용입니다.

빈 후처리기란?

  • 스프링 프레임워크에서 빈으로 등록할 목적으로 생성된 객체를 빈 저장소에 등록하기 전에 조작하는 기능이다.
  • 이를 통해 생성된 객체를 조작하거나 완전히 다른 객체로 변경하는 것도 가능하다.
  • 빈 후처리기를 사용하면 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다.

빈 후처리기를 사용한 빈 등록 과정

  1. 생성: 스프링 빈 대상이 되는 객체를 생성한다.
  2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
  4. 등록: 빈 후처리기는 빈을 반환한다. 전달 된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

BeanPostProcessor 인터페이스

  • 스프링에서 빈 후처리기 사용시 구현하는 인터페이스
  • postProcessBeforeInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서
  • postProcessAfterInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음 에 호출되는 포스트 프로세서
public interface BeanPostProcessor { 
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException 
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}

빈 후처리기 예제

BeanPostProcessorTest.class

  • A 객체 bean 등록 시 B 객체가 등록된다.
package hello.proxy.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 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));  
    } 

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

테스트 결과

BeanPostProcessorTest$AToBPostProcessor - beanName=beanA bean=hello.proxy.postprocessor.BeanPostProcessorTest$A@68be8808
BeanPostProcessorTest$B - hello B
  • 테스트 실행 시 postProcessAfterInitialization 메서드가 호출되며, 여기서 A 타입의 빈을 B 타입의 빈으로 변환한다.

빈 후처리기 적용 예시

PackageLogTracePostProcessor.class

  • 특정 패키지에 속하는 빈에 대해서만 프록시를 생성하여 반환하도록 설정한다.
package hello.proxy.config.v4_postprocessor.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 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;
}

BeanPostProcessorConfig.class

  • basePackage"hello.proxy.app"을 지정하여 해당 패키지에 속하는 빈들에 대해서만 프록시를 생성하도록 설정한다.
  • getAdvisor 메서드에서는 NameMatchMethodPointcut을 사용하여 특정 메서드 이름 패턴에 매칭되는 경우에만 LogTraceAdvice를 적용하는 Advisor를 생성한다.
package hello.proxy.config.v4_postprocessor;  

import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;  
import hello.proxy.config.v4_postprocessor.postprocessor.PackageLogTracePostProcessor;  
import hello.proxy.trace.logtrace.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;   

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

ProxyApplication.class

  • 빈 후처리기를 사용하여 컴포넌트 스캔으로 등록된 빈도 모두 프록시가 적용된다.
  • scanBasePackages 속성을 통해 "hello.proxy.app" 패키지와 그 하위 패키지를 스캔하여 빈을 등록합니다.
  • @Import(BeanPostProcessorConfig.class)를 사용하여 BeanPostProcessorConfig 클래스를 임포트하고, 해당 클래스에서 정의한 빈들을 사용할 수 있게 한다.
package hello.proxy;  

import hello.proxy.config.v4_postprocessor.BeanPostProcessorConfig;
import hello.proxy.trace.logtrace.LogTrace;  
import hello.proxy.trace.logtrace.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(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();  
    }
}

AutoProxyCreator(자동 프록시 생성기)

  • 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈에 자동으로 등록된다
  • 스프링 빈으로 등록된 Advisor 들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
  • Advisor 안에 있는 Pointcut 을 통해 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다.

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

  1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다.
  2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 모든 Advisor 빈 조회*: 빈 후처리기는 스프링 컨테이너에서 모든 Advisor 를 조회한다.
  4. 프록시 적용 대상 체크: 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를적용할 대상인지 아닌지 판단한다. 여러개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
  5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다.
  6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다

포인트컷의 2가지 사용방법

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

  • 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다.
  • 2. 어드바이스 적용 여부 판단 - 사용 단계
  • 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다.

AspectJExpressionPointcut

  • 스프링 프레임워크 AOP 에서 포인트컷을 정의하는 데 사용되는 클래스이다.
  • AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다.

자동 프록시 생성시 사용 예시

  • hello.proxy.app 하위 패키지 이거나 noLog 메서드가 아닌 대상만 프록시를 적용한다. (실행할 application 파일은 생략)
package hello.proxy.config.v5_autoproxy;  

import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;  
import hello.proxy.trace.logtrace.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;   

@Configuration  
public class AutoProxyConfig {  
    @Bean  
    public Advisor advisor(LogTrace logTrace) {  
        //pointcut  
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        // hello.proxy.app 하위 패키지 이거나 noLog 메서드가 아닌 대상만 프록시 적용
        pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");  
        //advice  
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);  
        return new DefaultPointcutAdvisor(pointcut, advice);  
    }  
}
반응형