[고급편] 스프링이 지원하는 프록시
포스트
취소

[고급편] 스프링이 지원하는 프록시

프록시 팩토리 - 소개

  • 우선 동적 프록시의 문제점을 다시 살펴보자.
    • 상황에 따라 다른 기술을 적용해야 한다.
      • 인터페이스가 있는 경우
        • JDK 동적 프록시
      • 인터페이스가 없는 경우
        • CGLIB
    • JDK 동적 프록시와 CGLIB를 함께 사용해야 한다면 각 기술마다 프록시를 관리해야 한다.
      • JDK 동적 프록시
        • InvocationHandler
      • CGLIB
        • MethodInterceptor
  • 이 떄 사용하는 것이 스프링에서 제공하는 프록시 팩토리다.
    • 프록시 팩토리는 상황에 따라 알아서 알맞는 동적 프록시 기술을 적용해준다.
    • 기본적으로 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다. (옵션 설정 가능)
      flowchart LR
        A[client] ----> B[ProxyFactory]
        B --[인터페이스 O]--> C[JDK 동적 프록시]
        B --[인터페이스 X]--> D[CGLIB]
      

어드바이스 (Advice)

  • 스프링은 InvocationHandler와 MethodInterceptor를 중복으로 생성해야 하는 문제점을 해결하기 위해 Advice라는 개념을 도입했다.
  • 이를 통해 개발자는InvocationHandler나 MethodInterceptor를 신경쓰지 않고, Advice 만 만들면 된다.
  • 프록시 팩토리를 사용하면 Advice 를 호출하는 전용 InvocationHandler와 MethodInterceptor를 내부에 서사용한다.
  • Advice는 프록시에 적용하는 부가 기능 로직이다.
  • Advice는 InvocationHandler와 MethodInterceptor를 추상적으로 개념화한 것이다.
  • Advice는 org.aopalliance.intercept 패키지에서 제공한다.
    • 커스텀 Advice를 만들 때는 해당 클래스를 구현하면 된다.
    • CGLIB의 MethodInterceptor와 이름이 같으니 패키지명을 주의하자.
    • org.aopalliance.intercept 패키지는 스프링 AOP 모듈(spring-aop) 안에 포함되어 있다.
    • 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보 등이 포함되어 있다.
    • 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.
    • MethodInterceptor는 Interceptor를 상속하고 Interceptor 는 Advice 인터페이스를 상속한다.
package org.aopalliance.intercept;
public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation invocation) throws Throwable;
}

포인트컷 (PointCut)

  • 단순하게 InvocationHandler와 MethodInterceptor를 하나로 관리하는 거라면 모든 곳에 강제로 적용된다.
  • 이를 위해 스프링은 PointCut이라는 개념을 도입하여 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공한다.
  • 스프링에서는 PointCut을 위한 인터페이스를 제공한다.
    • 포인트컷은 크게 ClassFilter와 MethodMatcher둘로 이루어진다.
    • ClassFilter는 클래스가 맞는지 확인할 때 사용한다.
    • MethodMatcher는 메소드가 맞는지 확인할 때 사용한다.
    • ClassFilter와 MethodMatcher는 true 로 반환해야 어드바이스를 적용할 수 있다.
public interface Pointcut {
    ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

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

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

프록시 팩토리 - 예제 코드1

Advice

  • invocation.proceed()를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
  • 이전의 InvocationHandler와 MethodInterceptor와 달리 target 클래스의 정보가 보이지 않는다.
    • target 클래스의 정보는 MethodInvocation invocation 안에 모두 포함되어 있다.
  • 프록시 팩토리로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받는다.
package com.example.common;

import lombok.extern.slf4j.Slf4j;
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={}ms", resultTime);
        return result;
    }
}

테스트 생성 및 실행 (JDK 동적 프록시)

  • 우선은 프록시 팩토리를 통해 JDK 동적 프록시를 적용하여 프록시를 생성해보자.
package com.example.proxyfactory;

import com.example.common.ServiceImpl;
import com.example.common.ServiceInterface;
import com.example.common.TimeAdvice;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
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.assertThat;

