Spring AOP (3)

앞서 AOP에대한 간단한 예시와 개념을 살펴보았다.

이번 포스트에서는 Spring에서 제공하는 AOP 기능과 작동방식에 대해서 알아보도록 하겠다.

Spring AOP

AOP란 횡단 관심사 (Cross Cutting Concern) 을 한데 모아 로직을 설계한다음
런타임(Runtime) 시, 클래스 로드 (Load Time) 시, 컴파일(Compile Time) 시, 횡단 로직을 핵심 로직에 적용하여 작동하게 해준다.

이 중에서 Spring AOP는 Proxy를 이용하여 런타임 위빙(Runtime Weaving)을 이용하여 횡단 로직을 수행 할 수 있도록 한다. Spring AOP에서 사용되는 Proxy는 2가지가 있다.
한가지는 JDK 1.3 부터 적용되기 시작한 JDK Dynamic Proxy가 있다. 이는 Java 라이브러리에서 제공하는 Proxy이다.
또 다른 한가지는 CGLIB Proxy가 있다. CGLIB를 사용하기 위해서는 개발하는 프로젝트에 CGLIB 라이브러리를 추가해야 사용이 가능하다.

Proxy란?

Proxy구성도

Proxy란 마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 하는 객체의 의미한다.

그리고 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타깃(Target) 또는 실체(Real Object) 라고 부른다.

토비의 스프링 vol.1 (p. 430)

토비의 스프링 책에서는 Proxy에 대한 정의를 위와 같이 하고 있다. 실제 Target이 담당하는 역할을 요청을 대리 받아서 요청 이전, 이후에 대한 추가적인 로직을 추가 할 수 있는 객체이다. 이렇게 하면 실제 Target이 담당하는 역할에 대해서는 관여 하지 않으면서 추가적인 역할을 추가 할 수 있기 때문이다.

Spring에서는 Proxy를 이용해 객체지향의 5대원칙 중 하나인 OCP(Open-Close Principal : 개방폐쇄의 원칙)을 적용하고 있다.

OCP (Open-Close Principal : 개방 폐쇄의 원칙)

개방-폐쇄 원칙(OCP, Open-Closed Principle)은 ‘소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.’는 프로그래밍 원칙이다.

실제 Target에 대한 수정을 하지 않으면서, Proxy를 통해 추가적인 코드를 작성 하여 기능을 확장 시킬 수 있음을 의미한다.

Proxy vs Proxy Pattern

일반적으로 사용하는 하는 Proxy라는 용어와 디자인패턴에서 말하는 프록시 패턴(Proxy Pattern)은 구분할 필요가 있다.
비슷한 개념이지만, 내용이 조금 다르다.

일반적으로 부르는 Proxy는 실제 Target의 기능을 수행하면서 기능을 확장하거나 추가 하는 실제 객체(Object)를 의미한다.

Proxy Pattern은 실제로 Target에 대한 기능을 확장하거나, 추가하지 않는다. 그저 클라이언트가 타깃에 접근하는 방식을 변경해 주는 역할을 한다.

JDK Dynamic Proxy

JDK Dynamic Proxy

JDK Dynamic Proxy는 JDK 1.3+ 부터 제공되는 Proxy Factory에 의해 런타임 시 동적으로 만들어 지는 오브젝트이다.
JDK Dynamic Proxy는 반드시 Interface가 정의 되어있고, Interface에 대한 명세를 기준으로 Proxy를 생성한다.
따라서 Interface 선언에 대한 강제성이 있다는 단점이 있다.
이전에는 직접 수동으로 Proxy 객체를 만들어서 사용하는 경우도 있었으나, 요즘은 자동 프록시 생성기라는 모듈을 이용하여 Dynamic Proxy를 생성한다.

내부적으로 Dynamic Proxy에서는 InvocationHandler라는 Interface를 구현하여 만들어지는데 InvocationHandler의 invoke라는 함수를 Override하여 Proxy의 위임 기능을 수행한다. 이 과정에서 Object에 대해 Reflection기능을 사용하여 기능을 구현하기 때문에 퍼포먼스의 하락의 원인이 되기도 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ExamDynamicHandler implements InvocationHandler {
private ExamInterface target; // 타깃 객체에 대한 클래스를 직접 참조하는것이 아닌 Interface를 이용

public ExamDynamicHandler(ExamInterface target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
// 메소드에 대한 명세, 파라미터등을 가져오는 과정에서 Reflection 사용
String ret = (String)method.invoke(target, args); //타입 Safe하지 않는 단점이 있다.
return ret.toUpperCase(); //메소드 기능에 대한 확장
}
}

