스프링 핵심 원리 - 고급편 - (20) 스프링 프록시 - 프록시 팩토리
인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.
포인트컷, 어드바이스, 어드바이저 - 소개
- 포인트컷(
Pointcut) : 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필링 로직이다. 주로 클래스와 메서드 이름으로 필터링한다. 이름 그대로 어떤 포인트(point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 것이다. - 어드바이스(
Advice) : 이전에 본 것처럼 프록시가 호출하는 부가 기능이다. 단순히 프록시 로직이라 생각하면 된다.
정리하면 부가 기능 로직을 적용해야 하는데, 포인트컷으로 어디에? 적용할지 선택하고, 어드바이스로 어떤 로직을 적용할지 선택하는 것이다. 그리고 어디에? 어떤 로직?을 모두 알고 있는 것이 어드바이저이다.
쉽게 기억하기
- 조언(
Advice)을 어디(Pointcut)에 할 것인가? - 조언자(
Advisor)는 어디(Pointcut)에 조언(Advice)을 해야 할지 알고 있다.
역할과 책임
이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것이다.
- 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다.
- 어드바이스는 깔끔하게 부가 기능 로직만 담당한다.
- 둘을 합치면 어드바이저가 된다. 스프링의 어드바이저는 하나의 포인트컷 + 하나의 어드바이스로 구성된다.

직전에 만들었던 로그 추적기인데 이걸 어드바이스라고 생각하면 그 안에 필터까지 들어간 코드가 되버린다.
즉 단일 책임 원칙을 어긴 코드가 된다. 이런 코드를 분리해야 한다.
참고
해당 단어들에 대한 정의는 지금은 문맥 상 이해를 돕기 위해 프록시에 맞춰서 설명하지만, 이후에 AOP 부분에서 다시 한번 AOP에 맞추어 정리할 것. 그림은 이해를 돕기 위한 것이고, 실제 구현은 다를 수 있음.

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

해당 위치에 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
}
}

save(), find() 다 잘 적용됬다.
new DefaultPointcutAdvisor:Advisor인터페이스의 가장 일반적인 구현체이다. 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어주면 된다. 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성된다.Pointcut.TRUE: 항상true를 반환하는 포인트컷이다. 이후에 직접 포인트컷을 구현해 볼 것이다.new TimeAdvice(): 앞서 개발한TimeAdvice어드바이스를 제공한다.proxyFactory.addAdvisor(advisor): 프록시 팩토리에 적용할 어드바이저를 지정한다. 어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있다. 따라서 어디에 어떤 부가기능을 적용해야 할지 어드바이저 하나로 알 수 있다. 프록시 팩토리를 사용할 때 어드바이저는 필수이다.proxyFactory.addAdvice(new TimeAdvice())포인트컷을 넣지 않아도 내부에서DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice())다음과 같이 동작한다.

예제 코드 2 - 직접 만든 포인트컷
이번에는 save()메서드에는 어드바이스 로직을 적용하지만, find() 메서드에는 어드바이스 로직을 적용하지 않도록 해보자.
Pointcut 관련 인터페이스 - 스프링 제공
1
2
3
4
5
6
7
8
9
10
11
12
13
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 둘로 이루어진다.
이름 그대로 하나는 클래스가 맞는지, 하나는 메서드가 맞는지 확인할 때 사용한다.
둘 다 true로 반환해야 어드바이스를 적용할 수 있다.
일반적으로 스프링이 이미 만들어둔 구현체를 사용하지만 개념 학습 차원에서 간단히 직접 구현해보자.
AdvisorTest - advisorTest2() 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Test
@DisplayName("직접 만든 포인트컷")
void advisorTest2() {
ServiceInterface 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";
@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;
}
}
MyPointcut
- 직접 구현한 포인트컷.
Pointcut인터페이스를 구현 - 현재 메서드 기준으로 로직을 적용하면 된다. 클래스 필터는 항상
true를 반환하도록 했고, 메서드 비교 기능은MyMethodMatcher를 사용한다.
MyMethodMatcher
- 직접 구현한
MethodMatcher이다.MethodMatcher인터페이스를 구현한다. matches(): 이 메서드에method,targetClass정보가 넘어온다. 이 정보로 어드바이스를 적용할 지 아닐 지 판단할 수 있다.- 여기서는 메서드 이름이
"save"인 경우에true를 반환하도록 판단 로직을 적용했다. isRuntime(),matches(... args):isRuntime()이 값이 참이면matches(... args)메서드가 대신 호출된다. 동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다.isRuntime()이false인 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만,isRuntime()이true인 경우 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱하지 않는다.- 크게 중요한 부분은 아니다.
new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice())
- 어드바이저에 직접 구현한 포인트컷을 사용한다.

