Spring/[인프런 김영한 스프링 핵심 원리 - 고급편]

[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링이 지원하는 프록시

h2boom 2024. 12. 10. 20:19

프록시 팩토리

  • 이전에는 상황에 따라서 인터페이스가 있으면 JDK 동적 프록시를 사용하고 구체 클래스의 경우 CGLIB를 사용했다면 프록시 팩토리 하나로 동적 프록시를 생성할 수 있다.
  • 프록시 팩토리 : 스프링에서 제공하는 동적 프록시를 통합해서 편리하게 만들어주는 기능이다.
    • 동작 방식 - 인터페이스가 있으면 JDK 동적 프록시를 사용, 구체 클래스만 있으면 CGLIB를 사용한다.

  • 부가 기능 적용 시 Advice를 만들면 InvocationHandler나 MethodInterceptor를 신경쓰지않아도 된다.
    • 프록시 팩토리를 사용하면 Advice를 호출하는 전용 (스프링에서 제공) InvocationHandler, MethodInterceptor를 내부에서 사용

 

  • Advice 동작 흐름
    1. 클라이언트가 프록시 팩토리에게 프록시 요청
    2. 프록시 팩토리는 프록시 기술을 선택 (JDK 동적 프록시 / CGLIB)
      • 인터페이스의 경우 JDK 동적 프록시
      • 구체 클래스의 경우 CGLIB
    3. 선택한 기술을 기반으로 동적 프록시를 생성
      • JDK 동적 프록시의 경우 스프링에서 제공하는 adviceInvocationHandler를 호출
      • CGLIB의 경우 스프링에서 제공하는 adviceMethodInterceptor를 호출
    4. handler / interceptor는 Advice를 호출
    5. Advice는 프록시 로직 수행 후 target(실제 객체) 호출
    6. target(실제 객체) 로직 수행

 

  • 특정 조건에 맞을 때만 프록시 로직을 적용하려면 스프링이 제공하는 Pointcut을 도입해서 일관성 있게 해결가능

프록시 팩토리 - 예제

  • Advice는 프록시에 적용하는 부가 기능 로직이다.
    • InvocationHandler와 MethodInterceptor 개념과 유사하며 개념적으로 둘을 추상화한 것

 

public interface MethodInterceptor extends Interceptor {
	Object invoke(MethodInvocation invocation) throws Throwable;
}
  • Advice를 만드는 가장 기본적인 방법
    • MethodInterceptor 인터페이스를 구현해서 만든다.
      • org.aopalliance.intercept 패키지의 MethodInterceptor로 CGLIB의 MethodInterceptor와 다르다.
      • Interceptor 인터페이스가 Advice를 구현하고 있기 때문
    • MethodInvocation invocation : 다음 메소드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메소드 정보 등을 가지고 있는 변수이다.
      • 기존 파라미터로 제공되는 부분들을 모두 합쳐놓았다.

 

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

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy 종료 resultTime={}", resultTime);
        return result;
    }
}
  • Advice 구현 예제
    • TimeAdvice는 MethodInterceptor 인터페이스를 구현
    • invocation.proceed() : target(실제 객체) 클래스를 호출하고 그 결과를 반환받는다.
      • target 정보도 MethodInvaction invocation 안에 모두 포함되어 있다.
      • 프록시 팩토리로 프록시를 생성하는 단계에서 target 정보를 파라미터로 받는다.

 

