Spring AOP(2)

김하영·2020년 12월 30일
0

AOP 의 동작 원리 (구성, 매커니즘)

Spring AOP 특징

1. 프록시 패턴 기반의 AOP 구현체이다.

  • Spring은 타겟(target) 객체에 대한 프록시를 만들어 제공한다.

  • 타겟을 감싸는 프록시는 실행시간(Runtime)에 생성된다.

  • 프록시는 어드바이스를 타겟 객체에 적용하면서 생성되는 객체이다.

  • 프록시가 호출을 가로챈다(Intercept)

프록시 패턴

일반적으로 프록시는 타겟을 감싸서 타겟의 요청을 대신 받아주는 랩핑(Wrapping) 오브젝트이다.

호출자 (클라이언트)에서 타겟을 호출하게 되면 타겟이 아닌 타겟을 감싸고 있는 프록시가 호출되어,
타겟 메소드 실행전에 선처리, 타겟 메소드 실행 후, 후처리를 실행시키도록 구성되어있다.

실제 작업을 행하는 오브젝트를 감싸서, 실제 오브젝트를 요청하기 전이나 후에 인가 처리(보호)나,
생성 자원이 많이 드는 작업 백그라운드 처리 (가상), 원격 메소드를 호출하기 위한 작업(원격 프록시) 등을 하는데 사용한다.

예) VPN

같은 인터페이스를 구현하고 있는 실제 요청 처리 객체와 프록시 객체를 만들고,
프록시 객체가 실제 요청 처리객체를 갖고 있는 구조이다.

프록시 패턴을 사용하면 어떤 기능을 추가하려 할때 기존 코드를 변경하지 않고 기능을 추가할수 있다.

Spring AOP는 프록시 패턴이라는 디자인 패턴을 사용해서 AOP 효과를 낸다.

어떤 클래스가 Spring AOP의 대상이라면 그 기존 클래스의 빈이 만들어질때,
Spring AOP가 프록시(기능이 추가된 클래스)를 자동으로 만들고 원본 클래스 대신 프록시를 빈으로 등록한다.

그리고 원본 클래스가 사용되는 지점에서 프록시를 대신 사용한다.

즉, Spring AOP 에서는 Dynamic Proxy 기법을 이용해서, Proxy 클래스를 덧씌워 AOP 를 구현한다.

2. Spring Bean에만 AOP 적용가능

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

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

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

3. Spring AOP는 메서드 조인 포인트만 지원한다.

  • Spring은 동적 프록시를 기반으로 AOP를 구현하므로 메서드 조인 포인트만 지원한다.

  • 핵심기능(타겟)의 메서드가 호출되는 런타임 시점에만 부가기능(어드바이스)을 적용할 수 있다.

  • 반면에 AspectJ 같은 고급 AOP 프레임워크를 사용하면 객체의 생성, 필드값의 조회와 조작, static 메서드 호출 및 초기화 등의 다양한 작업에 부가기능을 적용 할 수 있다.

4. 모든 AOP 기능을 제공하는 것이 아닌 스프링 컨테이너와 연동하여 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체들 간 관계 복잡도 증가)에 대한 해결책을 지원하는 것이 목적이다.

5. 중복되는 코드 제거, 효율적인 유지보수, 높은 생산성, 재활용성 극대화, 변화 수용이 용이 등의 장점이 있다.

Spring AOP 동작 원리

  • Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식이다
  • Spring AOP에서는 JDK Dynamic Proxy 와 CGlib 을 통해 Proxy화 한다
  • JDK Dynamic Proxy는 Reflection을 기반으로 이루어지고, CGlib 은 상속을 기반으로 이루어진다

Spring AOP 동적 프록시 종류

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 코드로 아래와 같이 설정할 수 있습니다.

  1. JDK Dynamic Proxy

JDK 에서 제공하는 Dynamic Proxy는 1.3 버젼부터 생긴 기능이며,Interface를 기반으로 Proxy를 생성해주는 방식이다.

만약 target Object가 적어도 하나 이상의 Interface가 있다면 JDK dynamic proxy가 사용되며 interface를 구현한 메서드들이 proxy를 사용한다.

JDK dynamic proxy로 구현되면 interface가 존재하는 메서드만 proxy가 생성되며,
Interface를 강제화 한다는 단점이 있다.

Dynamic Proxy는 Invocation Handler를 상속받아서 실체를 구현하게 되는데, 이 과정에서 특정 Object에 대해 Reflection을 사용하기 때문에 성능이 조금 떨어진다.

package study.proxy;
 
import core.aop.pointcut.MethodMatcher;
import lombok.RequiredArgsConstructor;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
 
@RequiredArgsConstructor
public class UpperCaseHandler implements InvocationHandler {
 
    private final Car car;
    private final MethodMatcher methodMatcher;
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        final String methodName = (String) method.invoke(car, args);
        if (methodMatcher.matches(method)) {
            return methodName.toUpperCase();
        }
        return methodName;
    }
}

