개발 이론/Spring

프록시??? 스프링 AOP?? 트랜젝션?? (3)

dal_been 2024. 3. 12. 23:31
728x90

지난 블로그에서 동적프록시에 대해서 이야기 해봤다. 오늘은 프록시 팩토리에 대해서 이야기해볼예정이다.

 


앞선 블로그를 정리해보자면

인터페이스가 있는 경우 JDK 동적 프록시가 적용되고, 그렇지 않은 경우 CGLIB가 적용된다.

근데 여기서 궁금한점이 있다.

 

누가 인터페이스 유무를 인지해서 프록시를 생성할까??

만약 JDK 동적 프록시, CGLIB프록시를 모두 사용하면 각각 InvocationHandler, MethodInterceptor를 만들어서 관리해야하는가??

또는 If문과 같이 어쩔때는 적용하고 안하고 싶다면 어떻게 해야할까?

 

ProxyFactory

 

스프링이 ProxyFactory를 통해서 동적 프록시를 통합하여 편리하게 만들어주는 기능을 제공한다.

뿐만 아니라 Advice라는 개념을 통해 InvocationHandler, MethodInterceptor를 신경쓰지 않고 부가기능을 적용할 수 있게 해준다.

또한..! PointCut라는 개념을 통해 조건에 대한 부가기능을 적용해준다.

 

 

일단 ProxyFactory에 대한 예제를 먼저 봐보자.

 

여기서 ServiceInterface는 인터페이스이고 ServiceImpl는 해당 구현체이다. 해당 구현체 안에는 save(),find()메서드가 로그를 찍는 행위를 수행하고 있다.

 

일단 ProxyFactory를 보기전에 Advice구현을 통해 부가기능을 넣어줘야한다.

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class HelloAdivce implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("HelloAdivce 실행");
        long startTime = System.currentTimeMillis();

        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("HelloAdivce 종료 resultTime={}", resultTime);
        return result;
    }
}

 

MethodInterceptor를 구현하여 부가기능을 추가한다.

여기서 invocation.proceed가 target클래스를 호출하고 그결과를 얻는다.

다만 target클래스에 대한 정보가 없는데 그것은 invocation안에 포함되어있으며 Proxy Factory 생성시 지정해주면 된다.

 

    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new HelloAdivce());
        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();
    }

 

 

ProxyFactory proxyFactory = new ProxyFactory(target);
  • ProxyFactory를 생성할때 target클래스를 넘겨준다. 만약 해당 인스턴스가 인터페이스면 JDK 동적프록시, 아니면 CGLIB를 통해 프록시를 생성한다.
proxyFactory.addAdvice(new HelloAdvice());
  • ProxyFactory에 프록시가 사용할 부가기능 로직을 설정해준다. (Adivce가 InvocationHandler와 MethodInterceptor개념과 유사하다)

이후 getProxy()를 통해 프록시 객체를 생성한다.

밑에 assertThat에서

  • isAopProxy는 프록시가 생성되었는지 확인 (JDK, CGLIB상관없음)
  • isJdkDynamicProxy Jdk프록시인가
  • isCglibProxy CGLIB 프록시인가

 

 

여기서 추가적으로 만약 Jdk프록시가 아니라 CGLIB프록시로 만들고 싶다면

proxyFactory.setProxyTargetClass(true);

 

설정해주면 강제로 CGLIB프록시로 생성해준다.

 

 

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

 

  • 포인트 컷 : 어디에 부가기능을 적용할지 필터링 기능으로 클래스나 메서드 이름으로 필터링한다.
  • 어드바이스 : 프록시 부가기능을 설정하는 것
  • 어드바이저 : 어디바이스 + 포인트 컷을 말함

이렇게 역할이 나눠져있는 이유는 말그대로 역할과 책임을 나누기 위해서다

포인트컷은 필터링 기능, 어드바이스는 로직 

 

일단 어드바이저 -> 하나 포인트컷 + 하나 어드바이스로 예제를 만들어보자

 

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

        proxy.save();
        proxy.find();
    }

 

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new HelloAdvice());​
  • Advisor 인터페이스 구현체로 생성자를 통해 포인트컷과 어드바이스를 넣어주면 된다.
  • Pointcut.TRUE의 경우 항상 true를 반환하는 포인트 컷이다
proxyFactory.addAdvisor(advisor);
  • ProxyFactory에 등록해준다

정리하면 ProxyFactory에서 Advisor에 등록한 PointCut와 Adivce를 실행한다.

 

Pointcut 생성하기

 

그러면 이제 Pointcut를 만들어보자

save메서드를 호출할때마다 어드바이스 로직이 수행되도록 만들어보자.

public interface Pointcut {
      ClassFilter getClassFilter();
      MethodMatcher getMethodMatcher();
}
  public interface ClassFilter {
      boolean matches(Class<?> clazz);
}
  public interface MethodMatcher {
      boolean matches(Method method, Class<?> targetClass);
      //..
}

 

포인트 컷은 크게 ClassFilter와 MethodMatcher로 이루어진다.

ClassFilter는 클래스가 맞는지, MethodMAtcher는 메서드가 맞는지 확인한다. 둘다 true여야지 어드바이스가 적용된다.

 

    @Test
    @DisplayName("직접 만든 포인트컷")
    void advisorTest2() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new HelloAdvice());
        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;
        }

        @Override
        public boolean matches(Method method, Class<?> targetClass, Object... args) {
            return false;
        }
    }

 

여기서 일단 ClassFilter는 true만 나오게 하였다. MyMethodMatcher를 좀더 자세하게 보자

 

@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;
}
  • 이를 통해 machName과 메서드 이름이 동일한지 확인한다
@Override
public boolean isRuntime() {
    return false;
}

@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
    return false;
}
  • 만약 isRuntime이 true이면 matches메서드가 대신 호출된다. 즉 동적으로 넘어오는 매개변수를 기준으로 판단하겠다는 말이다
  • isRuntime이 false인경우 클래스의 정적 정보만 사용하기 때문에 스프링 내부에서 캐싱되어 성능이 항샹되지만, true인 경우 매개변수에 따라 동적으로 판단하기 때문에 캐싱하지 않는다

 

결과를 보면 "HelloAdivce 실행" 이 save()호출할때마다 로그가 찍힌걸 볼 수 있다.

 

 

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

 

    @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 HelloAdvice());
        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();
    }

 

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save");
  • NameMatchMethodPointcut를 생성하고 setMappedNames로 메서드 이름을 지정하면 포인트컷 완성

 


Advisor 여러개

 

하나의 target에 Advisor를 여러개 만들고 싶다면 어떻게 해야할까? 매번 프록시를 여러개 만들어야할까?

물론 잘 작동은 하겠지만 Advisor가 100개면 프록시를 100개를 생성해야한다.

 

스프링은 하나의 프록시에 여러 Advisor를 적용할 수 있게 도와준다.

 

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

    }

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

 

이렇게 ProxyFactory에 여러 Advisor를 등록해주면 된다.

그럼 여기서 등록된 순서대로 Advisor가 호출된다.

 

 


 

프록시 팩토리에 대한 이야기는 여기서 마무리..

다음은 빈 후처리기 내용인데.. 사실 공부하다보니 결국에는 트랜젝션이 어떻게 프록시와 관련이 있는건지 더 궁금해졌다...

 

 

https://yejun-the-developer.tistory.com/7

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

(김영한님 "스프링 핵심원리 - 고급편 "강의를 마니 참고함)