Spring Advanced

스프링이 지원하는 ProxyFactory

cwchoiit 2023. 12. 14. 14:45
728x90
반응형
SMALL

https://cwchoiit.tistory.com/79

 

Proxy/Decorator Pattern 2 (동적 프록시)

https://cwchoiit.tistory.com/78 Proxy/Decorator Pattern 이제 스프링에서 굉장히 자주 사용되는 프록시와 데코레이터 패턴을 정리해 보자. 이 프록시 패턴을 이해하니 스프링이 어떻게 내가 만들어서 컴포넌

cwchoiit.tistory.com

https://cwchoiit.tistory.com/78

 

Proxy/Decorator Pattern

이제 스프링에서 굉장히 자주 사용되는 프록시와 데코레이터 패턴을 정리해 보자. 이 프록시 패턴을 이해하니 스프링이 어떻게 내가 만들어서 컴포넌트 스캔 대상에 넣은 클래스를 프록시로 주

cwchoiit.tistory.com

 

728x90
SMALL

위 두개의 포스팅에서 프록시 패턴을 자세히 배워봤다. 최초에는 프록시를 직접 만들어서 하나하나 빈으로 등록하여 사용했고 이후에는 이 프록시를 하나하나 만들어내는 것이 비효율적이라 동적 프록시를 사용해서 한 개의 프록시로 여러 클래스에 프록시를 입힐 수 있게 됐다. 그러나 아직 동적 프록시를 사용할 때 문제가 되는 부분을 해결하지 못했다.

 

동적 프록시를 사용할 때 JDK 동적 프록시는 인터페이스만을 취급하고 CGLIB는 구체 클래스만을 취급한다는 것. 그래서 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용해야 하는데 어떻게 해야할까?

 

스프링이 제공하는 'ProxyFactory'를 사용하면 된다.

 

ProxyFactory

프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다. 

말보단 코드를 작성해보자. 우선은 Advice를 만들어야 한다.

Advice는 프록시가 제공해주는 부가 기능을 정의한 로직이라고 생각하면 된다. 예를 들어, 실제 객체의 메서드 A()를 호출한다고 하면 그 A()메서드에 추가적으로 부가할 기능과 실제 A()메서드 호출 로직이 하나의 Advice라고 생각하면 될 것 같다.

TimeAdvice.java

package com.example.advanced.common.advice;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class TimeAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy Start");

        long startTime = System.currentTimeMillis();

        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();

        log.info("TimeProxy End. ResultTime = {}ms", endTime - startTime);
        return result;
    }
}

 

Advice라고 했는데 CGLIB를 만들어낼 때 구현하는 MethodInterceptor를 구현했다. 왜 그럴까? MethodInterceptor가 상속받는 Interceptor가 있는데 이 Interceptor가 상속받는 인터페이스가 Advice이기 때문이다.

 

그리고 invoke()에서 invocation.proceed()를 호출하는데 이게 실제 객체의 메서드를 호출하는 부분이라고 보면 된다. 원래 기존에 프록시는 항상 실제 객체를 주입받아야 한다고 했는데 여기에는 그런 코드가 없다. 이는 ProxyFactory를 만들 때 전달해주기 때문이다. 말보단 코드. ProxyFactory를 만들어내는 코드를 보자. 

 

ProxyFactoryTest.java

package com.example.advanced.proxyfactory;

import com.example.advanced.common.advice.TimeAdvice;
import com.example.advanced.common.service.ServiceImpl;
import com.example.advanced.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;

import static org.assertj.core.api.Assertions.*;

@Slf4j
public class ProxyFactoryTest {

    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시를 사용")
    void interfaceFactory() {
        // 프록시로 만들어 낼 실제 객체
        ServiceInterface target = new ServiceImpl();

        // ProxyFactory를 사용해서 동적 프록시를 만든다.
        ProxyFactory proxyFactory = new ProxyFactory(target);

        // ProxyFactory를 이용하기 위해선 스프링에서 제공하는 Advice 인터페이스 구현한 클래스가 필요하다.
        // 그 Advice를 상속받는 Interceptor를 상속받는 MethodInterceptor를 구현한 클래스를 만들었다. (TimeAdvice)
        proxyFactory.addAdvice(new TimeAdvice());

        // proxyFactory에서 proxy를 꺼내온다. 이 proxy는 인터페이스를 제공했으면 JDK Dynamic Proxy로 만들어지고
        // 구체 클래스를 제공했으면 CGLIB 프록시로 만들어진다.
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());