@Slf4j
public class ProxyFactoryTest {
    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    public 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());

        proxy.save();

        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }
    
    @Test
    @DisplayName("구체 클래스만 있으면 CGLIB 사용")
    public void concreteProxy() {
        ConcreteService target = new ConcreteService();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeAdvice());
        ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();

        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());

        proxy.call();

        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
        assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
    }
}
  • Advice 테스트 코드 예제
    • interfaceProxy() / concreteProxy()
      • new ProxyFactory(target) : 프록시 팩토리를 생성할 때 생성자로 프록시 호출 대상(실제 객체)을 같이 넘겨준다.
        • 인스턴스 정보를 기반으로 프록시를 만들어내는데 인터페이스가 있으면 JDK 동적 프록시로 생성, 구체 클래스만 있다면 CGLIB로 동적 프록시를 생성한다.
      • proxyFactory.addAdvice(new TimeAdvice()) : 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정한다.
        • Advice : 프록시가 제공하는 부가 기능 로직
      • proxyFactory.getProxy() : 프록시 객체를 생성하고 그 결과를 받는다.

 

    @Test
    @DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
    public void proxyTargetClass() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.setProxyTargetClass(true);
        proxyFactory.addAdvice(new TimeAdvice());
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());

        proxy.save();

        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
        assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
    }
  • Advice 테스트 코드 예제
    • proxyTargetClass()
      • proxyFactory.setProxyTargetClass(true) : 인터페이스가 있더라도 CGLIB를 사용해서 인터페이스가 아닌 클래스 기반으로 동적 프록시를 만든다.

 

  • 프록시 팩토리 기술 선택 방법
    • 인터페이스가 있는 경우 : JDK 동적 프록시, 인터페이스 기반 프록시
    • 인터페이스가 없는 경우 : CGLIB, 구체 클래스 기반 프록시
    • proxyTargetClass(true) : CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관이 없다.
  • 스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass(true)로 설정해서 사용한다.
    • 인터페이스가 있더라도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 동적 프록시 생성

 

  • 프록시 팩토리 서비스 추상화로 CGLIB, JDK 동적 프록시 기술에 의존하지 않고 편리하게 동적 프록시를 사용할 수 있다.

포인트컷, 어드바이스, 어드바이저 - 소개

  • 포인트컷(Pointcut) : 어디에 부가 기능을 적용할지, 적용하지 않을지 판단하는 필터 로직이다.
    • 주로 메소드 이름으로 필터링한다.
  • 어드바이스(Advice) : 프록시가 호출하는 부가 기능(=프록시 로직)이다.
  • 어드바이저(Advisor) : 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.
    • 어드바이저 = 포인트컷1 + 어드바이스1
  • 부가 기능을 적용할 때 포인트컷으로 어디에 적용할지 정하고 어드바이스로 어떤 로직을 적용할지 정하며 어디에? 어떤 로직을? 정할지 모두 알고 있는 것이 어드바이저이다.

 

  • 역할과 책임을 명확하게 하기위해 분리한 것이다.
    • 포인트컷은 대상 여부를 확인하는 필터 역할만 담당
    • 어드바이스는 부가 기능 로직만 담당

 

  • 프록시 전체 흐름
    1. 클라이언트 -> 프록시 호출
      1. 프록시는 Advisor를 알고 있다. 
    2. 프록시가 Advisor 호출
    3. Pointcut으로 Advice 적용 여부 확인
    4. 적용 대상이라면 Advice 부가 기능을 적용 후 target 호출
      • 적용 대상이 아닌 경우 부가 기능을 적용하지 않고 target 바로 호출


어드바이저 - 예제

  • 어드바이저 Advisor : 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
    • 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 적용할 지 알 수 있다.

 

public class AdvisorTest {
    @Test
    public 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를 반환하는 포인트컷이다.
    • proxyFactory.addAdvisor(advisor) : 프록시 팩토리에 적용할 어드바이저를 지정한다.
      • 어드바이저를 지정함으로 어떤 부가 기능을 어디에 적용할지 어드바이저 하나로 알 수 있다.
      • 프록시 팩토리를 사용할 때 어드바이저는 필수이다.
      • proxyFactory.addAdvice(advice) : 편의 메소드로 호출하면 내부에서는 DefaultPointcutAdvisor(Pointcut.TRUE,  advice) 어드바이저가 생성된다.

 

  • 프록시 팩토리는 어드바이저를 알고 있고 어드바이저는 포인트컷과 어드바이스를 알고 있는 관계이다.


직접 만든 포인트컷 예제

