스프링의 프록시 팩토리 - 1

이원석·2024년 2월 28일

Spring

목록 보기
17/20
post-thumbnail
*인프런 김영한 강사님의 강좌를 참고하여 정리한 내용입니다.*

프록시는 클래스의 수 만큼 생성해야 한다는 단점이 있었다. 이를 해결할 수 있는 두 가지 동적 프록시 기술 (JDK 동적 프록시, CGLIB)에 대해 정리해보았다.

그러나 동적 프록시 또한 문제점이 있었는데..

  • JDK 동적 프록시 (인터페이스 위임 기반) - InvocationHandler
  • CGLIB (클래스 상속 기반) - MethodInterceptor

특정 조건(인터페이스, 클래스)에 맞추어 자동으로 프록시 기술을 적용하는 기능이 공통으로 제공되는 ProxyFactory에 대해 알아보자!


ProxyFactory

지금까지 보다시피 스프링은 유사한 구체적인 기술들이 있을 때, 그것을 통합하여 일관성있게 접근할 수 있는 추상화된 기술을 제공한다.


구조

보다시피 사용자가 구조에 따라 동적 프록시 생성 기술을 선택하는 것이 아닌, ProxyFactory에 요청만 하면 어떠한 기술(JDK, CGLIB)을 사용할지 선택한다.

그렇다면 ProxyFactory에서 부가 기능을 적용하기 위해 JDK 동적 프록시의 InvocationHandler, CGLIB의 MethodInterceptor 둘 다 만들어야 할까?


Advice

스프링에서는 이러한 문제를 해결하기 위해 Advice라는 개념을 도입한다. Advice를 만들면 프록시 팩토리에서 Advice를 호출하는 전용 InvocationHandler, MethodInterceptor를 내부에서 호출하여 공통 부가 기능을 적용한다.

부가 기능을 적용하고 싶지 않은 상황이 있을수도 있다. (특정 메서드는 적용하지 않을래) 이 경우에는 Pointcut 이라는 개념을 도입하여 해결할 수 있다. (Filter와 같은 맥락)


구현

// 스프링 AOP 모듈 패키지 (spring-aop)
package org.aopalliance.intercept;

public interface MethodInterceptor extends Interceptor {
	Object invoke(MethodInvocation invocation) throws Throwable;
}

Advice를 만들기 위해서는 MethodInterceptor를 구현해야 한다. CGLIB의 MethodInterceptor과 이름이 같지만 패키지가 다르다.

  1. JDK 동적 프록시 - InvocationHandler
  2. GCLIB - MethodInterceptor
    이 둘을 개념적으로 추상화 하면?
  3. Advice - MethodInterceptor

실행시간 측정 기능 Advice 구현 클래스

public class TimeAdvice implements MethodInterceptor {
	// target은 MethodInvocation에 포함되어 있다.
	// private Object target;

	@Override
    public Object invoke(MethoInvocation invocation) throws Throwable {
        // 시간을 측정하는 로직..
        
        Object result = invocation.proceed();
        
        return result;
    }
}

기존의 JDK, CGLIB 방식과는 다르게 Methodinvoke하지 않는다. invocation.proceed()만 호출하면 target 클래스의 인스턴스를 호출한다. invocationtarget 클래스의 정보가 모두 포함되어있기 때문이다.

그렇다면 invocationtarget 정보는 언제 전달받는거지?


ProxyFactory 테스트 코드

@Test
@DisplayName("인터페이스/클래스가 있으면 JDK/CGLIB 동적 프록시 사용")
void interfaceProxy() {
	// 1. 실제 사용되는 구현체 target (JDK ver, CGLIB ver)
	ServiceInterface target = new ServiceImpl();
    // ConcreteService target = new ConcreteService();
    
    // 2. ProxyFactory에 target 생성자 주입
	ProxyFactory proxyFactory = new ProxyFactory(target);
    // 3. Advice 설정
	proxyFactory.addAdvice(new TimeAdvice());
    
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
    
    proxy.save();
    
    // 4. assertion
    // 프록시 팩토리를 통해 생성된 프록시인가? -> true
    assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    
    // 프록시 팩토리를 통해 생성된 JDK 동적 프록시인가? -> true
	assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
    
    // 프록시 팩토리를 통해 생성된 CGLIB 동적 프록시인가? -> false
	assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}

결과

ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl 
ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy13

부가 기능 로직 수행..

2번의 new ProxyFactory(target)에서 프록시 호출 대상인 target을 넘겨주는 것을 볼 수 있다. ProxyFactory에서는 target이 인터페이스 기반 구현체인지, 상속 기반 구현체인지를 확인하여 JDK, CGLIB 동적 프록시 방법중 선택한다.

3번의 proxyFactory.addAdvice(new TimeAdvice())에서 부가 기능 로직 Advice를 설정한다.

4번의 AopUtils를 통해 프록시를 검증할 수 있다.



ProxyTargetClass 옵션

ProxyFactory 테스트 코드 (ProxyTargetClass 추가)

@Test
@DisplayName("ProxyTargetClass 옵션은 무조건 CGLIB의 클래스 기반 프록시 사용")
void interfaceProxy() {
	// 1. 실제 사용되는 구현체 target 
	ServiceInterface target = new ServiceImpl();
    
    
    // 2. ProxyFactory에 target 생성자 주입
	ProxyFactory proxyFactory = new ProxyFactory(target);
    
    // 3. 무조건 클래스 기반의 프록시를 만들도록 설정
    proxyFactory.setProxyTargetClass(ture);
    
    // 4. Advice 설정
	proxyFactory.addAdvice(new TimeAdvice());
    
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
    
    proxy.save();
    
    // 4. assertion
    // 프록시 팩토리를 통해 생성된 프록시인가? -> true
    assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    
    // 프록시 팩토리를 통해 생성된 JDK 동적 프록시인가? -> false
	assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
    
    // 프록시 팩토리를 통해 생성된 CGLIB 동적 프록시인가? -> true
	assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}

결과

ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl 
ProxyFactoryTest - proxyClass=class hello.proxy.common.service.ServiceImpl$$EnhancerBySpringCGLIB$$2bbf51ab 

부가 기능 로직 수행..

결과를 보면 인터페이스 기반 target의 동적 프록시를 생성했지만, CGLIB 기반의 프록시가 생성되었다. 스프링 부트는 AOP 설정시 디폴트가 proxyTargetClass=true 라고 한다.



ProxyFactory의 추상화 덕분에 JDK, CGLIB에 의존하지 않고 동적 프록시를 생성할 수 있게 되었다. 또한, Advice를 통해 부가 기능도 JDK, CGLIB같이 특정 기술에 종속적이지 않게 하나로 사용할 수 있게 되었다. 이는 ProxyFactory에서 Advice를 호출하는 전용 클래스를 호출하기 때문이다.

JDK -> InvocationHandler -> Advice
CGLIB -> MethodInterceptory -> Advice





참고문헌
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

0개의 댓글