개발놀이터

프록시 팩토리 본문

Spring/Spring

프록시 팩토리

마늘냄새폴폴 2022. 1. 19. 16:58

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 고급 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요

 

 

앞서 마지막에 설명했던 동적 프록시를 사용할 때 문제점을 다시 확인해보자

 

문제점

1. 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?2. 두 기술을 함께 사용할 때 부가기능을 제공하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 만들어서 관리해야할까?

 

 

Q: 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?

 

스프링은 유사한 구체적인 기술들이 있을 때 그것들을 통합해서 일관성 있게 접근할 수 있고, 더욱 편리하게 사용할 수 있는 추상화된 기술을 제공한다.

 

스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리라는 기능을 제공한다.

 

이전에는 사오항에 따라서 JDK 동적 프록시를 사용하거나 CGLIB를 사용해야 했다면, 이제는 이 프록시 팩토리 하나로 편리하게 동적 프록시를 생성할 수 있다.

 

 

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

 

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

 

프록시 팩토리를 사용하면 Advice를 호출하는 전용 InvocationHandler, MethodInterceptor를 내부에서 사용한다.

 

 

그럼 이제 Advice에 대해 알아보자

 

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

 

Advice를 만드는 방법은 여러가지가 있지만, 기본적인 방법은 다음 인터페이스를 구현하면 된다.

-MethodInvocation invocation : 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보등이 포함되어 있다. 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.

 

-CGLIB의 MethodInterceptor와 이름이 같으므로 패키지 이름에 주의하자. 참고로 여기서 사용하는 org.aopalliance.intercept 패키지는 스프링 AOP 모듈 안에 들어있다. 

 

 

이제 Advice를 만들어보자

@Slf4j
public class TimeAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

//        Object result = method.invoke(target, args);
        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();

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

        return result;
    }
}

Object result = invocation.proceed()

invocation.proceed()를 호출하면 target 클래스를 호출하고 그 결과를 받는다. 그런데 기존에 보았던 코드들과 다르게 target 클래스의 정보가 보이지 않는다. target클래스의 정보는 MethodInvocation invocation안에 모두 포함되어 있다. 

 

그 이유는 바로 다음에 확인할 수 있는데, 프록시 팩토리로 프록시를 생성하는 단계에서 이미 target정보를 파라미터로 전달받기 때문이다. 

 

그럼 정말 인터페이스가 있으면 JDK 동적프록시를 없으면 CGLIB를 사용하는지 예제를 통해 알아보자

 

ServiceInterface.java

public interface ServiceInterface {

    void save();

    void find();
}

ServiceImpl.java

@Slf4j
public class ServiceImpl implements ServiceInterface {
    @Override
    public void save() {
        log.info("save 호출");
    }

    @Override
    public void find() {
        log.info("find 호출");
    }
}
    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new 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();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }

실행결과

실행해보면 프록시가 정상적용된것을 확인할 수 있다. proxyClass=class com.sun.proxy.$Proxy13 코드를 통해 JDK 동적 프록시가 적용된 것도 확인할 수 있다.

 

인터페이스가 있을 땐 JDK 동적 프록시를 사용했다. 그럼 인터페이스가 없는 구체클래스에는 CGLIB이 동작하는지 알아보자

ConcreteService.java

@Slf4j
public class ConcreteService {

    public void call() {
        log.info("ConcreteService 호출");
    }
}
    @Test
    @DisplayName("구체 클래스만 있으면 CGLIB 사용")
    void concreteProxy() {
        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();
    }

실행결과

ServiceImpl$$EnhancerBySpringCGLIB...를 보면 CGLIB 기반의 프록시가 생성된 것을 확인할 수 있다. 

 

 

마지막으로 인터페이스가 있지만 CGLIB를 사용해서 인터페이스가 아닌 클래스 기반으로 동적 프록시를 만들수도 있다. 프록시 팩토리는 proxyTargetClass라는 옵션을 제공하는데 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다.

 

proxyFactory.setProxyTargetClass(true); 이렇게 사용하면 된다.