  • 요구사항 - save() 메소드에는 어드바이스 로직을 적용하지만 find()에는 어드바이스 로직을 적용하지 않도록 하자

 

public interface Pointcut {
    ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

public interface ClassFilter {
    boolean matches(Class<?> clazz);
}

public interface MethodMatcher {
    boolean matches(Method method, Class<?> targetClass);
    //..
}
  • 스프링에서 제공하는 Pointcut 인터페이스
    • Pointcut은 ClassFilter와 MethodMatcher로 이뤄져있다.
    • ClassFilter는 클래스가 필터와 맞는지, MethodMatcher는 메소드가 맞는지 확인할 때 사용하며 둘 다 true를 반환할 때 어드바이스를 적용할 수 있다.

 

@Slf4j
public class AdvisorTest {
    @Test
    @DisplayName("직접 만든 포인트컷")
    public 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();
    }

    static class MyPointcut implements Pointcut {
        @Override
        public ClassFilter getClassFilter() {
            return ClassFilter.TRUE; //항상 TRUE를 반환
        }

        @Override
        public MethodMatcher getMethodMatcher() {
            return new MyMethodMatcher();
        }
    }

    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;
        }
    }
}
  • 직접 구현한 포인트컷을 사용하는 예제
    • MyPointcut 클래스
      • 직접 구현한 포인트컷으로 Pointcut 인터페이스를 구현
      • 요구 사항을 만족하기 위해서 메소드 기준으로 로직을 적용
        • 클래스 필터는 ClassFilter.true로 지정해서 항상 true를 반환
        • 메소드 필터로는 MyMethodMatch를 사용
    • MyMethodMatcher
      • 직접 구현한 MethodMatcher로 MethodMatcher 인터페이스를 구현
      • matches() : method, targetClass 정보가 넘어오며 이 정보로 어드바이스를 적용할지 여부를 판단할 수 있다.
        • 예제에서는 메소드 이름이 save인 경우에만 true를 반환하도록 판단 로직 적용
      • isRuntime(), matches(... args) : isRuntime()이 참이면 matches(... args)가 대신 호출되며 동적으로 넘어오는 파라미터를 판단 로직으로 사용할 수 있다.
        • isRutime()이 false인 경우 클래스 정적 정보만 사용하기에 스프링 내부에서 캐싱을 통한 성능 향상이 가능
        • isRutime()이 true인 경우 파라미터가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.


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

  • 스프링이 제공하는 대표적인 포인트컷
    • NameMatchMethodPointcut : 메소드 이름 기반으로 매칭하며 내부에서는 PatternMatchUtils 사용
      • ex) *xxx* 허용
    • JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
    • TruePointcut : 항상 참을 반환한다.
    • AnnotationMatchingPointcut : 어노테이션으로 매칭한다.
    • AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.
  • 이외에도 스프링에서 제공하는 많은 포인트컷이 존재한다.

 

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

    proxy.save();
    proxy.find();
}
  • 스프링이 제공하는 NameMatchPointcut 사용 예제
    • pointcut.setMappedName() : 인수로 전달한 메소드 이름과 일치하는 경우 Advice의 부가 기능을 적용한다.

 

  • 실무에서는 사용하기도 편리하고 가장 많은 기능을 제공하는 aspectJ 표현식을 기반으로 하는 AspectJExpressionPointcut을 사용한다.

여러 어드바이저 함께 적용

  • 하나의 target에 여러 어드바이저를 적용하려면?
    • 여러 프록시를 만드는 방법
    • 스프링에서 제공하는 기능을 사용하는 방법
public class MultiAdvisorTest {
    @Test
    @DisplayName("여러 프록시")
    public void multiAdvisorTest1() {
        //프록시1 생성
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory1 = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        proxyFactory1.addAdvisor(advisor1);
        ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();

        //프록시2 생성, target -> proxy1 입력
        ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
        DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
        proxyFactory2.addAdvisor(advisor2);
        ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();

        //실행
        proxy2.save();
        proxy2.find();
    }