@Slf4j
public class ProxyFactoryTest {
    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl(); //프록시 호출 대상
        ProxyFactory proxyFactory = new ProxyFactory(target); //프록시 호출 대상 전달 및 프록시 팩토리 생성
        proxyFactory.addAdvice(new TimeAdvice()); //프록시 팩토리에 Advice 등록
        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(); //JDK 동적 프록시에 의한 프록시 생성 여부
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse(); //CGLIB에 의한 프록시 생성 여부
    }
}
  • interfaceProxy 실행 로그

    com.example.proxyfactory.ProxyFactoryTest – targetClass=class com.example.common.ServiceImpl
    com.example.proxyfactory.ProxyFactoryTest – proxyClass=class jdk.proxy3.$Proxy12
    com.example.common.TimeAdvice – TimeProxy 실행
    com.example.common.ServiceImpl – save 호출
    com.example.common.TimeAdvice – TimeProxy 종료 resultTime=0ms

프록시 팩토리 - 예제 코드2

테스트 생성 및 실행 (CGLIB)

  • 이번에는 프록시 팩토리를 통해 CGLIB를 적용하여 프록시를 생성해보자.
package com.example.proxyfactory;

import com.example.common.ConcreteService;
import com.example.common.ServiceImpl;
import com.example.common.ServiceInterface;
import com.example.common.TimeAdvice;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
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.assertThat;

@Slf4j
public class ProxyFactoryTest {
    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl(); //프록시 호출 대상
        ProxyFactory proxyFactory = new ProxyFactory(target); //프록시 호출 대상 전달 및 프록시 팩토리 생성
        proxyFactory.addAdvice(new TimeAdvice()); //프록시 팩토리에 Advice 등록
        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(); //JDK 동적 프록시에 의한 프록시 생성 여부
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse(); //CGLIB에 의한 프록시 생성 여부
    }

    @Test
    @DisplayName("구체 클래스만 있으면 CGLIB 사용")
    void concreteProxy() {
        ConcreteService target = new ConcreteService(); //프록시 호출 대상
        ProxyFactory proxyFactory = new ProxyFactory(target); //프록시 호출 대상 전달 및 프록시 팩토리 생성
        proxyFactory.addAdvice(new TimeAdvice()); //프록시 팩토리에 Advice 등록
        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(); //JDK 동적 프록시에 의한 프록시 생성 여부
        assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); //CGLIB에 의한 프록시 생성 여부

    }
}
  • concreteProxy 실행 로그

    com.example.proxyfactory.ProxyFactoryTest – targetClass=class com.example.common.ConcreteService
    com.example.proxyfactory.ProxyFactoryTest – proxyClass=class com.example.common.ConcreteService\(SpringCGLIB\)0
    com.example.common.TimeAdvice – TimeProxy 실행
    com.example.common.ConcreteService – ConcreteService 호출
    com.example.common.TimeAdvice – TimeProxy 종료 resultTime=0ms

테스트 생성 및 실행 (적용 기술 변경)

package com.example.proxyfactory;

import com.example.common.ConcreteService;
import com.example.common.ServiceImpl;
import com.example.common.ServiceInterface;
import com.example.common.TimeAdvice;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
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.assertThat;

