프록시 팩토리

slee2·2022년 3월 16일
0

이전의 경우, 인터페이스를 사용할때는 InvocationHandler,
클래스를 사용할때는 MethodInterceptor를 사용하여 프록시를 적용하여 공통 로직 부분을 처리할 수 있었는데,
인터페이스와 클래스가 혼합해서 사용하는 경우가 많기 때문에 이를 둘다 사용해야한다는 점이 불편했다.

인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하기 위해서
스프링에서 프록시 팩토리(ProxyFactory)라는 기능을 제공한다.

스프링에서 Advice라는 개념을 도입하였다.
개발자는 이 Advice를 만들면,
InvocationHandlerMethodInterceptorAdvice를 호출하여 사용하게 된다.

특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 스프링은 Pointcut이라는 개념을 이용해서 해결할 수 있다.

예제 코드1

MethodInvocation invocation 에서
MethodInvocationInvocation을 상속하고 있어서 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보 등이 포함되어 있다.
MethodInterceptorCGLIBMethodInterceptor와 이름이 같으니 패키지 확인을 잘 할것
package org.aopalliance.intercept;
MethodInterceptor -> Interceptor -> Advice로 상속함.

Object result = invocation.proceed()

  • invocation.proceed()를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
  • 기본에 있던 target의 경우, MethodInvocation invocation에 포함되어있다.
  • 이는 처음에 프록시 팩토리로 프록시를 생성하는 단계에서 target 정보를 파라미터로 전달받기 때문에 가능하다.

new ProxyFactory(target): 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨준다. 프록시 팩토리는 이 인스턴스가 인터페이스인지 구체 클래스인지 확인하여 JDK 동적 프록시로 사용할지, CGLIB 동적 프록시로 사용할지 결정한 후에 생성한다.

여기서는 targetnew ServiceImpl() 인스턴스이므로 인터페이스가 있기 때문에 JDK 동적 프록시로 생성되었다.

proxyFactory.addAdvice(new TimeAdvice()): 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정한다. 이렇게 프록시가 제공하는 부가 기능 로직을 Advice라고 한다.

proxyFactory.getProxy() 프록시 객체를 생성하고 그 결과를 받는다.

예제 코드2

이제 CGLIB를 적용해볼 것이다.

CGLIB로 생성했기 때문에 isJdkDynamicProxy()isFalse()로 했을때, 참으로 나오는 모습

인터페이스가 있지만, CGLIB 동적 프록시를 이용하는 방법이다.
이때는 인터페이스가 있어도 이를 구현하는 클래스를 extend로 상속받아버린다.

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

  • 대상에 인터페이스가 있으면: JDK 동적 프록시, 인터페이스 기반 프록시
  • 대상에 인터페이스가 없으면: CGLIB, 구체 클래스 기반 프록시
  • proxyTargetClass=true: CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음

정리

  • 프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다.
  • 프록시의 부가 기능 로직도 특정 기술에 종속적이지 않게 Advice 하나로 편리하게 사용할 수 있었다. 이것은 프록시 팩토리가 내부에서 JDK동적 프록시인 경우 InvocationHandlerAdvice를 호출하도록 개발해두고, CGLIB인 경우 MethodInterceptorAdvice를 호출하도록 기능을 개발해두었기 때문이다.

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

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

AOP를 공부했다면 잘 보이는 단어라고 한다.
정리가 잘 안되는 단어들인데, 음 잘 정리해준다고 말씀하심.

포인트컷(Pointcut): 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다. 이름 그대로 어떤 포인트에 기능을 적용할지 하지 않을지 컷으로 구분하는 것.
어드바이스(Advice): 이전에 본 것처럼 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직이라 생각하면 된다.
어드바이저(Advisor): 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다. 쉽게 이야기해서 포인트컷1 + 어드바이스1이다.

쉽게 기억하기

  • 조언(Advice)을 어디(Pointcut)에 할 것인가
  • 조언자(Advicsor)는 어디에 조언해야할지 알고있다.

역할과 책임

  • 포인트컷은 대상 여부를 확인하는 필터 역할만 담당
  • 어드바이스는 부가 기능 로직만 담당
  • 둘을 합쳐 어드바이저

어드바이저

예제 코드1 - 어드바이저

new DefaultPointcutAdvisor: Advisor 인터페이스의 가장 일반적인 구현체. 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣는다.
Pointcut.TRUE: 항상 true를 반환하는 포인트컷.
new TimeAdvice(): 앞서 개발한 TimeAdvice 어드바이스르를 제공한다.
proxyFactory.addAdvisor(advisor): 프록시 팩토리에 적용할 어드바이저를 지정한다.
proxyFactory.addAdvice(new TimeAdvice())도 있었는데, 이 메서드 안을 보면,

이렇게 addAdvisor(pos, new DefaultPointcutAdvisor(advice))를 통해 결국 addAdvisor가 사용되는 것을 확인할 수 있다.
참고로 addAdvisor( , )

이렇게 파라미터가 하나인 addAdvisor() 메서드 안에도 있다.

예제코드2 - 직접 만든 포인트 컷

이제 save()에는 어드바이스 로직을 넣고 실행, find()에는 어드바이스 로직을 안넣고 실행으로 해보자.

이번에는 포인트컷을 직접 만들어보자.
포인트 컷은 크게 ClssFilterMethodMatcher 둘로 이루어진다.
하나는 클래스가 맞는지, 하나는 메서드가 맞는지이다.
둘다 true가 되야 어드바이스를 적용할 수 있다.

