본문 바로가기
프로그래밍/Spring

[Spring] Spring AOP - 원리편

by 사바라다 2020. 7. 1.

안녕하세요. 오늘은 Spring AOP의 3번째 시간으로 마지막 시간입니다. 오늘은 AOP가 Spring 내부에서 구현되는 원리에 대해서 한번 알아보는 시간을 가져보도록 하겠습니다. AOP를 사용하는 방법 및 기본적인 이론은 아래 링크를 통해 이전 포스팅을 확인해주시기 바랍니다.

[Spring] Spring AOP - 기본 이론편

[Spring] Spring AOP - 실전편

정적 프록시와 동적 프록시

Spring의 AOP는 프록시 패턴을 사용합니다. proxy pattern에 대해서는 예전에 [Spring & Design Pattern] Spring에서 발견한 디자인패턴_Proxy Pattern 으로 포스팅 한적이 있으니 참고하시면 자세히 알 수 있습니다.

만약 Class에 5개의 메서드가 있습니다. 이 Class의 모든 메서드에 AOP를 적용하기 위해서는 Proxy 패턴을 각 메서드마다 만들어 주어야합니다. 10개면 어떨까요? 그러면 10개를 똑같이 복제본을 떠서 만들어야합니다. class 별로 역할은 명확하겠지만 노가다가 있을 수 밖에 없습니다.

따라서 우리는 정적으로 프록시를 만들어서 AOP를 구현하는 것이 아니라 프록시를 조건에 따라 자동으로 만들어주는 다이나픽 프록시를 이용합니다.

Spring AOP 동적 프록시 종류

Spring AOP는 동적 프록시를 적용하기 위해서 JDK dynamic proxyCGLIB를 이용합니다.

JDK dynamic proxy는 JDK에 내장되어 있고 CGLIB는 오픈소스입니다. 만약 target Object가 적어도 하나 이상의 Interface가 있다면 JDK dynamic proxy가 사용되며 interface를 구현한 메서드들이 proxy를 탑니다. 만약 target object가 interface를 구현되어있지 않다면 CGLIB proxy시를 이용하여 Proxy를 생성니다.

JDK dynamic proxy로 구현되면 interface가 존재하는 메서드만 proxy가 생성되며 CGLIB를 이용할경우 해당 target에 해당하는 모든 메서드에 proxy가 생성됩니다.

  • CGLIB를 이용할 경우 final 메서더는 proxy 적용이 되지 않음에 유의해주시기 바랍니다.

CGLIB를 강제로 사용하고자 할 경우 xml 또는 Java 코드로 아래와 같이 설정할 수 있습니다.

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class TraceLoggerConfig {

}

JDK dynamic proxy

JDK dynamic proxy를 한번 실제로 구현해보도록 하겠습니다. 예제는 이전 시간들과 같이 시간체킹하는 예제입니다. 시간 체크와 비즈니스로직이 함께 있는 소스코드는 아래와 같습니다. 아래 소스를 JDK dynamic proxy를 이용하여 처리해보도록 하겠습니다.

public void timeCheck() {
    Stopwatch stopwatch = Stopwatch.createStarted();
    doSomething();
    stopwatch.stop();
    log.info("time : " + stopwatch.elapsed(MILLISECONDS));
}

JDK dynamic proxy는 내부적으로 Reflection을 이용합니다. Reflection은 객체를 통해 Class의 정보를 분석해내는 Java 프로그램 기법을 말합니다. Reflection을 이용하면 해당 객체의 생성자, 메서드, 맴버변수의 정보를 알 수 있으며 private 정보또한 얻을 수 있습니다. reflection을 이용하면 아래와 같은 구성이 가능합니다.

dynamic Proxy flow 

아래 코드는 Proxy를 적용할 Target Class와 Interface 입니다. JDK dynamic proxy에서 Interface를 기준으로 proxy 코드를 생성하기 때문에 부가기능을 추가할 메서드는 반드시 interface가 필요합니다.

@Component
public class Target implements TargetInterface{

    public void doSomething() {
        System.out.println("=== 작업 중 ===");
    }
}
public interface TargetInterface {
    void doSomething();
}

아래는 AOP의 Advice를 담당하는 코드입니다. InvocationHandler는 Proxy에 필요한 요소들을 가지고 있는 interface로 이것을 구현하면 우리는 Proxy를 구현할 수 있습니다.