실제로 Invocation Handler를 상속받아서 구현하였고, Car/MethodMatcher 인터페이스를 의존성 주입하였다.
해당 인터페이스는 Method 를 선택적으로 Proxy화 하기 위해서, 의존성 주입으로 설정했다.

위 Invocation Handler는 invoke를 통해서 proxy 로직이 진행되며,invoke 라는 메서드 내부 로직에 Reflection을 해야한다.

  1. CGLIB Proxy

CGLIB Proxy는 오픈소스로 Enhancer를 바탕으로 Proxy를 구현하는 방식이다.
이 방식은 JDK Dynamic Proxy와는 다르게 Reflection을 사용하지 않고 Extends(상속) 방식을 이용해서
Proxy화 할 메서드를 오버라이딩 한다.

만약 target object가 interface를 구현되어있지 않다면 CGLIB proxy를 이용하여ㅍ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 {}

먼저 Proxy화를 진행할 Target Class를 생성한다.

package study.proxy;
 
public class CarTarget implements Car {
    @Override
    public String start(String name) {
        return "Car " + name + " started!";
    }
 
    @Override
    public String stop(String name) {
        return "Car " + name + " stopped!";
    }
}

start 메서드만 Proxy 화를 진행을 위해, MethodMathcer라는 인터페이스를 선언하고 상속해서 특정 조건에 의해 필터링 한다.

package study.proxy.matcher;
 
import core.aop.pointcut.MethodMatcher;
 
import java.lang.reflect.Method;
 
public class StartMethodMatcher implements MethodMatcher {
    private static final String TALK_PREFIX = "start";
 
    @Override
    public boolean matches(Method method) {
        final String methodName = method.getName();
 
        return methodName.startsWith(TALK_PREFIX);
    }
}

그 다음은

실제 Proxy로 핸들링할 Handler가 필요한데, CGlib 에서는 이를 MethodInterceptor 라는 인터페이스로 정의된다.

Intercept 라는 메서드를 오버라이딩 해서, Proxy 로직을 진행한다.

package study.proxy;
 
import core.aop.pointcut.MethodMatcher;
import lombok.RequiredArgsConstructor;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
import java.lang.reflect.Method;
 
@RequiredArgsConstructor
public class UpperCaseInterceptor implements MethodInterceptor {
 
    private final MethodMatcher methodMatcher;
 
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        final String methodName = (String) proxy.invokeSuper(obj, args);
        if (methodMatcher.matches(method)) {
            return methodName.toUpperCase();
        }
        return methodName;
    }
}

실제 테스트를 진행하면,

 @Test
    @DisplayName("cglib Proxy 테스트")
    void cglibProxyTest() {
        /* given */
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(CarTarget.class);
        enhancer.setCallback(new UpperCaseInterceptor(new StartMethodMatcher()));
 
        /* when */
        final Car proxiedCar = (Car) enhancer.create();
 
        /* then */
        assertThat(proxiedCar.start("huisam")).isEqualTo("CAR HUISAM STARTED!");
        assertThat(proxiedCar.stop("huisam")).isEqualTo("Car huisam stopped!");
    }

정상적으로 start 라는 메서드만 proxy 처리된 Proxied 된 객체로 실행된다.

Enhancer 객체는 반드시 SuperClass(부모클래스)를 지정해야 한다.
그 다음 CallBack 을 통해서 어떠한 Handler 를 설정할 것인지 바로 명시해야한다.

CGlib은 기본적으로 Byte 코드를 조작해서, 바이너리가 만들어지기 때문에 JDK Dynamic Proxy보다 성능이 우세하다.

다만, final 객체 혹은 private 접근자로 된 메서드는 상속(Override)가 지원되지 않기 때문에 Proxy 구현이 제약적이다.

그래서 AOP랑 무슨 관계인데?

위 코드에서 예제로 이용한 코드를 Spring AOP에 접목시켜보면

JDK Dynamic Proxy에서 InvocationHandler, CGlib 에서 MethodInterceptor는 Spring AOP에서 JoinPoint 라는 개념이다.

그리고 위에서 특정 조건에 의해 필터링 하는 MethodMatcher는 Spring AOP에서 PointCut 라는 개념이다.

마지막으로 Proxy 로직이 실행되는 JDK Dynamic Proxy에 invoke 메서드, CGlib 에서 Intercept 메서드는 Spring AOP에서 Advice 라는 개념이다.

실제로 Spring AOP를 활용한다고, 하나부터 열까지 다 직접 구현하지는 않는다.

@AspectJ(Class, Method 단위) 어노테이션과,
어떻게 Advice를 지정할 것인가에 대한 @Before, @Around, @AfterThrowing ...
특정 조건을 필터링할 Expression 을 기반으로 PointCut 설정한다.

profile
Back-end Developer

1개의 댓글

comment-user-thumbnail
2021년 11월 2일

재미있게 잘 보았습니담..!

답글 달기