@Slf4j
public class ProxyFactoryTest {
    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl(); //프록시 호출 대상
        ProxyFactory proxyFactory = new ProxyFactory(target); //프록시 호출 대상 전달 및 프록시 팩토리 생성
        proxyFactory.addAdvice(new TimeAdvice()); //프록시 팩토리에 Advice 등록
        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(); //JDK 동적 프록시에 의한 프록시 생성 여부
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse(); //CGLIB에 의한 프록시 생성 여부
    }

    @Test
    @DisplayName("구체 클래스만 있으면 CGLIB 사용")
    void concreteProxy() {
        ConcreteService target = new ConcreteService(); //프록시 호출 대상
        ProxyFactory proxyFactory = new ProxyFactory(target); //프록시 호출 대상 전달 및 프록시 팩토리 생성
        proxyFactory.addAdvice(new TimeAdvice()); //프록시 팩토리에 Advice 등록
        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(); //JDK 동적 프록시에 의한 프록시 생성 여부
        assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); //CGLIB에 의한 프록시 생성 여부

    }
    @Test
    @DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
    void proxyTargetClass() {
        ServiceInterface target = new ServiceImpl(); //프록시 호출 대상
        ProxyFactory proxyFactory = new ProxyFactory(target); //프록시 호출 대상 전달 및 프록시 팩토리 생성
        proxyFactory.setProxyTargetClass(true); //무조건 CGLIB를 사용하도록 변경
        proxyFactory.addAdvice(new TimeAdvice()); //프록시 팩토리에 Advice 등록
        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(); //JDK 동적 프록시에 의한 프록시 생성 여부
        assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); //CGLIB에 의한 프록시 생성 여부
    }
}

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

  • 포인트컷 (Pointcut)
    • 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직
    • 주로 클래스명이나 메소드명으로 필터링한다.
    • 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(Cut) 구분하는 역할을 한다.
  • 어드바이스 (Advice)
    • 프록시가 호출하는 부가 기능
    • 프록시 내부에서 동작할 로직
  • 어드바이저 (Advisor)
    • 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것
    • 어드바이저 * 1 = 포인트 컷 * 1 + 어드바이스 * 1

예제 코드1 - 어드바이저

  • 하나의 어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
  • 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할 지 알 수 있다.

테스트 생성 및 실행

  • 테스트를 실행해보면 save와 find 모두 어드바이스가 적용된 것을 확인할 수 있다.
package com.example.advisor;

import com.example.common.ServiceImpl;
import com.example.common.ServiceInterface;
import com.example.common.TimeAdvice;
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(); //비즈니스 로직 실행
    }
}
  • advisorTest1 실행 로그

    com.example.common.TimeAdvice – TimeProxy 실행
    com.example.common.ServiceImpl – save 호출
    com.example.common.TimeAdvice – TimeProxy 종료 resultTime=1ms
    com.example.common.TimeAdvice – TimeProxy 실행
    com.example.common.ServiceImpl – find 호출
    com.example.common.TimeAdvice – TimeProxy 종료 resultTime=0ms

예제 코드2 - 직접 만든 포인트컷

  • 이번에는 스프링에서 제공해주는 인터페이스를 통해 직접 포인트컷을 만들어보자.

테스트 생성 및 실행

  • MyPointcut
    • Pointcut 인터페이스를 직접 구현한 포인트컷
    • 클래스 필터는 항상 true를 반환하게 해서 클래스에 대한 필터링은 적용하지 않게 했다.
    • MyMethodMatche를 통해 메소드 비교를 진행한다.
  • MyMethodMatcher
    • MethodMatcher 인터페이스를 직접 구현한 MethodMatcher
    • matches()
      • isRuntime()의 반환 값이 false면 matches()가 호출된다.
      • 이 메소드에 method와 target 클래스에 대한 정보가 넘어온다.
      • 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단한다.
      • 이번 테스트에서는 메소드명이 “save” 인 경우에 true 를 반환하도록 판단 로직을 적용했다.
      • 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하다.
    • isRuntime()과 matches(… args)
      • isRuntime()의 반환 값이 true면 matches(… args) 메소드가 호출된다.
      • 동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다.
      • 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.
package com.example.advisor;

import com.example.common.ServiceImpl;
import com.example.common.ServiceInterface;
import com.example.common.TimeAdvice;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

import java.lang.reflect.Method;