*
 * 프록시로써 필요한 기능을 제공받기 위해 InvocationHandler를 상속받는다.
 */
public class Proxy implements InvocationHandler {

    Target target;

    /*
     * 다이나믹 프록시로부터 전달받은 요청을 다시 타깃 오브젝트에 대입해야 하기 때문에 타깃 오브젝트를 주입받는다.
     */
    public Proxy(Target target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Stopwatch stopwatch = Stopwatch.createStarted();
        Object invoke = method.invoke(target, args); // 타깃으로의 위임 인터페이스의 메서드 호출에 모두 적용됨
        stopwatch.stop();
        System.out.println("time : " + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "ms");
        return invoke;
    }
}

구현을 위한 dynamic Proxy 코드는 모두 마무리 되었습니다. 이제 Test 코드를 작성하여 프록시가 정상적으로 적용되었는지 확인해보도록 하겠습니다. Client 코드에서 reflection 코드를 사용합니다.

@Test
public void proxyTest() {
    TargetInterface instance = (TargetInterface) Proxy.newProxyInstance(
            getClass().getClassLoader(),        // 클래스 로딩에 사용할 클래스 로더
            new Class[]{TargetInterface.class}, // 구현할 인터페이스
            new personal.ykh.sample.proxy.Proxy(new Target()) // 부가기능을 위임할 Proxy
    );

    instance.doSomething();
}

결과

=== 작업 중 ===
time : 3ms

결과적으로 부가기능이 잘 적용된것을 확인할 수 있습니다.

Factory Bean

위에서 reflection을 이용하여 Dynamic Proxy를 만들었습니다. 이걸로 끝이 아닙니다. 이제 그럼 Spring에서 이렇게 만든 Dynamic Proxy를 어떻게 Target에 적용할 수 있는지 알아보도록 하겠습니다. AOP를 사용할 때 aspect를 작성하며 따로 위의 테스트코드 같이 Proxy를 명시적 코드로 작성하지 않습니다.

Spring은 기본적으로 reflection API를 이용해서 bean 정의에 나오는 클래스 이름을 이용하여 bean 오브젝트를 생성합니다. 하지만 proxy 오브젝트는 내부적으로 다이나믹하게 새로 정의되어 사용되기 때문에 이렇게 bean으로 등록하지 못합니다. 따라서 Factory Bean을 이용하여 Bean 등록을 해야합니다. Factory Bean이란 Spring을 대신하여 오브젝트의 생성로직을 담당하도록 만들어진 특별한 Bean입니다.

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
    @Nullable T getObject() throws Exception;
    @Nullable Class<?> getObjectType();
    default boolean isSingleton() {
       return true;
    }
}

factory Bean의 getObject() 메서드를 통해서 Bean을 가져올 수 있습니다. 이 getObject() 메서드에 우리가 테스트에 이용했던 Proxy 생성 Reflection 코드를 넣습니다. 그렇게 하면 아래와 같이 Target Bean이 만들어지고 사용자가 Target Bean을 사용할 때의 flow가 됩니다.

public class ProxyBean implements FactoryBean<TargetInterface> {

    @Override
    public TargetInterface getObject() throws Exception {
        return (TargetInterface) Proxy.newProxyInstance(
                getClass().getClassLoader(),        // 클래스 로딩에 사용할 클래스 로더
                new Class[]{TargetInterface.class}, // 구현할 인터페이스
                new personal.ykh.sample.proxy.Proxy(new Target()) // 부가기능을 위임할 Proxy
        );
    }

    ... 이하 생략 ...
}

factory bean의 proxy 생성 및 client -> target 으로의 flow

마무리

오늘은 이렇게 Spring AOP의 기술에 대해서 조금 깊게 들어가보는 시간을 가졌습니다. 오늘까지 3번의 AOP 포스팅을 통해서 Spring이 쉽게 AOP를 구현하기 위해서 얼마나 고민하고 우리가 쉽게 사용할 수 있도록 추상화 하였는지 알 수 있는 시간이었습니다.

오늘까지 총 3편의 AOP 포스팅을 통해 AOP에 대해서 충분히 다져지셨으면 좋겠습니다.

감사합니다.

참조

토비의 스프링 3.1 Vol. 1, 6장. AOP

spring 5.2.7.RELEASE docs core AOP

baeldung - java dynamic proxy

댓글