    @Slf4j
    static class Advice1 implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            log.info("advice1 호출");
            return invocation.proceed();
        }
    }

    @Slf4j
    static class Advice2 implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            log.info("advice2 호출");
            return invocation.proceed();
        }
    }
}
  • 여러 어드바이스를 하나의 타겟에 적용하기 (여러 프록시 사용) 예제
    • 예제 흐름 : client -> proxy2(advisor2) -> proxy1(advisor1) -> target
    • 여러 프록시의 문제는 잘못된 것은 아니지만 적용해야하는 어드바이저 수만큼 프록시를 생성해야 한다.

 

 

  • 스프링에서 하나의 프록시에 여러 어드바이저를 적용할 수 있도록 기능을 제공한다.
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
public void multiAdvisorTest2() {
    DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
    DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());

    //프록시 생성
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory1 = new ProxyFactory(target);

    proxyFactory1.addAdvisor(advisor2);
    proxyFactory1.addAdvisor(advisor1);
    ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();

    //실행
    proxy.save();
}
  • 하나의 프록시에 여러 어드바이저 적용 예제
    • 예제 흐름 : client -> proxy -> advisor2 -> advisor1 -> target
    • proxyFactory.addAdvisor()를 통해 하나의 프록시에 원하는 수만큼 어드바이저를 등록할 수 있다.
      • 등록하는 순서대로 어드바이저가 호출된다.

 

  • 하나의 프록시에 여러 어드바이저를 등록하면 여러 프록시를 사용하는 것보다 성능은 좋고 결과는 같다.

 

  • 스프링은 AOP를 적용할 때 최적화를 위해 프록시는 하나만 생성하고 하나의 프록시에 여러 어드바이저를 적용한다.
    • 하나의 target에 여러 AOP가 동시에 적용되어도 스프링 AOP는 target마다 하나의 프록시만 생성한다.

 

  • 현재 방식의 문제점
    • 설정 파일이 지나치게 많다.
      • 애플리케이션에 스프링 빈이 100개 있고 프록시로 부가 기능을 적용하려면 100개의 동적 프록시 생성 코드를 만들어야 한다.
    • 컴포넌트 스캔
      • 컴포넌트 스캔을 사용하는 경우 실제 객체를 빈으로 등록하기 때문에 프록시 적용이 불가능하다.
  • 이런 문제점들을 모두 해결하는 방법이 빈 후처리기이다.

정리

  • 프록시 팩토리 : 스프링에서 제공하는 동적 프록시를 통합해서 편리하게 만들어주는 기능이다.
    • 동작 방식 - 인터페이스가 있으면 JDK 동적 프록시를 사용, 구체 클래스만 있으면 CGLIB를 사용한다.
    • 부가 기능 적용 시 Advice를 만들면 InvocationHandler나 MethodInterceptor를 신경쓰지않아도 된다.
      • Advice는 프록시에 적용하는 부가 기능 로직으로 InvocationHandler와 MethodInterceptor 개념과 유사하며 개념적으로 둘을 추상화한 것이다.
      • Advice는 MethodInterceptor를 구현해서 만든다.

 

  • 스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass(true)로 설정해서 사용한다.
    • 인터페이스가 있더라도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 동적 프록시 생성한다.

 

  • 포인트컷(Pointcut) : 어디에 부가 기능을 적용할지, 적용하지 않을지 판단하는 필터 로직이다.
  • 어드바이스(Advice) : 프록시가 호출하는 부가 기능(=프록시 로직)이다.
  • 어드바이저(Advisor) : 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.
    • 어드바이저 = 포인트컷1 + 어드바이스1
  • 부가 기능을 적용할 때 포인트컷으로 어디에 적용할지 정하고 어드바이스로 어떤 로직을 적용할지 정하며 어디에, 어떤 로직을 정할지 모두 알고 있는 것이 어드바이저이다.

 

  • 스프링이 제공하는 ProxyFactory를 사용하면 하나의 프록시에 여러 어드바이저를 등록해서 사용할 수 있다.
  • 스프링은 AOP를 적용할 때 최적화를 위해 프록시는 하나만 생성하고 하나의 프록시에 여러 어드바이저를 적용한다.
    • 하나의 target에 여러 AOP가 동시에 적용되어도 스프링 AOP는 target마다 하나의 프록시만 생성한다.

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

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

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com