@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(); //비즈니스 로직 실행
    }

    @Test
    void advisorTest2() {
        ServiceImpl 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";

        /**
         * - true일 경우 matches(Method method, Class<?> targetClass, Object... args) 실행
         * - false일 경우 matches(Method method, Class<?> targetClass) 실행
         */
        @Override
        public boolean isRuntime() {
            return false;
        }

        /**
         * isRuntime의 결과가 true일 때 실행
         */
        @Override
        public boolean matches(Method method, Class<?> targetClass, Object... args){
            throw new UnsupportedOperationException();
        }

        /**
         * isRuntime의 결과가 false일 때 실행
         */
        @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;
        }
    }
}
  • advisorTest2 실행 로그

    com.example.advisor.AdvisorTest – 포인트컷 호출 method=save targetClass=class com.example.common.ServiceImpl
    com.example.advisor.AdvisorTest – 포인트컷 결과 result=true
    com.example.common.TimeAdvice – TimeProxy 실행
    com.example.common.ServiceImpl – save 호출
    com.example.common.TimeAdvice – TimeProxy 종료 resultTime=1ms
    com.example.advisor.AdvisorTest – 포인트컷 호출 method=find targetClass=class com.example.common.ServiceImpl
    com.example.advisor.AdvisorTest – 포인트컷 결과 result=false
    com.example.common.ServiceImpl – find 호출

예제 코드3 - 스프링이 제공하는 포인트컷

  • 스프링은 우리가 필요한 포인트컷을 이미 대부분 제공한다.
  • 이번에는 스프링이 제공하는 NameMatchMethodPointcut를 사용해서 구현해보자.
  • 또한 스프링은 무수히 많은 포인트컷을 제공하는데 이중에서 AspectJ 표현식만 알면 된다.

테스트 생성 및 실행

package com.example.advisor;

import com.example.common.ServiceImpl;
import com.example.common.ServiceInterface;
import com.example.common.TimeAdvice;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;

import java.lang.reflect.Method;

@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(); //비즈니스 로직 실행
    }

    @Test
    void advisorTest2() {
        ServiceImpl 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(); //비즈니스 로직 실행
    }

    @Test
    void advisorTest3() {
        ServiceImpl target = new ServiceImpl(); //프록시 적용 대상
        ProxyFactory proxyFactory = new ProxyFactory(target); //프록시 호출 대상 전달 및 프록시 팩토리 생성
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); //포인트컷 생성
        pointcut.setMappedNames("save"); //포인트컷 적용 기준 설정
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, 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";

        /**
         * - true일 경우 matches(Method method, Class<?> targetClass, Object... args) 실행
         * - false일 경우 matches(Method method, Class<?> targetClass) 실행
         */
        @Override
        public boolean isRuntime() {
            return false;
        }

        /**
         * isRuntime의 결과가 true일 때 실행
         */
        @Override
        public boolean matches(Method method, Class<?> targetClass, Object... args){
            throw new UnsupportedOperationException();
        }

        /**
         * isRuntime의 결과가 false일 때 실행
         */
        @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;
        }
    }
}
  • advisorTest3 실행 로그

    com.example.common.TimeAdvice – TimeProxy 실행
    com.example.common.ServiceImpl – save 호출
    com.example.common.TimeAdvice – TimeProxy 종료 resultTime=0ms
    com.example.common.ServiceImpl – find 호출

예제 코드4 - 여러 어드바이저 함께 적용

  • 어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
  • 하나의 target에 여러 어드바이저를 적용하려면 어떻게 해야할까?
  • 우선은 프록시를 여러게 만들어보자.

테스트 생성 및 실행 (체이닝)

package com.example.advisor;

import com.example.common.ServiceImpl;
import com.example.common.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class MultiAdvisorTest {
    @Test
    @DisplayName("여러 프록시")
    void multiAdvisorTest1() {
        //client -> proxy2(advisor2) -> proxy1(advisor1) -> target

        //프록시1 생성
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory1 = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        proxyFactory1.addAdvisor(advisor1);
        ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();

        //프록시2 생성, target -> proxy1 입력
        ProxyFactory proxyFactory2 = new ProxyFactory(proxy1); //체이닝
        DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
        proxyFactory2.addAdvisor(advisor2);
        ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();

        //실행
        proxy2.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();
        }
    }
}
  • multiAdvisorTest1 실행 로그

    com.example.advisor.MultiAdvisorTest$Advice2 – advice2 호출
    com.example.advisor.MultiAdvisorTest$Advice1 – advice1 호출
    com.example.common.ServiceImpl – save 호출

테스트 생성 및 실행 (Proxy.addAdvisor 활용)

  • 어드바이저는 프록시 팩토리에 추가한 순서대로 실행된다.
package com.example.advisor;

