프록시 팩토리 - 소개
- 우선 동적 프록시의 문제점을 다시 살펴보자.
- 상황에 따라 다른 기술을 적용해야 한다.
- 인터페이스가 있는 경우
- JDK 동적 프록시
- 인터페이스가 없는 경우
- CGLIB
- 인터페이스가 있는 경우
- JDK 동적 프록시와 CGLIB를 함께 사용해야 한다면 각 기술마다 프록시를 관리해야 한다.
- JDK 동적 프록시
- InvocationHandler
- CGLIB
- MethodInterceptor
- JDK 동적 프록시
- 상황에 따라 다른 기술을 적용해야 한다.
- 이 떄 사용하는 것이 스프링에서 제공하는 프록시 팩토리다.
- 프록시 팩토리는 상황에 따라 알아서 알맞는 동적 프록시 기술을 적용해준다.
- 기본적으로 인터페이스가 있으면 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 애플리케이션은 컴포넌트 스캔을 사용한다.
- 컴포넌트 스캔을 사용하는 경우에는 현재까지 학습한 내용으로 프록시를 적용할 수 있는 방법이 없다.
- 프록시는 말 그대로 대리자다.
- 대리자를 활용해야 하는데 컴포넌트 스캔은 실제 객체를 스프링 컨테이너에 스프링 빈으로 등록시켜 버린다.
- 문제1 (너무 많은 설정)
- 이렇게 2가지 문제점을 해결하기 위해서는 스프링 컨테이너에 실제 객체가 아닌 부가 기능이 있는 프록시를 빈으로 등록할 방법이 필요하다.
- 발전한 기술은 이 2가지 문제점을 해결할 방법을 제공하게 되었다.
- 다음 게시글에서는 그 해답인
빈 후처리기
를 배워보자.