ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring 핵심 원리 고급편 - 7. 빈 후처리기
    Spring-Boot/스프링핵심원리 - 고급편 2022. 6. 8. 10:33

    - @Bean 이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고, 스프링 컨테이너 내부의 빈 저장소에 등록한다.

    - 그리고 그 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다

     

    * 빈 후처리기 - BeanPostProcessor *

    - 스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶으면 빈 후처리기를 사용하면 된다.

    - 빈 포스트 프로세서 ( BeanPostProcessor ) 는 번역하면 빈 후처리기인데, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다

     

    * 빈 후처리기 기능 *

    - 빈 후처리기의 기능은 막강하다

    - 객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다

     

    * 빈등록 과정을 빈 후처기와 함께 살펴보자 *

    1. 생성 : 스프링 빈 대상이 되는 객체를 생성한다 ( @Bean, 컴포넌트 스캔 모두 포함 )

    2. 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다

    3. 후 처리 작업 : 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기 할 수 있다

    4. 등록 : 빈 후처리기는 빈을 반환한다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

     

    * BeanPostProcessor 인터페이스 - 스프링 제공 *

    public interface BeanPostProcessor {
        Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
        Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
    }

    - 빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다

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

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

     

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

    - 빈 후처리기이다. 인터페이스인 BeanPostProcessor를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다

     

    *정리*

    - 빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트이다

    - 이것은 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다

    - 여기서 조작이라는 것은 해당 객체의 특정 메서드를 호출하는 것을 뜻한다

    - 일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다

    - 이 말은 * 빈 객체를 프록시로 교체 * 하는 것도 가능하다는 뜻이다.

     

    * 참고 - @PostConstruct의 비밀 *

     - @PostConstruct는 스프링 빈 생성 이후에 빈을 초기화하는 역할을 한다

    - 스프링은 CommonAnnotationBeanPostProcessor 라는 빈 후처리기를 자동으로 등록하는데, 여기에서 @PostConstruct 애노테이션이 붙은 메서드를 호출한다. 따라서 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다.

     


    빈 후처리기 적용

    PackageLogTraceProxyProcessor

    @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());
    
            // 프록시 적용 대상 여부 체크
            // 프록시 적용 대상이 아니면 원본을 그대로 진행
    
            // basepackage에서 온게 아니라면 원본을 반환
            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;
        }
    }

    - 원본 객체를 프록시 객체로 변환하는 역할을 한다

    - 이때 프록시 팩토리를 사용하는데, 프록시 팩토리는 advisor가 필요하기 때문에 이 부분은 외부에서 주입 받도록 했다

    - 모든 스프링 빈들에 프록시를 적용할 필요는 없다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용한다. 여기서는 ' hello.proxy.app '과 관련된 부분에만 적용하면 된다. 다른 패키지의 객체들은 원본 객체를 그대로 반환한다.

    - 프록시 적용 대상의 반환 값을 보면 원본 객체 대신에 프록시 객체를 반환한다. 따라서 스프링 컨테이너에 원본 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 원본 객체는 스프링 빈으로 등록되지 않는다.

     

    - 이제 * 프록시를 생성하는 코드가 설정 파일에는 필요없다. * 순수한 빈 등록만 고민하면된다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.

     

    * 컴포넌트 스캔에도 적용 *

    - 여기서 중요한 포인트는 v1, v2와 같이 수동으로 등록한 빈 뿐만 아니라 컴포넌트 스캔을 통해 등록한 빈들도 프록시를 적용할 수 있다는 점이다. 이것은 모두 빈 후처리기 때문이다.

     

    * 프록시 적용 대상 여부 체크 *

    - 애플리케이션을 실행해서 로그를 확인해보면 알겠지만, 우리가 직접 등록한 스프링 빈들 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다.

    - 그래서 어떤 빈을 프록시로 만들 것인지 기준이 필요하다. 여기서는 간단히 basePackage를 사용해서 특정 패키지를 기준으로 해당 패키지와 그 하위 패키지의 빈들을 프록시로 만든다

    - 스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다. 따라서 모든 객체를 프록시로 만들 경우 오류가 발생한다.

     


    빈 후처리기 - 정리

    * 문제1 - 너무 많은 설정 *

    * 문제2 - 컴포넌트 스캔 *

     

    * 문제 해결 *

    - 빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있다

    - 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈을 등록할 수 있다

     

    * 하지만 개발자의 욕심은 끝이 없다. *

    - 스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다.

     

    * 중요 *

    - 프록시의 적용 대상 여부를 포인트컷을 사용하면 더 깔끔할 것 같다.

    - 포인트컷은 이미 클래스, 메소드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다.

    - 참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다

     

    결과적으로 포인트컷은 다음 두 곳에서 사용된다

    1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다 ( 빈 후처리기 - 자동 프록시 생성 )

    2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다 ( 프록시 내부 )


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

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

    - ' aspectjweaver ' 라는 aspectJ 관련 라이브러리를 등록

    - 스프링 부트가 AOP관련 클래스를 자동으로 스프링 빈에 등록

    - 스프링 부트가 없던 시절에는 @EnableAspectAutoProxy 직접 사용 → 스프링 부트가 자동 처리

     

    * 자동 프록시 생성기 - AutoProxyCreator *

    - 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈에 자동으로 등록

    - 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다

    - 이 빈 후처리기는 스프링 빈으로 등록된 Advisor 들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용

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

     

    * 참고 *

    - AnnotationAwareAspectJAutoProxyCreator 는 @AspectJ 와 관련된 AOP 기능도 자동으로 찾아서 처리해준다

    - Advisor 는 물론이고, @Aspect 도 자동으로 인식해서 프록시를 만들고 AOP를 적용

     

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

    1. 생성 : 스프링이 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean, 컴포넌트 스캔 모두 포함 )

    2. 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다

    3. 모든 Advisor 빈 조회 : * 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회한다

    4. 프록시 적용 대상 체크 : 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다.

        ㆍ이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다.

        ㆍ그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다.

        ㆍ예를 들면 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다

    5. 프록시 생성 : 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다.. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.

    6. 빈 등록 : 반환된 객체는 스프링 빈으로 등록된다.

     

    중요 : 포인트컷은 2가지에 사용된다

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

    - 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.

    - 클래스 + 메서드 조건을 모두 비교한다. 이때 모든 메서드를 체크하는데, 포인트컷 조건에 하나하나 매칭해본다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다

    - 만약 조건에 맞는 것이 하나도 없으면 프록시를 생성할 필요가 없으므로 프록시를 생성하지 않는다

     

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

    - 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단

     

     

    * 참고 *

    - 프록시를 모든 곳에서 생성하는 것은 비용 낭비이다. 꼭 필요한 곳에 최소한의 프록시를 적용해야한다.

    - 그래서 자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 것이 아니라 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다.

     

     

    - 애플리케이션 서버를 실행해보면, 스프링이 초기화되면서 기대하지 않은 이러한 로그들이 올라온다

    - 결론적으로 패키지에 메서드 이름까지 함께 지정할 수 있는 매우 정밀한 포인트컷이 필요하다.

     

    * AspectJExpressionPointcut *

    - AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다.

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

    - AspectJExpressionPointcut : AspectJ 포인트컷 표현식을 적용할 수 있다.

    - ' execution( * hello.proxy.app..*(..))  : AspectJ가 제공하는 표현식

        ㆍ* : 모든 반환 타입

        ㆍhello.proxy.app.. : 해당 패키지와 그 하위 패키지

        ㆍ*(..) : *모든 메서드 이름, (..) 파라미터는 상관없음

    - 쉽게 이야기해서 hello.proxy.app 패키지와 그 하위 패키지의 모든 메서드는 포인트컷의 매칭 대상이 된다.

     

     

    * 프록시 자동 생성기 상황별 정리 *

    - advisor1 의 포인트컷만 만족 → 프록시 1개 생성, 프록시에 advisor1 만 포함

    - advisor1, advisor2 의 포인트컷을 모두 만족 → 프록시 1개 생성, 프록시에 advisor1, advisor2 모두 포함

    - advisor1, advisor2 의 포인트컷을 모두 만족하지 않음 → 프록시 생성되지 않음

     


    정리

    - 자동 프록시 생성기인 AnnotationAwareAspectJAutoProxyCreator 덕분에 개발자는 매우 편리하게 프록시를 적용할 수 있다.

    - 이제 Advisor만 스프링 빈으로 등록하면 된다

    - Advisor = Pointcut + Advice

     

     

     

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

    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

    댓글

Designed by Tistory.