import com.example.common.ServiceImpl;
import com.example.common.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class MultiAdvisorTest {
    @Test
    @DisplayName("여러 프록시")
    void multiAdvisorTest1() {
        //client -> proxy2(advisor2) -> proxy1(advisor1) -> target

        //프록시1 생성
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory1 = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        proxyFactory1.addAdvisor(advisor1);
        ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();

        //프록시2 생성, target -> proxy1 입력
        ProxyFactory proxyFactory2 = new ProxyFactory(proxy1); //체이닝
        DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
        proxyFactory2.addAdvisor(advisor2);
        ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();

        //실행
        proxy2.save();
    }

    @Test
    @DisplayName("하나의 프록시, 여러 어드바이저")
    void multiAdvisorTest2() {
        //proxy -> advisor2 -> advisor1 -> target
        DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
        DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());

        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();
        }
    }
}
  • multiAdvisorTest2 실행 로그

    com.example.advisor.MultiAdvisorTest$Advice2 – advice2 호출
    com.example.advisor.MultiAdvisorTest$Advice1 – advice1 호출
    com.example.common.ServiceImpl – save 호출

프록시 팩토리 - 적용1

  • 이번에는 실제로 프록시 팩토리를 애플리케이션에 적용해보자.
  • 우선은 인터페이스 기반의 v1 애플리케이션에 적용해보자.

어드바이스 생성

package com.example.advice;

import com.example.trace.LogTrace;
import com.example.trace.TraceStatus;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.reflect.Method;

@Slf4j
public class LogTraceAdvice implements MethodInterceptor {
    private final LogTrace logTrace;
    
    public LogTraceAdvice(LogTrace logTrace) {
        this.logTrace = logTrace;
    }
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TraceStatus status = null;