CGLIB Proxy

CGLIB Proxy는 순수 Java JDK 라이브러리를 이용하는 것이 아닌 CGLIB라는 외부 라이브러리를 추가해야만 사용할 수 있다.
실제 CGLIB의 Enhancer라는 클래스를 바탕으로 Proxy를 생성하며, JDK Dynamic Proxy의 단점인 Interface가 없어도 Proxy를 생성 할 수 있다. CGLIB Proxy는 Target Class를 상속받아 생성된다. 그렇기 때문에 개발자는 Proxy를 생성 하기 위해 굳이 Interface를 만들어야 하는 수고를 덜 수 있다.

하지만, 상속을 이용하는 만큼 final이나 private과 같이 상속에 대해 Override를 지원하지 않는 경우 Proxy에서 해당 메소드에 대한 Aspect를 적용할 수 없다는 단점이 있다.

CGLIB Proxy의 경우 실제 바이트 코드를 조작하여 JDK Dynamic Proxy보다는 퍼포먼스가 상대적으로 빠른 장점이 있다.

CGLIB Proxy

  • CGLIB Proxy 설정법

  • ~~~yaml
    spring.aop.proxy-target-class=false

    1
    2
    3
    4
    5

    ~~~xml
    <aop:config proxy-target-class="true">
    </aop:config>
    <aop:aspectj-autoproxy proxy-target-class="true"/> <!-- AspectJ를 사용하는 경우 -->
    1
    2
    3
    //AOP를 사용하는 경우 Interface가 있더라도 cglib사용(target class기반)을 강제한다
    @EnableAsync(proxyTargetClass = true)
    @EnableCaching(proxyTargetClass = true)

Spring Bean에 대한 Proxy는?

Spring에서 Bean으로 등록된 객체는 싱글톤으로 등록되어 IoC 컨테이너에서 관리 된다.
하지만 Bean 객체에 대해 Spring AOP를 적용하는 경우 Proxy가 생성 될 텐데.. 이 경우 이 Bean을 DI하여 사용하는 곳에서는 어떻게 Proxy를 주입하여 사용할까?

00.png

Spring에서는 자동 프록시 생성기 라는 것이 있다.

빈 후처리기들 중에서 자동으로 프록시를 생성하기 위해  DefaultAdvisorAutoProxyCreator 라는 클래스를 사용한다.
이 클래스는 어드바이저를 이용한 자동 프록시 생성기 이다. 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록시킬 수 있다.

DefaultAdvisorAutoProxyCreator 빈 후처리가 등록되어 있다면, 스프링은 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보낸다.

  1. 후처리기는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인한다.
  2. 프록시 적용 대상이면 내장된 프록시 생성기를 통해 현재 빈에 대한 프록시를 생성하고 어드바이저를 연결한다.
  3. 프록시가 생성되면 전달받은 Target Bean 오브젝트 대신에 Proxy 오브젝트를 스프링 컨테이너에게 돌려준다.
  4. 컨테이너는 빈 후처리가 돌려준 Proxy 오브젝트를 빈으로 등록한다.

이 후처리기를 통해 일일이 ProxyFactoryBean을 빈으로 등록하지 않아도 여러 타깃 오브젝트에 자동으로 프록시를 적용시킬 수 있다.

요약..

  • Spring AOP는 무조건 Proxy 기반의 RTW(Run Time Weaving)을 적용한 AOP방식이다.
  • Spring AOP에서 사용하는 Proxy는 JDK Dynamic Proxy와 CGLIB Proxy를 사용한다.
    • JDK Dynamic Proxy는 Interface 기반으로 생성 된다. (반드시 Interface가 있어야지만 생성가능하다)
    • CGLIB Proxy는 Class에 대한 Proxy생성을 지원한다 (상속을 이용)
      따라서 final이나 private Method에 대한 AOP 불가 (상속 된 Proxy 객체 생성 시, Override가 불가하기 때문)
  • Spring AOP에서는 Method에 대해서만 JoinPoint를 지원한다.
    (그 이상의 Join Point를 이용하기 위해서는 AspectJ와 같은 모듈을 이용해야 한다.)

참고

토비의 스프링 vol.1

http://blog.naver.com/PostView.nhn?blogId=tmondev&logNo=220556587811

https://ko.wikipedia.org/wiki/%EA%B0%9C%EB%B0%A9-%ED%8F%90%EC%87%84_%EC%9B%90%EC%B9%99