static class MyPointcut implements Pointcut {

        @Override
        public ClassFilter getClassFilter() {
            return ClassFilter.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

  • 직접 구현한 포인트컷.
  • 클래스 필터는 항상 true를 반환하고, 메서드 비교 기능은 MyMethodMatcher를 사용한다.

MyMethodMatcher

  • 직접 구현한 MethodMatcher이다.
  • matches(): 이 메서드에 method, targetClass정보가 넘어온다. 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단할 수 있다.
  • 여기서는 메서드 이름이 save인 경우에 true를 반환하도록 했다.
  • isRuntime(), matches(...args):
    • isRuntime() 값이 참이면 matches(...args)메서드가 대신 호출된다. 동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다.
    • isRuntime()false인 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만, isRuntime()true인 경우 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.
    • 중요X

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

스프링은 우리가 필요한 포인트컷을 대부분 제공한다.
NameMatchMethodPointcut을 사용할 것이다.

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

스프링은 무수히 많은 포인트컷을 제공한다.
대표적인 몇가지만 알아보자.

  • NameMatchMethodPointcut: 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtiles를 사용한다.
    • 예) *xxx* 허용
  • JdkRegexpMethodPointcut: JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
  • TruePointcut: 항상 참을 반환한다.
  • AnnotationMatchingPointcut: 어노테이션으로 매칭한다.
  • AspectJExpressionPointcut: aspectJ 표현식으로 매칭한다.

가장 중요한 것은 aspectJ 표현식
실무에서 사용하기도 편리하고 기능도 많은 aspectJ 표현식을 기반으로 사용하는 AspectJExpressionPointcut을 사용하게 된다.
aspectJ 표현식과 사용방법은 중요해서 이후 AOP를 설명할 때 자세히 설명

예제 코드4 - 여러 어드바이저 함께 적용

여러 어드바이저를 하나의 target에 적용해보자.

여러 프록시의 문제
이 방법이 잘못된건 아니지만, 프록시를 2번 생성해야한다는 문제가 있다.
적용해야 하는 어드바이저 개수만큼 프록시를 생성해줘야한다.

하나의 프록시, 여러 어드바이저
스프링은 이를 해결하기위해 하나의 프록시에 여러 어드바이저를 적용할 수 있게 만들었다.

등록하는 순서대로 호출된다.

중요 포인트
스프링 AOP를 처음 공부하거나 사용하면, AOP 적용 수만큼 프록시가 생서된다고 착각하는 경우가 많다고 한다.
스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
정리하면 하나의 target에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target마다 하나의 프록시만 생성한다. 이 부분을 꼭 기억해두자.

프록시 팩토리

적용1

이제 어플리케이션에 적용해보자.

V1을 먼저 적용해보자.

LogTraceAdvice

package hello.proxy.config.v3_proxyfactory.advice;

import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import java.lang.reflect.Method;

public class LogTraceAdvice implements MethodInterceptor {

    private final LogTrace logTrace;

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

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TraceStatus status = null;
        try {
            Method method = invocation.getMethod();
            String message = method.getDeclaringClass().getSimpleName() + "." +
                    method.getName() + "()";
            status = logTrace.begin(message);
            //로직 호출
            Object result = invocation.proceed();
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

ProxyFactoryConfigV1

package hello.proxy.config.v3_proxyfactory;

import hello.proxy.app.v1.*;
import hello.proxy.app.v2.OrderControllerV2;
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
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 ProxyFactoryConfigV1 {

    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
        return proxy;
    }

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
        ProxyFactory factory = new ProxyFactory(orderService);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
        return proxy;
    }

    @Bean
    public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
        OrderRepositoryV1Impl orderRepository = new OrderRepositoryV1Impl();

        ProxyFactory factory = new ProxyFactory(orderRepository);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
        return proxy;
    }

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

실행했을때, JDK 동적 프록시로 잘 생성된 것을 확인할 수 있다.

적용2

V2 ㄱㄱ

ProxyFactoryConfigV2

package hello.proxy.config.v3_proxyfactory;

import hello.proxy.app.v1.*;
import hello.proxy.app.v2.OrderControllerV2;
import hello.proxy.app.v2.OrderRepositoryV2;
import hello.proxy.app.v2.OrderServiceV2;
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
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 ProxyFactoryConfigV2 {

    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
        OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
        return proxy;
    }

    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
        OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderService);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
        return proxy;
    }

    @Bean
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
        OrderRepositoryV2 orderRepository = new OrderRepositoryV2();

        ProxyFactory factory = new ProxyFactory(orderRepository);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
        return proxy;
    }

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

V1과 코드 내용이 거의 비슷하다.

인터페이스 없이 구체 클래스로 만들어졌기 때문에 프록시가 CGLIB 동적 프록시로 생성된 것을 확인할 수 있다.

하지만,

남은 문제
일단 ProxyFactoryConfigV1, ProxyFactoryConfigV2 와 같은 설정파일을 일일이 만들어야 한다는게 불편하다.
그리고 가장 큰 문제는 @Controller, @Service 와 같이 컴포넌트 스캔을 이용하는 경우가 많은데 이 경우에는 이미 빈에 등록이 되기 때문에 프록시 적용을 할 수가 없다는 점이다.

이 문제들을 해결하기 위해 뒤에서 빈 후처리기라는 것을 배우게 된다.

0개의 댓글