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

[Spring] 프록시 팩토리

by 뜨끔쓰 2022. 9. 26.
728x90
728x90
인프런의 김영한님의 강의 스프링 핵심 원리 - 고급편을 학습하며 정리한 글입니다.

 

스프링 핵심 원리 - 고급편 (인프런)

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

프록시 팩토리란?

스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory)라는 기능을 제공하여 인터페이스가 존재하면 JDK 동적 프록시를 사용해 프록시를 생성하고, 구체 클래스만 있다면 CGLIB를 사용하여 동적 프록시를 생성 할 수 있게 만들어 준다.

 

프록시 팩토리 사용흐름

 

프록시 팩토리가 동적으로 프록시를 생성해준다고 하였는데 그렇다면 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor을 각각 중복으로 만들어야 할까?

일반적으로 생각하면 그렇게 해야 할 것 같지만 이부분도 스프링이 해결해준다.

이문제를 해결하기 위해 스프링은 Advice라는 새로운 개념을 도입하여 처리하기 때문에 개발자는 신경쓰지 않고 Advice만 생성하면 된다! 이 얼마나 편리한가!? 스프링 없는 개발은 정말 끔찍 할 것 같다.

 


Advice 만들기

Advice는 프록시에 적용하는 부가 기능 로직이다. 프록시 팩토리를 사용하면 JDK 동적프록시(InvocationHandler), CGLIB(MethodInterceptor)의 개념과 유사하다. 둘을 개념적으로 추상화 한 것이기 때문에 Advice를 사용하면 된다.

 

Advice를 생성할때는 많은 방법이 있지만 MethodInterceptor 인터페이스를 구현하면 됩니다.

 

MethodInterceptor
public interface MethodInterceptor extends Interceptor {

	/**
	 * Implement this method to perform extra treatments before and
	 * after the invocation. Polite implementations would certainly
	 * like to invoke {@link Joinpoint#proceed()}.
	 * @param invocation the method invocation joinpoint
	 * @return the result of the call to {@link Joinpoint#proceed()};
	 * might be intercepted by the interceptor
	 * @throws Throwable if the interceptors or the target object
	 * throws an exception
	 */
	@Nullable
	Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;

}

인터페이스 내부를 보면 이렇게 되어 있습니다.

 

invoke 메소드의 인자값으로 MethodInvocation을 가져올 수 있는데 내부에는 다음 메소드 호출방법, 현재 프록시 객체 인스턴스, args, 메서드 정보등이 포함되어 있기 때문에 적절히 사용하면 된다.

CGLIB의 MethodInterceptor와 이름이 같으므로 패키지 이름에 주의가 필요하다.

 

또한 Interceptor을 상속하고 있는데 내부적으로 타고 가보면 최종적으로 Advice인터페이스를 상속하고 있다.

 

그럼 이제 실제 사용 할 Advice를 만들어보자!

 

TimeAdvice
class TimeAdvice: MethodInterceptor {

    private val log = LoggerFactory.getLogger(TimeAdvice::class.java)
    override fun invoke(invocation: MethodInvocation): Any? {
        log.info("TimeProxy 실행")
        val startTime = System.currentTimeMillis()

        /* 리플렉션을 사용하여 target 인스턴스의 메서드를 실행, args는 메서드 호출시 넘겨줄 인수 */
//        val result = method.invoke(target, *(args ?: arrayOfNulls<Any>(0)))
        val result = invocation.proceed()

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

기존에 Proxy를 생성하던 코드와 별반 다른부분은 없지만 기존에는 method.invoke()를 호출하여 target클래스의 메소드를 호출 했다면 invocation.proceed()를 호출하여 메서드를 호출하는 부분이 바뀌었다.

 

이제 테스트코드를 작성하여 잘 작동하는지 확인해봅시다.

여기에서는 인터페이스로 작성한 테스트코드만 실습을 해보려고합니다. 

interfaceProxy
    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    fun interfaceProxy(){
        val target = ServiceImpl()
        val proxyFactory = ProxyFactory(target)
        proxyFactory.addAdvice(TimeAdvice())
        val proxy = proxyFactory.getProxy() as ServiceInterface
        log.info("targetClass={}", target.javaClass)
        log.info("proxyClass={}", proxy.javaClass)

        proxy.save()

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

    }

코드를보면 AopProxy인지 JDK동적 프록시인지 CglibProxy인지 확인하는 코드가 있습니다.

 

결과를 확인해봅시다.

 

테스트코드 결과

테스트가 잘 통과한 것을 볼 수 있고 Advice도 잘 적용된 것을 확인 할 수 있습니다.

 


여기까지 따라하고나면 또 한가지 의문이 생기는데 기존에 내가 원하는 곳에서만 Proxy를 생성하여 Advice를 적용할 순 없을까? 생각 해볼 수 있는데 당연히도 역시나 스프링에서는 손쉽게 적용 할 수 있도록 만들어 놓았습니다.

 

바로 어드바이저를 생성하면 이모든것을 적용이 가능합니다. 함께 확인해봅시다.

 

  • 포인트컷(Pointcut): 어디에 부가 기능을 적용할지, 하지 않을지 판단하는 필터링 로직 주로 클래스와 메서드 이름으로 필터링한다. 이름 그대로 어떤 포인트(Point)에 기능을 적용할지 안할지 잘라서(cut) 구분한다고 보면된다.
  • 어드바이스(Advice): 방금전에 본 것 처럼 프록시가 호출하는 부가 기능부분이다. 단순하게 프록시 로직이라 생각하면 된다.

  • 어드바이저(Advisor): 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 조합하면 어드바이저가 만들어진다.

 

전체 구조를 그림으로 기억해봅시다.

어드바이저의 전체 흐름

이걸 잘 기억해놨다가 이제 어드바이저를 직접 코드로 확인해봅시다.

 

예제에는 간단히 스프링이 제공하는 포인트컷을 이용하여 메소드명에 save가 들어있으면 어드바이스를 적용하도록 테스트 코드를 작성해봅시다. 

 

advisorTest
    @Test
    @DisplayName("스프링이 제공하는 포인트컷")
    fun advisorTest() {
        val target = ServiceImpl()
        val proxyFactory = ProxyFactory(target)
        val pointcut = NameMatchMethodPointcut()
        pointcut.setMappedNames("save")
        val advisor = DefaultPointcutAdvisor(pointcut, TimeAdvice())
        proxyFactory.addAdvisor(advisor)
        val proxy = proxyFactory.proxy as ServiceInterface

        proxy.save()
        proxy.find()
    }

코드는 간단합니다. 스프링에서 제공해주는 NameMatchMethodPointcut을 이용하여 save라는 문자열을 넣어줍시다.

 

이렇게하고 테스트코드를 실행하면 save()메소드를 호출할때만 TimeAdvice가 작동 할 것입니다.

테스트 결과

확인해보면 save 호출 전,후로 log가 찍히는것을 확인 할 수 있습니다. 

 


이렇게 이번글에서는 스프링에서 제공해주는 Advisor에 대하여 정리해보았는데요.

 

정말 스프링을 공부하면 할 수록 개발자가 불편하다고 생각한 것들은 대부분 제공해주고 있다는 사실이 놀랍습니다.

 

앞으로도 공부하면 글로 정리하여 제것으로 만들어서 사용 할 수 있도록 열심히 해야 할 것 같습니다.

 

긴글 읽어주셔서 감사합니다.

 

 

728x90
반응형

댓글