ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring 핵심 원리 고급편 - 6. 스프링이 지원하는 프록시
    Spring-Boot/스프링핵심원리 - 고급편 2022. 6. 7. 16:47

    프록시 팩토리

    - 스프링은 유사한 구체적인 기술들이 있을 때, 그것들을 통합해서 일관성 있게 접근할 수 있고, 더욱 편리하게 사용할 수 있는 추상화된 기술을 제공한다.

    - 스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리( ProxyFactory ) 라는 기능을 제공한다

    - 프로시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다. 그리고 이 설정은 변경할 수도 있다.

    - 부가 기능을 적용할 때 Advice라는 새로운 개념을 도입했다

    - 개발자는 InvocationHandler 나 MethodInterceptor를 신경쓰지않고 Advice만 만들면 된다.

    - 결과적으로 InvocationHandler 나 MethodInterceptor는 Advice를 호출하게 된다. 

     

    - 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?

    : 앞서 특정 메서드 이름의 조건에 맞을 때만 프록시 부가 기능이 적용되는 코드를 직접 만들었다. 스프링은 ' Pointcut ' 이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.

     

    * Advice *

    - 프록시에 적용하는 부가 기능 로직

    - Advice를 만드는 방법은 여러가지가 있지만, 기본적인 방법은 인터페이스를 구현

     

    * MethodInterceptor - 스프링이 제공하는 코드 *

    package org.aopalliance.intercept;
    
    public interface MethodInterceptor extends Interceptor{
        Object invoke(MethodInvocation invocation) throws Throwable;
    }

    - MethodInvocation invocation

    : 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, 'args', 메서드 정보 등이 포함되어 있다.

      기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다

    - CGLIB의 MethodInterceptor와 이름이 같으므로 패키지 이름에 주의하자

         ㆍ org.aopalliance.intercept 패키지는 스프링 AOP모듈 ( spring-top ) 안에 들어 있다

    - MethodInterceptor는 Interceptor를 상속하고 Interceptor는 Advice 인터페이스를 상속한다.

     

    @Slf4j
    public class TimeAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            log.info("TimeProxy 실행");
            long startTime = System.currentTimeMillis();
    
            Object result = invocation.proceed();   // target을 찾아서 알아서 다음 객체 호출
    
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("TimeProxy 종료 resultTume={}", resultTime);
            return result;
        }
    }

    - Object result = invocation.proceed( )

         ㆍinvocation.proceed( )를 호출하면 target 클래스를 호출하고 그 결과를 받는다

         ㆍtarget 클래스의 정보는 MethodInvocation invocation 안에 모두 포함되어 있다

         ㆍ프록시 팩토리로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받기 때문

     

        @Test
        void interfaceProxy() {
            ServiceInterface target = new ServiceImpl();
            ProxyFactory proxyFactory = new ProxyFactory(target);
            proxyFactory.addAdvice(new TimeAdvice());
            ServiceInterface proxy = (ServiceInterface)proxyFactory.getProxy();
            log.info("targetClass={}", target.getClass());
            log.info("proxyClass={}", proxy.getClass());
        }

    - new ProxyFactory( target )

         ㆍ프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨준다.

         ㆍ프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸다.

    - proxyFactory.addAdvice( new TimeAdvice()) 

         ㆍ프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정한다.

         ㆍ프록시가 제공하는 부가 기능 로직을 Advice 라 한다.

    - proxyFactory.getProxy( )

         ㆍ프록시 객체를 생성하고 그 결과를 받는다.

     

    * 프록시 팩토리를 통한 프록시 적용 확인 *

    @Test
    void interfaceProxy() {
        // 프록시 팩토리를 통해서 프록시가 생성되면 JDK 동적 프록시나, CGLIB 모두 참이다
        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    
        // 프록시 팩토리를 통해서 프록시가 생성되고, JDK 동적 프록시인 경우 참
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
    
        // 프록시 팩토리를 통해서 프록시가 생성되고, CGLIB 동적 프록시인 경우 참
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }

    - 물론 proxy.getClass() 처럼 인스턴스의 클래스 정보를 직접 출력해서 확인할 수 있다.

     

    proxyFactory.setProxyTargetClass(true);     // 항상 CGLIB로 proxy 생성

    - 인터페이스가 있지만 CGLIB를 사용해서 인터페이스가 아닌 클래스 기반으로 동적프록시를 만드는 방법

     

    * 프록시 팩토리의 기술 선택 방법 *

    - 대상에 인터페이스가 있으면 : JDK 동적 프록시, 인터페이스 기반 프록시

    - 대상에 인터페이스가 없으면 : CGLIB, 구체 클래스 기반 프록시

    - proxyTargetClass = true : CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음

     

    * 정리 *

    - 프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다

    - 프록시의 부가 기능 로직도 특정 기술에 종속적이지 않게 ' Advice ' 하나로 편리하게 사용할 수 있었다. 

           이것은 프록시 팩토리가 내부에서

                JDK 동적 프록시인 경우 ' InvocationHandler ' 가 ' Advice '를 호출하도록 개발해두고,

                CGLIB인 경우 ' MethodInterceptor ' 가 ' Advice '를 호출하도록 기능을 개발해두었기 때문이다.

     

    * 참고 *

    - 스프링 부트는 AOP를 적용할 때 기본적으로 ' proxyTargetClass = true ' 로 설정해서 사용한다

    - 따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다.

     


    포인트컷, 어드바이스, 어드바이저

    - *포인트컷*(Pointcut) : 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링한다. 이름 그대로 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 것이다

    - *어드바이스*(Advice) : 이전에 본 것처럼 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직이라 생각하면 된다

    - *어드바이저*(Advisor) : 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다. 쉽게 이야기해서 *포인트컷1 + 어드바이스1*이다.

     

    - 정리하면 부가 기능 로직을 적용해야 하는데, 포인트컷으로 어디에? 적용할지 선택하고, 어드바이스로 어떤 로직을 적용할지 선택하는 것이다. 그리고 어디에? 어떤 로직?을 모두 알고 있는 것이 *어드바이저*이다.

     

    *쉽게 기억하기*

    - 조언( Advice )을 어디( Pointcut )에 할 것인가?

    - 조언자( Advisor )는 어디( Pointcut )에 조언( Advice )을 해야할지 알고 있다.

     

    *역할과 책임*

    이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것이다

    - 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다.

    - 어드바이스는 깔끔하게 부가 기능 로직만 담당한다

    - 둘을 합치면 어드바이저가 된다. 스프링의 어드바이저는 하나의 포인트컷 + 하나의 어드바이스로 구성된다.

     

    - 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할 지 알 수 있다.

     

    어드바이저

        @Test
        void advisorTest1() {
            ServiceInterface target = new ServiceImpl();
            ProxyFactory proxyFactory = new ProxyFactory(target);
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
            proxyFactory.addAdvisor(advisor);
            ServiceInterface proxy = (ServiceInterface)proxyFactory.getProxy();
    
            proxy.save();
            proxy.find();
        }

    - new DefaultPointcutAdvisor : Advisor 인터페이스의 가장 일반적인 구현체이다. 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어주면 된다. 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성된다

    - Pointcut.TRUE : 항상 true를 반환하는 포인트컷이다. 이후에 직접 포인트컷을 구현해볼 것이다.

    - new TimeAdvice( ) : 앞서 개발한 TimeAdvice 어드 바이스를 제공한다

    - proxyFactory.addAdvisor(advisor) : 프록시 팩토리에 적용할 어드바이저를 지정한다

         ㆍ어드바이저는 내부에 포인터컷과 어드바이스를 모두가지고 있다

         ㆍ따라서 어디에 어떤 부가 기능을 적용해야 할지 어드바이스 하나로 알 수 있다.

         ㆍ프록시 팩토리를 사용할 때 어드바이저는 필수이다

     

    포인트컷

    - 포인트컷은 크게 ClassFilter와 MethodMatcher 둘로 이루어진다. 이름 그대로 하나는 클래스가 맞는지, 하나는 메서드가 맞는지 확인할 때 사용한다. 둘다 'true'로 반환해야 어드바이스를 적용할 수 있다.

     

        static class MyPointcut implements Pointcut {
    
            @Override
            public ClassFilter getClassFilter() {
                return ClassFilter.TRUE;
            }
    
            @Override
            public MethodMatcher getMethodMatcher() {
                return null;
            }
        }

    - 직접 구현한 포인트컷이다

    - 현재 메서드 기준으로 로직을 적용하면 된다. 클래스 필터는 항상 true를 반환하도록 했고, 메서드 비교 기능은 MyMethodMatcher를 사용한다.

     

        @Slf4j
        static class MyMethodMatcher implements MethodMatcher {
    
            private String matchName = "save";
    
            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                boolean result = method.getName().equals(matchName);
                log.info("포인트컷 호출 method={} targetClass={}",method.getName(), targetClass);
                log.info("포인트컷 결과 result={}",result);
                return result;
            }
    
            @Override
            public boolean isRuntime() {
                return false;
            }
    
            @Override
            public boolean matches(Method method, Class<?> targetClass, Object... args) {
                return false;
            }
        }

    - 직접 구현한 MethodMatcher이다. MethodMatcher 인터페이스를 구현한다.

    -  matches( ) : 이 메서드에 method, targetClass 정보가 넘어온다. 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단할 수 있다

    - 여기서 메서드이름이 save인 경우에 true 반환

    - isRuntime( ), matches( ... args ) : isRuntime( )이 값이 참이면 matches( ... args ) 메서드가 대신 호출

         ㆍ동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다

         ㆍisRuntime( ) 이 false인 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만, isRuntime( )이 true인 경우 매개변수가 동적으로 변경된다고 가정하기 떄문에 캐싱을 하지 않는다

         ㆍ크게 중요한 부분은 아니니 참고만 하고 넘어가자

     

     

        @Test
        @DisplayName("직접 만든 포인트컷")
        void advisorTest2() {
            ServiceInterface target = new ServiceImpl();
            ProxyFactory proxyFactory = new ProxyFactory(target);
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
            proxyFactory.addAdvisor(advisor);
            ServiceInterface proxy = (ServiceInterface)proxyFactory.getProxy();
    
            proxy.save();
            proxy.find();
        }

    - save일때만 TimeAdvice 부가기능 적용!

     

    스프링이 제공하는 포인트컷

    - 스프링이 제공하는 NameMatchMethodPointcut를 사용

    @Test
        @DisplayName("스프링이 제공하는 포인트컷")
        void advisorTest3() {
            ServiceInterface target = new ServiceImpl();
            ProxyFactory proxyFactory = new ProxyFactory(target);
            NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
            pointcut.setMappedNames("save");
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
            proxyFactory.addAdvisor(advisor);
            ServiceInterface proxy = (ServiceInterface)proxyFactory.getProxy();
    
            proxy.save();
            proxy.find();
        }

     

     

    스프링이 제공하는 포인트컷

    - NameMatchMethodPointcut : 메서드 이름 기반으로 매칭한다. 내부에서는 PatternMatchUtils를 사용한다

         ㆍ예) *xxx* 허용

    - JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다

    - TruePointcut : 항상 참을 반환한다

    - AnnotationMatchingPointcut : 애노테이션을 매칭한다

    - AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다

     

    * 가장 중요한 것은 aspectJ 표현식 *

    - 여기에서 사실 다른 것은 중요하지 않다

    - 실무에서는 사용하기도 편하고 기능도 가장 많은 aspectJ 표현식을 기반으로 사용하는 AspectJExpressionPointcut을 사용

     

    하나의 프록시, 여러 어드바이저

    @Test
        @DisplayName("하나의 프록시, 여러 어드바이저")
        void multiAdvisorTest2() {
            //client -> proxy -> advisor2 -> advisor1 -> target
    
            DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
            DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
    
    
            // 프록시1 생성
            ServiceInterface target = new ServiceImpl();
            ProxyFactory proxyFactory1 = new ProxyFactory(target);
            proxyFactory1.addAdvisor(advisor2);
            proxyFactory1.addAdvisor(advisor1);
            ServiceInterface proxy1 = (ServiceInterface)proxyFactory1.getProxy();
    
            // 실행
            proxy1.save();
        }

     

     

    * 중요 *

    - 스프링의 AOP를 처음 공부하거나 사용하면, AOP 적용 수 만큼 프록시가 생성된다고 착각하게 된다. 실제 많은 개발자들도 이렇게 생각하는 것을 보았다

    - 스프링의 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.

    - 정리하면 하나의 target에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target마다 하나의 프록시만 생성한다.

     

     

    정리

    - 프록시 팩토리 덕분에 개발자는 매우 편리하게 프록시를 생성할 수 있게 되었다

    - 추가로 어드바이저, 어드바이스, 포인트컷이라는 개념 덕분에 *어떤 부가 기능*을 *어디에 적용*할 지 명확하게 이해할 수 있었다.

     

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

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

    - 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능하다

         ㆍ실제 객체를 컴포넌트 스캔으로 스프링컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 떄문

    - 부가기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야한다.

     

    → 두가지 문제를 한번에 해결하는 방법이 바로 다음에 설명할 빈 후처리기이다.

     

     

     

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

    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.