        try {
            Method method = invocation.getMethod();
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = logTrace.begin(message);

            Object result = invocation.proceed(); //비즈니스 로직 호출

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

스프링 빈 등록

  • 포인트컷은 NameMatchMethodPointcut을 사용한다.
    • 여기에는 심플 매칭 기능이 있어서 * 을 매칭할 수 있다.
    • XXX* : 메소드명이 XXX로 시작하는 메소드는 포인트컷이 true를 반환하도록 한다.
    • 필터링을 통해서 원하는 메소드에만 기능이 동작하게 한다.
  • 어드바이저는 포인트컷과 어드바이스를 가지고 있다.
  • 프록시 팩토리에 각각의 target과 advisor를 등록해서 프록시를 생성한다.
  • 생성된 프록시는 스프링 빈으로 등록한다.
package com.example;

import com.example.advice.LogTraceAdvice;
import com.example.app.v1.*;
import com.example.trace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {
    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
        return proxy;
    }
    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
        ProxyFactory factory = new ProxyFactory(orderService);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
        return proxy;
    }
    @Bean
    public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
        OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
        ProxyFactory factory = new ProxyFactory(orderRepository);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
        return proxy;
    }

    /**
     * 어드바이저 정의
     * @param logTrace
     */
    private Advisor getAdvisor(LogTrace logTrace) {
        //pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");

        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        
        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
package com.example;

import com.example.trace.LogTrace;
import com.example.trace.ThreadLocalLogTrace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

//@Import({AppV1Config.class, AppV2Config.class})
//@Import(InterfaceProxyConfig.class)
//@Import(ConcreteProxyConfig.class)
//@Import(DynamicProxyBasicConfig.class)
//@Import(DynamicProxyFilterConfig.class)
@Import(ProxyFactoryConfigV1.class)
@SpringBootApplication(scanBasePackages = "com.example.app.v3") //컨트롤러때문에 생기는 충돌 방지
public class Chapter2Application {
    public static void main(String[] args) {
        SpringApplication.run(Chapter2Application.class, args);
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }
}

테스트

  • http://localhost:8082/v1/request?itemId=test로 접속해보자.

    [d5551f77] OrderControllerV1.request()
    [d5551f77] |–>OrderServiceV1.orderItem()
    [d5551f77] | |–>OrderRepositoryV1.save()
    [d5551f77] | |<–OrderRepositoryV1.save() time=1002ms
    [d5551f77] |<–OrderServiceV1.orderItem() time=1002ms
    [d5551f77] OrderControllerV1.request() time=1002ms

프록시 팩토리 - 적용2

  • 이번에는 인터페이스가 없는 구체 기반의 v2 애플리케이션에 적용해보자.

스프링 빈으로 등록

  • v1을 위해 생성한 코드에서 프록시를 적용하기 위해 불러오는 클래스만 다를 뿐 동일한 코드다.
package com.example;

import com.example.advice.LogTraceAdvice;
import com.example.app.v2.*;
import com.example.trace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {
    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
        OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
        return proxy;
    }
    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
        OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderService);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
        return proxy;
    }
    @Bean
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
        OrderRepositoryV2 orderRepository = new OrderRepositoryV2();
        ProxyFactory factory = new ProxyFactory(orderRepository);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
        return proxy;
    }
    private Advisor getAdvisor(LogTrace logTrace) {
        //pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");

        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

테스트

  • http://localhost:8082/v2/request?itemId=test로 접속해보자.

    [01b0ea14] OrderControllerV2.request()
    [01b0ea14] |–>OrderServiceV2.orderItem()
    [01b0ea14] | |–>OrderRepositoryV2.save()
    [01b0ea14] | |<–OrderRepositoryV2.save() time=1012ms
    [01b0ea14] |<–OrderServiceV2.orderItem() time=1013ms
    [01b0ea14] OrderControllerV2.request() time=1013ms

정리

  • 프록시 팩토리 덕분에 개발자는 매우 편리하게 프록시를 생성할 수 있게 되었다.
  • 어드바이저, 어드바이스, 포인트컷을 통해 어떤 부가 기능을 어디에 적용할 지 명확하게 이해할 수 있었다.
  • 프록시 팩토리와 새로 배운 개념들 덕분에 프록시도 깔끔하게 적용하고, 어디에 부가 기능을 적용할지도 명확하게 정의할 수 있다.
  • 원본 코드를 전혀 손대지 않고 프록시를 통해 부가 기능도 적용하는 방법도 알 수 있었다.
  • 하지만 아직 해결되지 않는 문제가 있다.
  • 남은 문제
    • 문제1 (너무 많은 설정)
      • 설정 파일이 지나치게 많다.
      • 분명 이전에 비해서는 반복 작접이 많이 줄어든 것은 많다.
        • 클래스마다 프록시 클래스를 생성하지 않는다.
        • JDK 동적 프록시와 CGLIB를 각각 관리하지 않아도 된다.
      • 다만 아직 개별로 설정해야할 부분들이 많다.
        • 단순 계산했을 떄 프록시를 적용해야할 스프링 빈이 100개라면 프록시 설정 코드도 100번 작성해야 한다.
        • 만약 A 클래스와 B 클래스가 있는데, A 클래스에는 프록시 2개를 B 클래스에는 프록시 3개를 적용해야 한다면 골치가 아파진다.
      • 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 한다.
        • 스프링 빈을 등록하기 귀찮아서 컴포넌트 스캔을 사용하는 게 요즘 시대다.
    • 문제2 (컴포넌트 스캔)
      • 지금까지 v1과 v2만 주로 다뤄서 살짝 잊혀진 비운의 v3 애플리케이션은 컴포넌트 스캔을 사용한다.
      • 컴포넌트 스캔을 사용하는 경우에는 현재까지 학습한 내용으로 프록시를 적용할 수 있는 방법이 없다.
      • 프록시는 말 그대로 대리자다.
        • 대리자를 활용해야 하는데 컴포넌트 스캔은 실제 객체를 스프링 컨테이너에 스프링 빈으로 등록시켜 버린다.
  • 이렇게 2가지 문제점을 해결하기 위해서는 스프링 컨테이너에 실제 객체가 아닌 부가 기능이 있는 프록시를 빈으로 등록할 방법이 필요하다.
    • 발전한 기술은 이 2가지 문제점을 해결할 방법을 제공하게 되었다.
    • 다음 게시글에서는 그 해답인 빈 후처리기를 배워보자.

출처

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.