본문 바로가기
Spring

[Spring] 스프링 프록시 팩토리 간단 정리

by 개미가되고싶은사람 2024. 9. 19.

목차

     

    프록시 팩토리의 필요성: JDK 동적 프록시와 CGLIB의 한계

    JDK 동적 프록시와 CGLIB는 각각 인터페이스와 클래스 기반으로 작동하는 프록시 생성 방식으로, 각기 다른 사용 조건과 제한이 있습니다. JDK 동적 프록시는 인터페이스를 기반으로 하므로 반드시 인터페이스가 필요하며, final 메서드나 private 메서드에 대해서는 적용할 수 없는 제한이 있습니다. 반면, CGLIB는 바이트코드를 조작하여 클래스 기반의 프록시를 생성하지만, final 메서드는 오버라이드할 수 없고 성능 저하와 메모리 사용량 증가 등의 문제를 야기할 수 있습니다.

    이러한 제안사항들 때문에 스프링은 JDK 동적 프록시와 CGLIB를 편리하게 사용할 수 있도록 추상화된 기술을 제공합니다. 스프링 프록시 팩토리는 이 두 가지 방식을 모두 지원하여 상황에 맞는 적절한 방법을 선택할 수 있게 합니다. 이를 통해 AOP(Aspect-Oriented Programming)를 지원하며, 메서드 호출을 가로채고 부가적인 기능을 적용할 수 있는 유연성을 제공합니다. 또한, 어노테이션을 통해 간편하게 프록시를 설정할 수 있어, 복잡한 프록시 생성 과정에 신경 쓰지 않고 비즈니스 로직에 집중할 수 있습니다. 이렇게 스프링 프록시 팩토리는 두 기법의 사용 조건을 효과적으로 추상화하여 더욱 편리하게 활용할 수 있도록 도와주는 기술 입니다.

     

    프록시 팩토리(Proxy Factory)

    프록시 팩토리는 실제 객체에 대한 대리자 역할을 하는 프록시 객체를 쉽게 생성할 수 있도록 도와주는 기술입니다. 이 기술은 프록시 기술이므로 클라이언트는 실제 객체에 직접 접근하는 대신 프록시를 통해 접근하게 되어, 여러 가지 부가 기능을 추가할 수 있습니다.
    프록시 팩토리의 중요한 특징 중 하나는 인터페이스가 있는 경우 JDK의 동적 프록시를, 클래스 기반일 경우 CGLIB 프록시를 자동으로 선택하여 사용하는 것입니다. 이런 장점을 이용해 프록시 객체에 원하는 부가 기능을 추가하거나, 메서드 호출 전후에 특정 로직을 삽입하는 유연성을 가질 수 있습니다.

     

    프록시 팩토리의 구성 요소

    프록시 팩토리는 다음과 같은 구성 요소로 이루어져 있습니다:

    • ProxyFactory: 실제로 프록시 객체를 생성하는 클래스입니다. 클라이언트와 실제 객체 사이에서 중재 역할을 하며, 요청을 처리하는 프록시를 제공합니다.
    • Advisor: 어떤 조언(기능)을 적용할지 결정하는 역할을 합니다. Advisor는 Pointcut과 Advice를 모두 포함하여, 어떤 기능을 어느 시점에서 적용할지를 정의합니다.
    • Pointcut: 적용될 클래스, 메서드가 실행될 특정 조건을 정의합니다. Pointcut은 메서드뿐만 아니라 클래스나 패키지, 접근 제한자 등 다양한 기준으로 기능이 적용될 대상을 필터링할 수 있습니다.
    • Advice: 특정 메서드 호출 전후에 실행될 코드를 정의합니다. 
      • Advice에는 Before Advice(메서드 실행 전), After Returning Advice(메서드 성공 후), After Throwing Advice(예외 발생 후), Around Advice(메서드 실행 전후 모두 포함)와 같은 다양한 유형이 존재합니다.

     

    프록시 팩토리 예제 코드

    프록시 팩토리를 적용할 인터페이스나 클래스는 따로 만들지 않는 점 주의바랍니다.!!!

    1  Advice클래스 생성

    import org.aopalliance.intercept.MethodInterceptor를 사용해야 합니다.

    @Slf4j
    public class TimeAdvice implements MethodInterceptor {
    
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            log.info("TimeAdvice.invoke 실행");
            long startTime = System.currentTimeMillis();
    
            Object result = invocation.proceed();
          
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("TimeAdvice.invoke 종료 resultTime={}ms", resultTime);
            return result;
        }
    }

    invocation.proceed()메소드는 실제 객체를 호출하고 그 결과를 반환합니다. JDK 동적 프록시와 CGLIB는 실제 사용할 클래스를 주입 받아 명시해야 하지만, 스프링이 제공하는 프록시 팩토리는 invocation.proceed()를 사용함으로써 실제 객체를 직접 지정할 필요가 없습니다. 
    이는 프록시 팩토리를 생성하는 과정에서 이미 실제 객체에 대한 정보가 파라미터로 전달되어, 스프링 빈을 통해 스프링 컨테이너에서 관리되기 때문입니다.

     

    2-1 예제 테스트 코드 작성

    @Test
    @DisplayName("인터페이스가 있으니 JDK 사용")
    void interfaceProxy() {
        // 1. 타겟 객체 생성(실제 객체)
        ServiceInterface target = new ServiceImpl();
        
        // 2. 타겟 객체를 주입받아 ProxyFactory 생성
        ProxyFactory proxyFactory = new ProxyFactory(target);
        
        // 3. 프록시에 추가하여 메서드 호출 시 추가적인 기능을 추가
        proxyFactory.addAdvice(new TimeAdvice());
        
        // 4. 프록시 객체 생성
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
    
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    
        // 5. 프록시 객체를 통해 메서드 호출
        proxy.save();
        
        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    }

    AopUtils 클래스의 메서드는 스프링 프록시 팩토리를 통해 생성된 프록시 객체를 사용해야 하며, 원하는 결과가 반환됩니다.

     

    2-2 예제 테스트 코드 작성

        @Test
        @DisplayName("인터페이스가 있지만 CGLIB를 사용")
        void proxyTargetClass() {
            ServiceInterface target = new ServiceImpl();
            ProxyFactory proxyFactory = new ProxyFactory(target);
            proxyFactory.setProxyTargetClass(true); // 해당 옵션을 false로 설정하면, JDK 동적 프록시를 사용 
            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();
        }

     

    중요!!! 🚫🚫

    스프링 부트에서 AOP(스프링 프록시 팩토리)를 적용할 때, 기본적으로 @Configuration 클래스와 AOP설정에서 proxyTargetClass가 true로 설정되어 있습니다. 이 설정으로 인해 인터페이스가 존재하더라도 항상 CGLIB를 사용하여 구체 클래스를 기반으로 프록시를 생성합니다.

    -> 해당 설정은 스프링 부트 2.x 버전부터 변경된 내용이라고 합니다.