[Spring] Proxy를 적용하는 다양한 방법 : ProxyFactory (1)

 

프록시 패턴

proxy pattern diagram

 

프록시 패턴은 하나의 객체가 다른 객체의 대리자 역할을 하도록 설계하는 디자인 패턴으로 대리자는 실제 객체와 동일한 인터페이스를 가지며, 클라이언트는 실제 객체를 직접 사용하는 대신 대리자를 통해 간접적으로 상호작용한다.

 

이 패턴을 사용함으로써 얻는 이점은 다음과 같다 :

1. 로깅 : 프록시 객체는 클라이언트의 요청과 실제 객체의 작업 결과를 로깅할 수 있다. 

2. 보안 강화 : 프록시 객체가 클라이언트의 요청을 검증하고 필요한 권한 확인 등의 작업을 수행함으로써 보안상의 이점을 가져간다. 

3. 성능 최적화 : 실제 객체가 생성 및 초기화에 많은 비용이 드는 경우, 프록시 객체를 사용하여 실제 객체가 필요한 시점에 생성하거나 초기화할 수 있다. 

 

스프링에서는 프록시 객체를 만드는 여러 가지 방법을 제공하고 있으며 가장 대표적인 방법 몇 가지를 정리하려 한다.

 

ProxyFactory

자바에서 프록시 객체를 만드는 대표적인 방법으로 JDK 리플렉션CGLIB 라이브러리가 사용된다. 조금 더 대중적인 단어로는 JDK 동적 프록시CGLIB라고도 하는데 이 둘의 가장 큰 차이는 JDK 동적 프록시는 인터페이스를 기반으로 프록시 객체를 생성하고 CGLIB는 구체 클래스 기반으로 프록시 객체를 생성한다.

 

스프링은 다양한 케이스를 커버하기 위해 두 기능을 모두 지원하고 이것을 ProxyFactory로 추상화했다.

public class ProxyFactory extends ProxyCreatorSupport {

	public ProxyFactory() {
	}

	public ProxyFactory(Object target) {
		setTarget(target);
		setInterfaces(ClassUtils.getAllInterfaces(target));
	}

```
}

 

프록시 객체에 적용할 행동JDK 동적 프록시는 InvocationHandler에, CGLIB는 MethodInterceptor에 정의했는데 스프링은 이것 또한 MethodInterceptor로 추상화했다. (CGLIB의 MethodInterceptor와 다르다.)

@FunctionalInterface
public interface MethodInterceptor extends Interceptor {

	@Nullable
	Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;

}

 

 

JDK 동적 프록시 예제

프록시를 적용할 객체(target)을 생성하고 ProxyFactory에 target과 행동(advice)을 전달한다.

기본적으로 인터페이스가 존재하면 자동으로 JDK 동적 프록시를 사용해서 인터페이스 기반 프록시 객체를 생성한다.

@Test
@DisplayName("인터페이스가 존재하면 JDK 동적 프록시 사용")
void interfaceProxy() {
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvice(new TimeAdvice());

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

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

 

 

CGLIB

JDK 동적 프록시와 마찬가지로 target을 생성하고 advice를 전달한다.

인터페이스가 없다면 자동으로 CGLIB를 사용해서 구체 클래스 기반으로 프록시 객체를 생성한다.

@Test
@DisplayName("구체 클래스가 있으면 CGLIB 사용")
void concreteProxy() {
    ConcreteService target = new ConcreteService();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvice(new TimeAdvice());

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

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

 

 

CGLIB를 강제로 사용하기

ProxyFactory가 제공하는 setProxyTargetClass를 활성화하면 인터페이스 유무와 상관없이 CGLIB를 사용해서 프록시 객체를 생성한다.

@Test
@DisplayName("proxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB을 사용하고 클래스 기반 프록시 사용")
void proxyTargetClass() {
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);

    // ServiceImpl 기반으로 상속받아서 프록시 생성
    proxyFactory.setProxyTargetClass(true);
    proxyFactory.addAdvice(new TimeAdvice());

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

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