[Spring & Java] Proxy 를 코드로 이해하기(2)

식빵·2022년 7월 31일
1

Spring Lab

목록 보기
16/33
post-thumbnail

이전 글 링크: https://velog.io/@dailylifecoding/spring-and-java-proxy-usage-1

이전 글에서는 ProxyFactory 를 사용해서 프록시를 편하게 생성하는 방법을 알아봤다.
그런데 여태 하면서 AOP 와 관련된 어떤 용어를 쓴 건 본적이 없을 것이다.

이번에는 AOP 에서 자주 쓰는 용어들과 그 용어와 관련된 구현체들을 사용해서
ProxyFactory 를 사용해보자.



🥝 AOP 용어 몇가지 알아두기

AOP 에는 다양한 용어가 쓰이지만, 이번 실습에서 필요한 3가지만 알고 넘어가겠다.

1. Pointcut

포인트컷(Pointcut)는 부가기능을 어디에 적용할지를 판단하는 기준이다.

2. Advice

어드바이스(Advice)는 프록시가 사용하는 부가기능(로직)이다.

3. Advisor

어드바이저(Advisor)는 1개의 Pointcut + 1개의 Advice 을 하나로 묶은 것이라 생각하면 된다.


지금부터 프록시 부가기능(Advice)프록시 부가기능 적용지점(Pointcut)을 모두 담고 있는
Advisor 를 ProxyFactory 에 적용해 볼 것이다.




🥝 ProxyFactory + AOP

용어는 대충 알았으니 이제 저 용어와 관련된 구현체들을 ProxyFactory 에 적용해보자.


🥥 DefaultPointcutAdvisor 사용

스프링에서는 DefaultPointcutAdvisor 라는 것을 제공해서 편하게
Advisor 를 생성할 수 있다. 한번 ProxyFactory 에 적용해보자.

// 일부 import 생략...
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

class AdvisorTest {

    @Test
    void proxyFactoryWithAdvisorTest() {
        User target = new UserImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);

        // 1번째 인자. Pointcut.TRUE : 프록시 부가기능이 모든 곳(여기서는 타겟 객체의
        //                            모든 메소드)에 동작한다.
        // 2번째 인자. 부가기능을 의미하는 Advice 인터페이스 구현체를 넣는다.
        DefaultPointcutAdvisor advisor
                = new DefaultPointcutAdvisor(Pointcut.TRUE, new MyAdvice());

        proxyFactory.addAdvisor(advisor);
        User proxy = (User) proxyFactory.getProxy();
        proxy.say("i am advisor Test");
    }


    // MethodInterceptor 는 Advice 를 상속하는 인터페이스다.
    static class MyAdvice implements MethodInterceptor {

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            System.out.println("MyAdvice intercept [START]");
            Object result = invocation.proceed();
            System.out.println("MyAdvice intercept [END]");
            return result;
        }
    }
}

출력 결과

MyAdvice intercept [START]
word = i am advisor Test
MyAdvice intercept [END]




🥥 Custom Pointcut

위에서는 Pointcut.TRUE 를 사용해서 모든 지점(여기서는 타겟 객체의 모든 메소드)에 프록시 부가기능이 동작하도록 했다. 하지만 Pointcut 을 직접 만들어서 특정 메소드에만
부가기능이 동작하도록 할 수 있다.

Pointcut 인터페이스는 아래와 같은 메소드 시그니처를 갖고 있다.

보면 알겠지만 Class, Method 단위로 필터링 할 수 된다는 것을 알 수 있다.
지금부터 직접 이 Pointcut을 구현하고 적용해보자.

일반적으로는 스프링이 이미 구현한 Pointcut 클래스를 쓰지만,
배워가는 과정이니 직접 만들어보겠다.


static class CustomMethodMatcher implements MethodMatcher {

    private String matchingMethodName = "say";

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        return method.getName().equals(matchingMethodName);
    }

    @Override
    public boolean isRuntime() {
        return false;
    }

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

static class MyPointcut implements Pointcut {

    /**
     * 타겟 객체가 어떤 클래스든 프록시 대상 후보!
     */
    @Override
    public ClassFilter getClassFilter() {
        return ClassFilter.TRUE;
    }

    /**
     * 타겟 객체의 특정 메소드만 프록시의 대상이 되도록 설정
     */
    @Override
    public MethodMatcher getMethodMatcher() {
        return new CustomMethodMatcher();
    }
}


@Test
void pointcutTest() {
    User target = new UserImpl(1L, "dailyCode", "dailyCode");
    ProxyFactory proxyFactory = new ProxyFactory(target);
    
    // NameMatchMethodPointcut 라는 스프링이 이미 구현한 포인트 컷 사용도 가능
    DefaultPointcutAdvisor advisor
            = new DefaultPointcutAdvisor(new MyPointcut(), new MyAdvice());
    proxyFactory.addAdvisor(advisor);
    User proxy = (User) proxyFactory.getProxy();
    
    // 프록시 적용
    proxy.say("i am advisor Test");

    // 프록시 미적용
    proxy.whoAmI();
}

참고:
MethodMatcher 인터페이스의 아래 메소드는 주제에서 벗어남으로 설명을 생략하겠다.

  • isRuntime() 메소드
  • matches(Method method, Class<?> targetClass, Object... args) 메소드

