WEB/Spring

스프링 핵심 원리 - 고급편 5) 스프링이 지원하는 프록시

Tony Lim 2022. 6. 9. 10:37
728x90

ProxyFactory

인터페이스가 있을때 -> JDK동적프록시 (InvocationHandler)

인터페이스가 없을때 -> CGLIB(MethodInterceptor)

둘다 구현하기는귀찮다. 특정 조건이 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면한다.

 

추상화 레이어인 ProxyFacotry가 생겼고 InvocationHandler, MethodInterceptor 를 추상화한 Advice가 제공된다.

Advice = 조언 , 프록시가 제공하는 부가 기능 로직

Factory안에서 우리가 주입해준 Advice 를 CGLIB,JDK Dynamic을 알아서 구분하여 호출 하게 된다.

 


 

ProxyFactory + Advice 예제

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

MethodInterceptor는 Advice를 구현하고 있다.

MethodInvocation에 target 클래스의 정보가 모두 포함되어있어 proceed()를 호출하면 target을 호출하는것과 같다.

 

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

new ProxyFactory에 넘겨준 인스턴스를 보고 인터페이스의 유무에 따라 jdk, CGLIB를 사용하여 프록시를 생성하게 된다.

setProxyTargetClass를 true로 하면 인터페이스가 있어도 CGLIB 를 사용하여 프록시를 생성해준다.

ProxyFactory를 쓰면 AopUtils 스프링이 제공해주는 기능을 쓸 수 있다.

 


 

AOP 단어정리

Pointcut = 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는것

Advice = 프록시가 호출하는 부가기능 로직, 조언!

Advisor = Pointcut + Advice 1세트를 의미함 == 조언자 (어디에 어떤 조언을 해야하는지 알고 있음)

프록시가 일종의 필터역할을 하게 된다. 부가 기능을 적용할지 말지~

 


 

예제

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

위에서 proxyFactory.addAdvice(new TimeAdvice()) 를 보면 어드바이스를 바로 적용한것 같지만 내부적으로 들어가면 Pointcut.True를 기본으로 가지고 있다. 
즉 내부적으로 advisor 만들고 getProxy 때 proxy를 준것이다.

 


 

Pointcut - ClassFilter , MethodMatcher

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

isRuntime이 false 면 위에 메소드가 호출된다. 정적인 정보이기에 캐싱을 할수 있다. 성능상의 장점이 잇다.

true이면 아래 메소드가 호출된다. 매개변수가 동적으로 변경(args) 되기 떄문에 캐싱이 힘들어 성능을 올리기 힘들다.

 

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());

해당 구현체를 기본 Pointcut대신 넣어주면 적용이된다.

 

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());

스프링에서 제공해주는 기본 Pointcut 중에 NameMatchMethodPointCut이 존재한다. AspectJ를 쓰면 훨씬 다양하고 정교한 포인트컷을 사용할 수 있다.

 

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

}

하나의 target에 하나의 프록시를 생성하고 여러개의 Advisor를 적용하는게 정석이다.

AOP적용수 만큼 프록시가 생성되는 것이 아니다.

 

실제 앱에서 적용할시에 일일이 @Configuration 파일을 만들어서 ProxyFactory를 통해 proxy를 return 하게 만들어줘야한다.

 

 

 

728x90