본문 바로가기
Spring

프록시 팩토리

by sangyunpark99 2025. 3. 11.
인터페이스와 클래스를 둘다 사용해서
JDK 동적 프록시와 CGLIB를 사용하는 경우엔 어떻게 해야할까?

 

 

 

이번글은  프록시 팩토리에 대해 정리했습니다.

참고 강의 : 김영한의 스프링 핵심 원리 - 고급편

 

JDK 동적 프록시와 CGLIB를 동시에 사용하는 경우에 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MehtodIntercetpor를 둘다 만들어 관리해야 하는 문제가 생깁니다.

 

한번에 관리해줄 수 있는 방법이 없을까요?

 

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

 

 

프록시 팩터리

Spring에서 제공하는 동적 프록시를 편리하게 만들어주는 기능입니다.

프록시 팩터리는 인터페이스가 있는 경우엔 JDK 동적 프록시를  사용하고, 구체 클래스만 있는 경우엔 CGLIB를 사용합니다.

또한, 이러한 설정을 변경할 수 도 있습니다.

 

프록시 팩터리 의존관계

프록시 팩터리 의존관계를 그림으로 나타내면 다음과 같습니다.

프록시 팩토리가 사용되는 흐름을 그림으로 나타내면 다음과 같습니다.


Spring에서는 JDK 프록시와 CGLIB 프록시를 함께 사용하는 경우 InvocationHandler와 MethodInterceptor를 각각 구현해주어야 하는 번거로움을 해결하기 위해서 Advice라는 개념을 적용했습니다.

InvocationHandler와 MethodInterceptor를 둘 다 구현하지 않고, Advice하나만 만들어주면 됩니다.

InvocationHandler와 MethodIntercetpor가 Advice를 호출하는 흐름으로 흘러갑니다.

 

Advice

프록시에 적용하는 부가 기능 로직입니다. JDK 동적 프록시가 제공하는 InvocationHandler와 GCLIB가 제공하는 MethodInterceptor의 개념을 추상화해서 만든 것입니다.

 

Advice가 도입된 후의 흐름을 나타내는 그림은 다음과 같습니다.

 

전체 흐름을 나타내는 그림은 다음과 같습니다.

 

프록시 팩토리 적용

Advice를 적용하기 위한 방법은 많이 존재하지만, 기본적으로 아래와 같은 인터페이스를 구현합니다.

package org.aopalliance.intercept;
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}

 

MethodInvocation invocation : 다음 메서드를 호출 하는 방법, 현재 프록시 객체 인스턴스, args, 메서드 정보 등이 포함되어 있습니다.

기존 파라미터로 제공되는 부분들이 안으로 모두 들어가있습니다.

MethodInterceptor는 Interceptor를 상속하고 Interceptor는 Advice 인터페이스를 상속합니다.

 

테스트 코드를 통해서 정말 잘 동작하는지 알아보겠습니다.

먼저 Adivce를 만들어줍니다.

 

TimeAdvice

@Slf4j
public class TimeAdvice implements MethodInterceptor {

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

 

TimeAdvice는 MethodIntercetpor를 구현합니다.

invocation.proceed()를 호출하면 target 클래스를 호출하고 결과를 받습니다. target 클래스의 정보는 MethodInvocation invocation안에 모두 포함되어 있습니다.

포함되어 있는 이유는 프록시 팩토리로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받기 때문입니다.

 

먼저, ProxyBean에 Interface를 제공한 경우, JDK 동적 프록시 객체를 생성하는지 확인해보겠습니다.

 

ProxyFactoryTest

@Slf4j
public class ProxyFactoryTest {

    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() throws Exception {

        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();
    }
}

 

new ProxyFactory(target) - 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨 줍니다.

프록시는 이 인스턴스에 인터페이스가 있는 경우 JDK 동적 프록시를 기본으로 사용하고, 구체 클래스만 있는 경우 CGLIB를 통해서 동적 프록시를 생성합니다. 

target는 new ServiceImpl()의 인터페이스 이므로, 인터페이스를 기반으로 JDK 동적 프록시를 생성하게 됩니다.

 

proxyFactory.addAdvice(new TimeAdvice()) - 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정합니다.

JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodIntercetpor의 개념과 유사합니다.

이렇게 프록시가 제공하는 부가 기능 로직을 Advice라고 합니다. 

 

proxyFactory.getProxy() - 프록시 객체를 생성한 후, 그 결과를 받습니다.

 

 

출력 결과

 

생성된 프록시 객체의 정보를 보면, $Proxy13으로 나옵니다. 이는 JDK 동적 프록시로 인해서 생성된 프록시 객체입니다.

 

 

다음으로, ProxyBean에 구체 Class를 제공한 경우, CGLIB 프록시 객체를 생성하는지 확인해보겠습니다.

@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();
}

 

 

출력 결과

 

생성된 프록시 객체의 정보를 보면, $$EnhancerBySpringCGLIB$$7d3a331d로 나옵니다. 이는 CGLIB를 사용해서 프록시를 적용됨을 의미합니다.

 

 

proxyTargetClass라는 옵션을 사용하는 경우, 옵션 값에 true를 넣게 되면 인터페이스가 있음에도 클래스 기반 동적 프록시를 만들게 됩니다. 즉, 인터페이스가 있음에도 불구하고 JDK 동적 프록시를 사용하는 것이 아닌, CGLIB를 사용하게 됩니다.

@Test
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
void proxyTargetClass() {
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(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();
}

 

setProxyTargetClass(true)를 해주는 경우 인터페이스가 있어도 CGLIB방식을 사용합니다.

 

출력 결과

 

출력 결과에 보이는 프록시 객체의 정보를 보면 $$EnhanceBySpringCGLIB$$672f86aa 라는 이름이 보이고,

이는 CGLIB 방식으로 프록시 객체가 생성됨을 의미합니다. 즉, 인터페이스를 ProxyFactory에 제공했음에도 불구하고 CGLIB가 사용됨을 알 수 있습니다.

 

 

정리

  • 프록시 팩터리의 서비스 추상화로 인해 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 간편하게 동적 프록시를 생성할 수 있습니다.
  • Advice 하나로 편리하게 프록시의 부가 기능 로직을 사용할 수 있습니다. 이는 프록시 팩토리에서 내부적으로 JDK 동적 프록시인 경우엔 InvocationHandler가 Advice를 호출하도록 구현해두고 CGLIB인 경우엔 MethodInterceptor가 Advice를 호출하도록 구현해두었기 때문에 가능합니다.

'Spring' 카테고리의 다른 글

빈 후처리  (0) 2025.03.13
어드바이저  (0) 2025.03.12
CGLIB  (0) 2025.03.10
동적 프록시  (0) 2025.03.08
트랜잭션 AOP 주의사항  (0) 2025.03.01