다만 isRuntime 의 반환값에 따라 다른 matches 가 동작한다는 점만 알자.

  • isRuntime 이 true 일 경우 :
    • matches(Method method, Class<?> targetClass, Object... args) 실행
  • isRuntime 이 false 일 경우:
    • matches(Method method, Class<?> targetClass) 실행

출력 결과

MyAdvice intercept [START]
word = i am advisor Test
MyAdvice intercept [END]
i am dailyCode             // whoAmI 메소드는 프록시 미적용!!

잠시 프록시 객체의 요청 처리 흐름을 정리하고 넘어가자.

말로 풀자면 이렇다.

  • 프록시에 요청이 오면 프록시는 자신이 갖고 있는 Advisor 확인한다.
  • Advisor 에서 Pointcut 에 부합하는 것이면 Advice(부가기능)을 수행하고, 타겟 호출
  • Advisor 에서 Pointcut 에 부합하지 않으면 곧바로 타겟을 호출




🥥 참고) 스프링의 포인트컷

위에서 작업한 내용은 사실 아래처럼 간단하게 구현할 수 있다.
이래서 스프링이 이미 구현한 클래스를 사용하는 게 편리하다.

// import org.springframework.aop.support.NameMatchMethodPointcut

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("say");

DefaultPointcutAdvisor advisor
        = new DefaultPointcutAdvisor(pointcut, new MyAdvice());

스프링은 이외에도 아래와 같은 포인트 컷을 제공한다.

  • JdkRegexpMethodPointcut
  • TruePointcut
  • AnnotationMatchingPointcut
  • AspectJExpressionPointcut ==> (가장 실무에서 많이 보는 녀석)




🥥 ProxyFactory & multi Advisor

그런데 위처럼 하나의 프록시에 하나의 Advisor 만 적용했다.
이번에는 하나의 proxy 에 1개 이상의 Advisor 를 적용해보자.
ProxyFactory 를 사용하면 이게 가능하다!

가볍게 코드를 짜서 동작하는 것을 확인하고 넘어가자.


// 적용할 부가기능 1
static class MyAdvice1 implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("START of MyAdvice1 Intercept!!!");
        Object result = invocation.proceed();
        System.out.println("END of MyAdvice1 Intercept!!!");
        return result;
    }
}

// 적용할 부가기능 2
static class MyAdvice2 implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("START of MyAdvice2 Intercept!!!");
        Object result = invocation.proceed();
        System.out.println("END of MyAdvice2 Intercept!!!");
        return result;
    }
}


@Test
void multiAdvisorTest() {

	// 포인트컷 하나 생성, say 메소드에만 프록시 적용하도록 하기 위함.
    NameMatchMethodPointcut myPointcut = new NameMatchMethodPointcut();
    myPointcut.setMappedName("say");

	// Advisor 생성 - 
    DefaultPointcutAdvisor advisor1 
    	= new DefaultPointcutAdvisor(myPointcut, new MyAdvice1());
        
    DefaultPointcutAdvisor advisor2 
    	= new DefaultPointcutAdvisor(Pointcut.TRUE, new MyAdvice2());

	// 타겟 객체 생성
    UserImpl target = new UserImpl(1L, "dailycode", "dailycode");
    
    // 타겟 객체 정보를 갖는 ProxyFactory 생성
    ProxyFactory proxyFactory = new ProxyFactory(target);
    
    // Multi Advisor 적용
    proxyFactory.addAdvisor(advisor1);
    proxyFactory.addAdvisor(advisor2);

	// 프록시 객체 생성
    User proxy = (User) proxyFactory.getProxy();

    // advisor1, advisor2 모두 적용
    proxy.say("wow");
    
    // advisor2 만 적용
    proxy.whoAmI();
}

출력 결과

START of MyAdvice1 Intercept!!!
START of MyAdvice2 Intercept!!!
word = wow
END of MyAdvice2 Intercept!!!
END of MyAdvice1 Intercept!!!
START of MyAdvice2 Intercept!!!
i am dailycode
END of MyAdvice2 Intercept!!!
  • say 메소드에는 2개의 부가기능 모두 적용
  • whoAmI 메소드에는 1개의 부가기능만 적용

왜 이렇게 동작하는지는 이제 다 알거라고 생각하고 넘어가겠다.



👏 마치며

내가 생각하는 Proxy 생성법은 이게 다인 거 같다.
그런데 사실 여태 쓴 코드를 우리가 직접 작성할 일은 거의 없을 것이다.

왜냐하면 스프링 프레임워크가 제공하는 @Aspect자동 프록시 생성기
사용할 것이기 때문이다.

하지만 이 자동 프록시 생성기도 결국은 내부적으로 ProxyFactory를 사용한다.
그러니 여태 배운 게 헛된 것은 절대로 아니다.

기회가 된다면 다음에는 AOP 뭔지 가볍게 알아보고,
스프링 프레임워크에서 제공하는 @Aspect자동 프록시 생성기를 사용하는 방법도
테스트 코드와 설명으로 게시물을 작성해보겠다.

아무튼 오늘은 여기까지 😋
에구 힘들다...

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글