실행 결과를 보면 기대한 것과 같이 save()를 호출할 때는 어드바이스가 적용되지만, find()를 호출할 때는 어드바이스가 적용되지 않는다.

- 클라이언트가 프록시의
save()를 호출한다. - 포인트컷에
Service클래스의save()메서드에 어드바이스를 적용해도 될지 물어본다. - 포인트컷이
true를 반환한다. 따라서 어드바이스를 호출해서 부가 기능을 적용한다. - 이후 실제 인스턴스의
save()를 호출한다.

- 클라이언트가 프록시의
find()를 호출한다. - 포인트컷에
Service클래스의find()메서드에 어드바이스를 적용해도 될지 물어본다. - 포인트컷이
false를 반환한다. 따라서 어드바이스를 호출하지 않고, 부가 기능도 적용되지 않는다. - 실제 인스턴스의
find()를 호출한다.
예제 코드 3 - 스프링이 제공하는 포인트컷
스프링은 우리가 필요한 포인트컷을 이미 대부분 제공한다.
이번에는 스프링이 제공하는 NameMatchMethodPointcut를 사용해서 구현해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
NameMatchMethodPointcut 사용 코드
1
2
3
4
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
NameMatchMethodPointcut을 생성하고 setMappedNames(...)으로 메서드 이름을 지정하면 포인트컷이 완성된다.

실행 결과를 보면 save()를 호출할 때는 어드바이스가 적용되지만, find()를 호출할 때는 어드바이스가 적용되지 않는다.
스프링이 제공하는 포인트컷
스프링은 무수히 많은 포인트컷을 제공한다. 대표적인 몇 가지만 알아보자.
NameMatchMethodPointcut: 메서드 이름을 기반으로 매칭한다. 내부에서는PatternMatchUtils를 사용한다.- 예)
*xxx*허용
- 예)
JdkRegexpMethodPointcut: JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.TruePointcut: 항상true를 반환한다.AnnotationMatchingPointcut: 애노테이션으로 매칭한다.AspectJExpressionPointcut: aspectJ 표현식으로 매칭한다.
가장 중요한 것은 aspectJ 표현식
여기에서 사실 다른 것은 중요하지 않다.
실무에서는 사용하기도 편리하고 기능도 가장 많은 aspectJ표현식을 기반으로 사용하는 AspectJExpressionPointcut을 사용하게 된다.
aspectJ 표현식과 사용 방법은 중요해서 이후 AOP에서 설명.
지금은 Pointcut의 동작 방식과 전체 구조에 집중
예제 코드 4 - 여러 어드바이저 함께 적용
어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
만약 여러 어드바이저를 하나의 target에 적용하려면 어떻게 해야 할까?
프록시를 여러 개 만들면 될 것 같다.
MultiAdvisorTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package hello.proxy.advisor;
import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.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;
import org.springframework.aop.support.NameMatchMethodPointcut;
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();
}
}
}

그림으로 보면 다음과 같다. 프록시 2에 프록시 1을 넘겼고, 프록시1엔 target을 넘겨서 거꾸로 실행된다.

다음과 같이 2 -> 1 -> target 순으로 실행된다.
포인트컷은 advisor1, advisor2 모두 항상 true를 반환하도록 설정했다. 따라서 둘 다 어드바이스가 적용된다.
여러 프록시의 문제
이 방법이 잘못된 것은 아니지만, 프록시를 2번 생성해야 한다는 문제가 있다. 만약 적용해야 하는 어드바이저가 10개 라면 10개의 프록시를 생성해야 한다.
스프링은 이 문제를 해결하기 위해 하나의 프록시에 여러 어드바이저를 적용할 수 있게 만들어두었다.

그럼 이 기능을 코드로 작성해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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());
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// 등
proxyFactory.addAdvisor(advisor2);
proxyFactory.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
// 실행
proxy.save();
}
- 프록시 팩토리에 원하는 만큼
addAdvisor()를 통해서 어드바이저를 등록하면 된다. - 등록하는 순서대로
advisor가 호출된다. 여기서는advisor2,advisor1순서로 등록했다.

실행 결과를 보면 advice2, advice1 순서대로 호출된 것을 알 수 있다.

결과적으로 여러 프록시를 사용할 때와 비교해서 결과는 같고, 성능은 더 좋다.
중요
스프링 AOP 적용 수만큼 프록시가 생성된다고 착각하는 경우가 많다.
스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
정리하면 하나의target에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는target마다 하나의 프록시만 생성한다. 이 부분을 꼭 기억하자.
댓글남기기