Back-End/Spring Advance & Boot

스프링 고급편 - 스프링이 지원하는 프록시 (프록시 팩토리)

Meluu_ 2024. 9. 25. 14:22

✔️ 프록시 팩토리


 

스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory) 기능을 제공

프록시 팩토리 하나로

인터페이스의 경우 JDK 동적 프록시를,

구체 클래스의경우CGLIB를 적용해준다. 

 

그런데 동적 프록시는 InvocationHandler가 , CGLIB는 MethodInterceptor 로 부가기능을 적용하였는데 

프록시팩토리를 사용해도따로 만들어야할까?

 

스프링은 이 문제를 해결하기 위해 부가기능을 적용할 때 Advice   라는 새로운 개념을 도입했다.

프록시 팩토리를 사용하면 Advice를 호출하는 전용 InvocationHandler, MethodInterceptor 를 내부에서 사용한다.

 

 

 

Advice 만들기

Advice는 프록시에 적용하는부가 기능 로직이며,

InvocationHandler, MethodInterceptor  개념과 유사하다. (둘을 개념적으로 추상화 한 것)

따라서 프록시 팩토리를 사용하면 둘 대신 Advice 를 사용하면 된다.

 

 

MethodInterceptor - 스프링이 제공하는 코드

package org.aopalliance.intercept;

public interface MethodInterceptor extends Interceptor {
	Object invoke(MethodInvocation invocation) throws Throwable;
}

Interceptor는 Advice 인터페이스를 상속

invaction : 내부에 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보 등이 포함

CGLIBMethodInterceptor와 이름이 같으므로 패키지 이름 주의

 

 

TimeAdvice 

@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 클래스를 호출하고 그 결과를 반환
  • target 클래스 정보MethodInvaction invocation 안에 모두 포함되어있다. 
    • 그 이유는 프록시 팩토리 생성 단계에서 이미 target 정보를 파라미터로 전달받기 때문

 

인터페이스

@Slf4j
public class ProxyFactoryTest {

    @Test
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl(); // 타겟 생성
        ProxyFactory proxyFactory = new ProxyFactory(target); // 프록시 팩토리를 생성 (타겟을 파라미터로 전달)
        proxyFactory.addAdvice(new TimeAdvice()); // advice 추가
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy(); // getProxy()로 프록시 반환

        proxy.save(); // 호출
    }
}

 

 

구체 클래스

@Slf4j
public class ProxyFactoryTest {

    @Test
    void concreteProxy() {
        ConcreteService target = new ConcreteService(); // 타겟 생성
        ProxyFactory proxyFactory = new ProxyFactory(target); // 프록시 팩토리 생성 (타겟 파라미터 전달)
        proxyFactory.addAdvice(new TimeAdvice()); // advice 추가 
        ConcreteService proxy = (ConcreteService) proxyFactory.getProxy(); // 프록시 반환

        proxy.call(); // 호출 및 사용 
    }
}

 

 

ProxyTargetClass 옵션 사용 (인터페이스도 CGLIB로 클래스 기반 프록시 사용)

@Slf4j
public class ProxyFactoryTest {

    @Test
    void proxyTargetClass() {
        ServiceInterface target = new ServiceImpl(); // 타겟 생성
        ProxyFactory proxyFactory = new ProxyFactory(target); // 프록시 팩토리 생성 (타겟 파라미터 전달)

        // 타켓 클래스를 대상으로, 실무에서 사용함 중요!
        proxyFactory.setProxyTargetClass(true);  // true = 인터페이스 -> CGLIB 사용
        proxyFactory.addAdvice(new TimeAdvice()); // advice 추가
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy(); // 프록시 반환

        proxy.save(); // 호출
    }
}

 

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

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

 

참고

스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true 로 설정해서 사용

 

 

 

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


포인트 컷(Pointcut) : 부가 기능 적용 대상 필터

  • 주로 클래스와 메서드 이름을 필터링
  • 이름 그대로 어떤 포인트(point)에 기능을 적용할지 말지를 잘라서(cut) 구분

어드바이스(Advice) : 프록가 호출하는 부가 기능 (프록시 로직) 

어드바이저(Advisor) : 포인트컷1 + 어드바이스 1

  • 어디에? 어떤 로직을? 모두 알고 있는 것이 어드바이저

 

어드바이저를사용함으로써 역할과 책임이 명확하게 분리됨

 

사용

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); // advisor 추가
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy(); // 프록시 반환

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

앞서 알아봤던 프록시 팩토리에서 advice만 추가하는 것이 아닌

advisor를 만들고 프록시 팩토리에 advisor를 추가한다.

 

DefaultPointcutAdvisor : Advisor 인터페이스의 가장 일반적인 구현체로, 생성자를 통해 포인트 컷과 어드바이스 하나를 넣어주면 된다.

 

Pointcut.TRUE : .항상 True를 반환하는 포인트 컷

 

 

 

✔️ 직접 만든 포인트 컷


포인트 컷 관련 인터페이스 - 스프링 제공 

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

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

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

 

포인트 컷은 크게 ClassFilterMethodMatcher 둘로 이루어진다. 

클래스 이름이 맞는지, 메서드가 맞는지확인할 때 사용한다.

둘 다 true를 반환해야 어드바이스 적용 가능

 

 

구현

static class MyPointcut implements Pointcut {

    @Override
    public ClassFilter getClassFilter() {
        return ClassFilter.TRUE; // 여기에 클래스 필터를 반환하면 된다.
    }

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

@Slf4j
static class MyMethodMatcher implements MethodMatcher {

    private String matchName = "save";
    
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        boolean result = method.getName().equals(matchName);
        return result;
    }


    // 밑에 두개는 무시해도 됨
    @Override
    public boolean isRuntime() {
        return false;
    }

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

 

 

 

포인트 컷 필터를 통과하지 못할  경우 proxy는 advice를 적용하지 않고 target의 메서드를 호출한다. 

 

 

 

 

 

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


스프링은 우리가 필요한 포인트 컷을 이미 대부분 제공

 

 

public class AdvisorTest {
    @Test
    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  : 이름 기반 매칭으로 필터링한다. 

setMappedNames(String mappedNamePattern) :  파라미터로 메서드 이름을 지정하면 된다.

 

 

스프링은 사실 무수히 많은 포인트 컷을 제공한다.

대표적으로 

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

실무에[서 사용하기 편하고 기능도 많은 aspectJ 표현식을 많이사용한다고 한다. 

 

 

 

 

✔️ 하나의 프록시, 여러 어드바이저


하나의 target에 여러 어드바이저를 적용하기 위해 프록시를 여러개 만들 필요 없다. 

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

 

 

 

@Test
void multiAdvisorTest2() {
    // client -> proxy2(advisor2) -> proxy1(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 proxy1 = (ServiceInterface) proxyFactory1.getProxy();
    proxy1.save();
}

 

 

중요

스프링은 AOP를 적용할 때 최적화를 진행해서 지금처럼 프록시는 하나, 하나의 프록시에 여러 어드바이저를 적용

정리하면 스프링 AOP는 target마다 하나의 프록시 생성한다.

 

 

 

 

 

정리

애플리케이션에 적용하는 파트는 적지 않았지만 문제점이 있다.

프록시 팩토리를 적용하는데 너무 많은 설정이 필요하다.

컴포넌트 스캔의 경우 프록시 적용이 불가능하다.

 

따라서 이 두가지를 해결해줄 빈 후처리기 에 대한 학습을 한다.

 

 

 

🔖 학습내용 출처


스프링 핵심 원리 - 고급편 / 김영한