제육's 휘발성 코딩
article thumbnail
반응형

프록시 팩토리

프록시 팩토리스프링에서 동적 프록시를 통합해서 관리해주는 클래스 팩토리이다.

프록시 팩토리를 사용하면, 인터페이스가 있을 경우 JDK 동적 프록시를 사용하고, 구체 클래스만 있으면 CGLIB 동적 프록시를 사용하도록 처리해준다.

프록시 팩토리 흐름도

  • client의 요청이 오면 대상 Target 클래스의 정보를 파악하여 동적 프록시를 선택하여 반환해준다.
  • JDK 동적 프록시는 InvocationHandler, CGLIB 동적 프록시는 MethodInterceptor를 각각 구현해야 하는데, 스프링에선 Advice라는 개념을 통해 공통 구현을 할 수 있도록 지원한다.

 

Advice

개발자가 Advice를 구현하게 되면 동적 프록시 별로 요청오는 핸들러의 정보를 받아 실제 타겟의 로직을 실행시켜 준다.

즉, Advice는 프록시에 적용하는 부가 기능 로직으로 두 동적 프록시를 추상화한 개념이다.

Advisor 적용 - Advice, Pointcut 1개 씩 보유

  • Pointcut : 포인트 컷은 AOP에서 주로 클래스와 메서드 이름으로 필터로직을 하며, 기능을 적용할지 여부를 지정하는 역할을 한다.
  • Advisor : 어드바이저는 하나의 포인트 컷과 하나의 어드바이저를 합친 것을 의미한다.

 

MethodInterceptor - org.aopalliance.interceptor

@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
    @Nullable
    Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
  • MethodInvocation에는 다음 메서드 호출방법, 현재 프록시 객체 정보, 파라미터, 메서드 정보 등이 내장되어 있다.
  • CGLIBMethodInterceptor와 이름이 같으므로 패키지 경로를 주의해서 사용하자.

 

TimeAdvice - MethodInterceptor 구현 클래스

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@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;
  }
}
  • invocation.proceed() 를 호출하면 target 클래스를 호출하고 결과를 반환한다. MethodInvocation 에 타겟 클래스의 정보가 모두 포함되어 있다.

 

ProxyFactoryTest - ProxyFactory 프록시 생성 결과 테스트

import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ConcreteService;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import static org.assertj.core.api.Assertions.*;

@Slf4j
public class ProxyFactoryTest {

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

      proxy.save();

      assertThat(AopUtils.isAopProxy(proxy)).isTrue();
      assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
      assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
  }

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

  @Test
  void proxyTargetClass() {
      ServiceInterface target = new ServiceImpl(); //인터페이스 사용
      ProxyFactory proxyFactory = new ProxyFactory(target);
      proxyFactory.setProxyTargetClass(true); // 강제로 CGLIB 프록시로 변경
      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.isCglibProxy(proxy)).isTrue();
      assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
  }
}
  • new ProxyFactory(target) : 프록시 팩토리를 생성할 때, 호출 대상도 같이 넘겨준다.
    • 이 때 대상 인스턴스의 정보를 확인하여 인터페이스가 있으면 JDK 프록시, 없으면 CGLIB 프록시를 채택한다.
  • addAdvice(new TimeAdvice()) : 프록시 팩토리를 통해 부가 로직을 추가한다. (뒤에 나오지만, 사실은 어드바이스가 아닌 어드바이저를 add해야 한다. 편의 메서드로 어드바이스를 넣을 수 있게 구현되어있으며, 포인트 컷은 디폴트값이 들어간다.)
  • proxyTargetClass(true) : 인터페이스를 갖고 있는 인스턴스여도 강제로 CGLIB로 프록시로 만들도록 설정한다.

스프링 부트는 기본적으로 proxyTargetClass=true로 설정해서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성하도록 채택하였다.

 

Advisor

AdvisorTest - 어드바이저 테스트 (DefaultPointcutAdvisor)

import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

@Slf4j
public class AdvisorTest {

  @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();
  }
}
  • DefaultPointcutAdvisor는 어드바이저 인터페이스의 일반적인 구현체로, 생성자를 통해 하나의 포인트 컷과 어드바이스를 넣어주면 된다.
  • Pointcut.TRUE : 항상 true를 반환하는 포인트 컷
  • addAdvisor(advisor) : 프록시 팩토리를 적용하기 위해 반드시 넣어줘야할 어드바이저 정보

 

Pointcut

필터역할을 하는 포인트컷을 이용해서 이전 예제에 save() 메서드에는 어드바이스를 적용하고, find() 메서드에는 적용하지 않도록 구현해보자.

 

Pointcut 관련 인터페이스 - org.springframework.aop

public interface Pointcut {
    ClassFilter getClassFilter(); // 클래스 비교
    MethodMatcher getMethodMatcher(); // 메서드 비교
    Pointcut TRUE = TruePointcut.INSTANCE;
}
  • Pointcut은 크게 ClassFilterMethodMatcher 로 나뉜다. 두 비교 결과가 true를 반환하는 대상에만 어드바이스를 적용할 수 있다.

 

AdvisorTest - 직접 만든 포인트 컷

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

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; // true면 밑에 matches(...args) 가 호출된다. false는 클래스 정적 정보를 사용하여 스프링 내부에서 캐싱 가능, true는 동적 변경을 가정하기에 캐싱 불가능
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        throw new UnsupportedOperationException();
    }
}
  • Pointcut 인터페이스와 MethodMatcher 인터페이스를 구현체로 직접 만들어서 사용할 수 있다.

 

AdvisorTest - 스프링이 제공하는 포인트컷 (NameMatchMethodPointcut)

@Test
@DisplayName("스프링이 제공하는 포인트컷")
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();
}
  • NameMatchMethodPointcut에서 setMappedName() 을 사용하면 포인트컷 메서드명을 지정할 수 있다.

 

MultiAdvisorTest - 하나의 프록시에 여러 어드바이저 적용

@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 proxy = (ServiceInterface) proxyFactory1.getProxy();

    proxy.save();
}
  • ProxyFactory에 여러 어드바이저를 등록하면, 등록한 순서대로 호출 된다. 스프링에서 프록시는 지금처럼 AOP 적용 수만큼 프록시가 생성되는 것이 아닌, 하나의 프록시로 처리한다는 점을 기억하자.

 

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

스프링은 다양한 포인트 컷을 제공한다.

  • NameMatchMethodPointcut : 메서드 이름을 기반으로 사용한다. 내부에선 PatternMatchUtils 를 사용
  • JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷 매칭
  • TruePointcut : 항상 참을 반환
  • AnnotationMatchingPointcut : 어노테이션 기반으로 포인트컷 매칭
  • AspectJExpressionPointcut : aspectJ 표현식으로 매칭하며 가장 많이 사용되는 포인트컷 사용 방식이다.

 

정리

프록시 팩토리와 어드바이저를 사용하면서 편리하게 프록시를 생성할 수 있다.

다만, 아직 두 가지의 문제가 남아있다. 컨트롤러, 서비스, 레포지토리 등 프록시 팩토리를 적용하기 위해 프록시를 빈으로 등록하는 설정을 해야한다는 점과 컴포넌트 스캔을 사용하는 경우 프록시 적용 전에 스프링 빈으로 등록되기 때문에 적용이 안되는 문제다.

이 두 가지 문제를 빈 후처리기를 통해 한번에 처리할 수 있다. 빈 후처리기는 다음 포스팅에 정리하자.


본 포스팅은 인프런 - 스프링 핵심원리(고급편)을 참고하여 정리하였습니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^