        // 만든 프록시로 실제 객체(ServiceImpl)의 메서드를 실행
        proxy.save();

        // AopUtils.isAopProxy()는 스프링에서 제공해주는 기능인데 Proxy 인지를 알려준다.
        // ProxyFactory를 사용해서 만든 Proxy여야만 사용할 수 있다.
        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }
}

 

프록시로 만들어 낼 실제 객체를 생성한다. 그 부분이 위에서 target을 만드는 코드이다.

그리고 ProxyFactory 객체를 만들어낸다. 이 때 target이 넘어간다.

이 ProxyFactory 객체에서 addAdvice()로 위에서 만든 TimeAdvice()를 전달한다.

 

이렇게 ProxyFactory를 만들기 때문에 Advice 클래스에서는 실제 객체를 주입받을 필요가 없는 것.

그리고 getProxy()를 통해 프록시를 받아온다. 이러면 끝난다.

 

확인을 위해 ApoUtils를 이용한다. 이 isAopProxy는 ProxyFactory를 통해 만들어진 프록시인지를 확인한다.

isJdkDynamicProxy()는 JDK 동적 프록시로 만들어졌는지를 확인하고, isCglibProxy()는 CGLIB로 만든 동적 프록시인지 확인한다.

 

우리는 인터페이스를 넘겨줬기 때문에 JDK 동적 프록시로 만들어졌음을 확인할 수 있다.

 

 

이제 인터페이스 말고 구체 클래스를 넘겨줘보자.

@Test
@DisplayName("구체 클래스만 있으면 CGLIB 사용")
void concreteFactory() {
    ConcreteService target = new ConcreteService();

    ProxyFactory proxyFactory = new 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();
    assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
    assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}

 

ConcreteService는 구체 클래스다. 이 객체를 'target'으로 넘기면 ProxyFactory는 CGLIB로 동적 프록시를 만든다.

나머지 내용은 위 인터페이스를 넘긴것과 동일하다.

 

 

ProxyFactory한테 인터페이스를 넘겨도 CGLIB로 만들어달라고 할 수 있다.

ProxyFactory가 가지고 있는 setProxyTargetClass() 메서드를 사용하면 된다. 이 메서드는 이름 그대로 Proxy를 Target의 클래스로 설정하겠다는 의미다. 파라미터로 'true'를 넘기면 된다.

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

    ProxyFactory proxyFactory = new ProxyFactory(target);

    // Proxy를 만드는데 Target의 클래스를 기반으로 (즉, 구체 클래스로) 만들것인지에 대한 옵션. 중요하다⭐️
    proxyFactory.setProxyTargetClass(true);

    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();
    assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
    assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}

 

궁금한 점

근데, JDK 동적 프록시는 InvocationHandler 라는 인터페이스를 상속받은 클래스를 직접 구현해서 공통 로직과 실제 객체 호출을 하는 메서드를 만들고, CGLIB는 MethodInterceptor라는 인터페이스를 구현해서 공통 로직과 실제 객체 호출을 하는 메서드를 만드는데 어떻게 이 ProxyFactory는 그냥 Advice라는것 하나만 만들면 이 두개를 분개할까? ProxyFactory는 내부적으로 만들어진 프록시가 JDK 동적 프록시라면 InvocationHandler가 Advice를 호출하도록 개발하고 CGLIB 프록시라면 MethodInterceptor가 Advice를 호출하도록 개발해두었다. 그래서 상관없이 결국 Advice만 만들면 되는것이다.

 

결론

ProxyFactory와 Advice로 이젠 동적 프록시를 만들 때 JDK 동적 프록시를 사용해야하나 CGLIB를 사용해야하나 번거로움을 해소할 수 있었다. 그런데 Advice는 갑자기 어디서 튀어나온 개념일까? 다음 포스팅에서 Advice, Advisor, Pointcut 개념을 정리하고자 한다.

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

빈 후처리기(BeanPostProcessor)  (2) 2023.12.27
Advisor, Advice, Pointcut  (0) 2023.12.15
Proxy/Decorator Pattern 2 (동적 프록시)  (0) 2023.12.14
Proxy/Decorator Pattern  (0) 2023.12.13
Strategy Pattern  (2) 2023.12.12