본문 바로가기
Spring/spring AOP

스프링 코어 2 - 스프링이 지원하는 프록시(프록시 팩토리)

by 킹차니 2022. 1. 10.

프록시 팩토리

 

인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB을 작용하기 위해 스프링이 추상화된 편리한 인터페이스를 제공한다. 즉 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(Proxy Factory)라는 기능을 제공한다.

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

 

 

Q: 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는 InvocationHandlerCGLIB가 제공하는 MethodInterceptor를 각각 중복으로 따로 만들어야 할까?


스프링은 이 문제를 해결하기 위해 부가 기능을 적용할 때 Advice 라는 새로운 개념을 도입했다. 개발자는 InvocationHandler MethodInterceptor 를 신경쓰지 않고, Advice 만 만들면 된다.
결과적으로 InvocationHandler MethodInterceptor Advice 를 호출하게 된다.
프록시 팩토리를 사용하면 Advice를 호출하는 전용 InvocationHandler , MethodInterceptor 를 내부에서 사용한다.

개발자가 Advice를 구현하면 스프링이 adviceInvocationHandler 또는 adviceMethodInterceptor를 사용하여 개발자가 구현한 Advice를 호출하도록 해준다.

 

런터임 시에는 아래와 같다.

Q: 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
앞서 특정 메서드 이름의 조건에 맞을 때만 프록시 부가 기능이 적용되는 코드를 직접 만들었다. 스프링은 Pointcut 이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.

 

 


 

스프링이 제공하는 프록시 팩토리를 예제 코드로 살펴보자.

 

Advice 만들기

Advice 는 프록시에 적용하는 부가 기능 로직이다. 이것은 JDK 동적 프록시가 제공하는 InvocationHandler CGLIB가 제공하는 MethodInterceptor 의 개념과 유사하다. 둘을 개념적으로 추상화한 것이다. 프록시 패턴을 사용한다면 둘 대신에 Advice를 사용하면 된다.

Advice를 만드는 가장 기본적인 방법은 스프링이 제공하는 MethodInterceptor를 구현하는 것이다. (MethodInterceptor는 Interceptor를 상속하고 Interceptor는 Advice를 상속한다.)

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;//aopalliance 패키지의 MethodInterceptor 사용
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class TimeAdvice implements MethodInterceptor {

    //MethodInvocation에 target 클래스 정보가 담겨있다.
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("Time Proxy 실행");
        long startTime = System.currentTimeMillis();

        Object result = invocation.proceed();//invocation.proceed()를 호출하면 알아서 target을 찾아 호출해준다.

        long endTime = System.currentTimeMillis();
        log.info("TimeProxy 종료 resultTime = {}", endTime-startTime);
        return result;
    }
}

 

 

테스트에 사용할 서비스의 인터페이스와 구현체는 아래와 같다.

 

1. 인터페이스가 있는 경우를 테스트하기 위한 ServiceInterface와 그 구현체인 ServiceImpl

public interface ServiceInterface {
    void save();
    void find();
}

 

@Slf4j
public class ServiceImpl implements ServiceInterface {

    @Override
    public void save() {
        log.info("save 호출");
    }

    @Override
    public void find() {
        log.info("find 호출");
    }
}

 

2. 인터페이스가 없고 구체 클래스만 있는 경우를 테스트하기 위한 ConcreteService

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConcreteService {

    public void call() {
        log.info("ConcreteService 호출");
    }
}

 

이제 테스트해보자.

 

 

1. 인터페이스가 있는 경우를 테스트:

    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);//이때 proxyFactory에 target의 정보가 넘어감
        proxyFactory.addAdvice(new TimeAdvice());
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());

        proxy.save();
		
        // 프록시 팩토리로 프록시가 잘 적용되었는지 확인
        assertThat(AopUtils.isAopProxy(proxy)).isTrue(); //OK
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); //OK
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse(); //OK
    }


실행결과:
targetClass = class hello.proxy.common.ServiceImpl
proxyClass = class com.sun.proxy.$Proxy13 //JDK 동적 프록시
Time Proxy 실행
save 호출
TimeProxy 종료 resultTime = 0

 

 

 

2. 인터페이스가 없는 경우 테스트 :

    @Test
    @DisplayName("구체 클래스만 있으면 CGLIB 사용")
    void noInterfaceProxy() {
        ConcreteService target = new ConcreteService();
        ProxyFactory proxyFactory = new ProxyFactory(target);//이때 proxyFactory에 target의 정보가 넘어감
        proxyFactory.addAdvice(new TimeAdvice());
        ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());

        proxy.call();

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


실행결과:

targetClass = class hello.proxy.common.ConcreteService
proxyClass = class hello.proxy.common.ConcreteService$$EnhancerBySpringCGLIB$$3f3ec610
Time Proxy 실행
ConcreteService 호출
TimeProxy 종료 resultTime = 11

 

 

3. 인터페이스를 가지고 있지만 무조건 구체 클래스 기반으로 프록시 사용하기 ( setProxyTargetClass )

@Test
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
void proxyTargetClass() {
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.setProxyTargetClass(true);//CGLIB를 사용하고, 클래스 기반 프록시 사용
    proxyFactory.addAdvice(new TimeAdvice());
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
    log.info("targetClass = {}", target.getClass());
    log.info("proxyClass = {}", proxy.getClass());

    proxy.save();

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


실행결과:

targetClass = class hello.proxy.common.ServiceImpl
proxyClass = class hello.proxy.common.ServiceImpl$$EnhancerBySpringCGLIB$$eb04c714 //CGLIB
Time Proxy 실행
save 호출
TimeProxy 종료 resultTime = 10

프록시 팩토리는 proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다. 그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다.

 

참고
스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true 로 설정해서 사용한다. 따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다. 

 

 

 

 

 

김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.