Home Spring Framework - 스프링이 지원하는 프록시
Post
Cancel

Spring Framework - 스프링이 지원하는 프록시

프록시 팩토리

스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory)라는 기능을 제공한다. 과거에는 상황에 따라서 JDK 동적 프록시를 사용하거나 CGIL을 사용해야 했다면, 이제는 프록시 팩토리 하나로 편리하게 동적 프록시를 생성 할 수 있다.

인터페이스, 구체 클래스별 사용 기술

프록시 팩토리는 인터페이스가 있으면 JDK 동적프록시를 사용하고, 구체 클라스만 있다면 CGLIB을 사용한다. 그리고 이 설정또한 변경할 수 있다.

Alt text

Alt text

Handler 구현 방식

JDK 동적 프록시에는 InvocationHandler 인터페이스를, CGLIB은 MethodInterceptor 인터페이스를 구현하여 프록시의 handler를 구현 하였는데, 프록시 팩토리에서는 Advice라는 개념을 도입하여 이 두개의 인터페이스를 별도로 구현하지 않도록 하였다.

즉, 프록시 팩토리는 내부적으로 Advice를 호출하는 전용 InvocationHandler나 MethodInterceptor를 사용하고 있기 때문에 개발자는 InvocationHandler나 MethodInterceptor를 신경쓰지 않고 Advice만 구현하면 된다.

Alt text

Alt text

프록시 팩토리 사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface AInterface() {
  void call();
}

public class AImpl() implements AInterface {
  void call() {
    log.info("Interface");
  }
}

public class Concrete {
  void Call() {
    log.info("Concrete");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
  }
}
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
void interfaceProxy() {
  AInterface target = new AImpl();
  ProxyFactory proxyFactory = new ProxyFactory(target);
  proxyFactory.addAdvice(new TimeAdvice());

  AInterface proxy = (AInterface)proxyFactory.getProxy();

  proxy.call();

  Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
  Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
  Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}

@Test
void concreteProxy() {
  Concrete target = new Concrete();
  ProxyFactory proxyFactory = new ProxyFactory(target);
  proxyFactory.addAdvice(new TimeAdvice());

  Concrete proxy = (Concrete)proxyFactory.getProxy();

  proxy.call();

  Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
  Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
  Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}

@Test
void proxyTargetClass() {
  AInterface target = new AImpl();
  ProxyFactory proxyFactory = new ProxyFactory(target);

  // interface 가 존재하지만
  // JDK 동적 프록시가 아닌 CGLIB을 사용하도록 옵션 설정
  proxyFactory.setProxyTargetClass(true);
  proxyFactory.addAdvice(new TimeAdvice());

  AInterface proxy = (AInterface)proxyFactory.getProxy();

  proxy.call();

  Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
  Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
  Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}

스프링 부트는 AOP를 적용할떄 기본적으로 proxyTargetClass=true로 설정해서 사용한다. 그러므로 인터페이스가 있어도 항상 CGLIB을 사용하여 구체 클래스를 기반으로 프록시를 생성한다.

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

  • 포인트컷(Pointcut)
    • 어디에 부가 기능을 적용할지, 적용하지 않을지 판단하는 필터링 로직
    • 주로 클래스와 메소드 이름으로 필터링함
  • 어드바이스(Advice)
    • 프록시가 홏출하는 부가 기능(프록시 로직)
  • 어드바이저(Advisor)
    • 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 인스턴스 (포인트컷 1 + 어드바이스 1)

위와 같이 구분한 것은 역할과 책임을 명확하게 분리한 것이다.

  • 포인트컷은 대상 여부를 확인하는 필터 역할만 담당
  • 어드바이스는 부가 기능 로직만을 담당
  • 이 둘을 합쳐 어드바이저를 구성함

Alt text

어드바이저 사용 코드 예시

1
2
3
4
5
6
7
8
9
10
11
@Test
void advisorTest() {
  AInterface target = new AImpl();
  ProxyFactory proxyFactory = new ProxyFactory(target);
  DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
  proxyFactory.addAdvisor(advisor);

  AInterface proxy = (AInterface)proxyFactory.getProxy();

  proxy.call();
}
  • Pointcut.TRUE
    • 항상 true를 반환하는 포인트컷
  • proxyFactory.addAdvisor(advisor)
    • 프록시 팩토리에 적용할 어드자이저 지정
    • 어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있기 때문에 어디에 어떤 부가기능을 적용해야 할지 어드바이저 하나로 알 수 있음
    • 프록시 팩토리를 사용할 떄 어드바이저는 필수

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
void adAdvisorTest3() {
  AInterface target = new AImpl();
  ProxyFactory proxyFactory = new ProxyFactory(target);

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

  DefaultPointcutAdvisor advisor =
    new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
  proxyFactory.addAdvisor(advisor);

  AInterface proxy = (AInterface)proxyFactory.getProxy();

  proxy.call();
}

위 예시 코드에서의 NameMatchMethodPointcut 뿐만 아니라 다른 여러 포인트컷들도 지원한다.

  • NameMatchMethodPointcut
    • 메서드 이름을 기반으로 매칭
    • 내부에서는 PatternMatchUtils를 사용
  • JdkRegexpMethodPointcut
    • JDK 정규 표현식을 기반으로 포인트컷을 매칭
  • TruePointcut
    • 항상 참을 반환
  • AnnotationMatchingPointcut
    • 애노테이션으로 매칭
  • AspectJExpressionPointcut
    • aspectJ 표현식으로 매칭
    • 실무에서 사용하기 편하고 기능도 가장 많음
    • 주로 많이 사용됨

여러 어드바이저 함께 적용

스프링은 하나의 어드바이저당 프록시를 생성하는것이 아닌, 하나의 프록시에 여러 어드바이저를 적용할 수 있게 구현되어 있다.

Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
void advisorTest() {
  DefaultPointcutAdvisor advisor1 =
    new DefaultPointcutAdvisor(Pointcut.TRUE, new MyAdvisor1());
  DefaultPointcutAdvisor advisor2 =
    new DefaultPointcutAdvisor(Pointcut.TRUE, new MyAdvisor2());

  AInterface target = new AImpl();
  ProxyFactory proxyFactory = new ProxyFactory(target);

  proxyFactory.addAdvisor(advisor2);
  proxyFactory.addAdvisor(advisor1);

  AInterface proxy = (AInterface)proxyFactory.getProxy();

  proxy.call();
}
  • 등록된 어드바이저 순으로 호출됨
    • advisor2, advisor1 순으로 등록되었으므로 advisor2 -> advisor1 -> target 순으로 호출됨


Alt text

스프링 AOP를 처음 공부하다보면 AOP 적용 수 만큼 프록시가 생성된다고 생각할 수 있다. 그러나 스프링은 AOP를 적용할 때 최적화를 진행하여 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.

즉, 하나의 target 에 여러 AOP가 동시에 적용되더라도, 스프링의 AOP는 target 마다 하나의 프록시만 생성한다.


참고

  • 스프링 핵심 원리 - 고급편(김영한)
This post is licensed under CC BY 4.0 by the author.