본문 바로가기

Java and Spring

[Spring] 프록시 팩토리

이번 글에서는 프록시 팩토리에대해서 알아보려고 합니다.

 

프록시 팩토리란?

동적으로 프록시를 만들어주는 기술은 크게 2가지로 JDK 동적 프록시 CGLIB이 있습니다.

인터페이스가 존재하는 경우라면 JDK 동적 프록시가 제공하는 InvocationHandler를 사용하여 부가 기능을 제공하고, 인터페이스가 존재하지 않는다면 CGLIB이 제공하는 MethodInterceptor를 사용하여 부가 기능을 제공하게 됩니다.

 

그렇다면, 인터페이스가 있는 경우와 없는 경우가 공존하게 된다면, 우리 개발자들은 2가지 형태 즉, InvocationHandler와 MethodInterceptor을 중복으로 만들어서 관리하는 불편함을 경험하게 됩니다.

이러한 불편함을 제공하지 않기 위해 스프링은 일관된 방법으로 사용자가 편리하게 사용할 수 있는 추상화 된 기술을 제공합니다. 그러한 기술이 프록시 팩토리입니다.

 

즉, 프록시 팩토리를 사용하면 JDK 동적 프록시와 CGLIB으로 나누어 관리하지 않고, 프록시 팩토리를 통해 일관된 방법으로 프록시를 만들 수 있습니다.

 

프록시 팩토리 기능1 - 프록시 반환(JDK proxy or CGLIB)

프록시 팩토리의 프록시 반환 방식은 아래와 같습니다.

  1. 클라이언트가 프록시 팩토리에 프록시 요청을 합니다.
  2. 프록시 팩토리는 해당 타겟의 인터페이스 유무를 확인하고 프록시 기술을 선택합니다.
  3. 인터페이스가 있으면 JDK 동적 프록시를 반환하고, 없으면 CGLIB 프록시를 반환하게 됩니다.

 

프록시 팩토리 기능2 - 부가기능 처리(Advice)

프록시 팩토리를 사용하면 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB이 제공하는 MethodInterceptor을 신경쓰지 않고, Advice에만 부가기능 로직을 만들면 됩니다.

왜냐하면, 프록시 팩토리를 통해 반환된 프록시는 최종적으로 Advice를 호출하기 때문입니다.

  1. 클라이언트의 요청시, 프록시 팩토리는 인터페이스의 유무에 따라 JDK 프록시 혹은 CGLIB 프록시를 반환합니다.
  2. JDK 프록시에서 제공하는 InvocationHandler와 CGLIB에서 제공하는 MethodInterceptor는 최종적으로 Advice를 호출하게 됩니다.

그럼 지금부터 부가기능을 처리하는 Advice에 대해 좀 더 알아보려고 합니다.

Advice를 구현하기 위해서는 MethodInterceptor를 구현하면 되는데, 이는 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB이 제공하는 MethodInterceptor를 개념적으로 추상화 한 것입니다.

 

여기서 잠깐!!

Advice를 구현한다고 하였는데 아래코드의 TimeAdvice는 왜 MethodInterceptor를 구현하고 있을까요??

그건 바로 MethodInterceptor는 Interceptor 인터페이스를 구현하고 있고 Interceptor인터페이스는 Advice 인터페이스를 구현하고 있기 때문입니다.

@Slf4j
public class TimeAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        ~~ 부가기능 로직구현 ~~
    }
}

public interface MethodInterceptor extends Interceptor {
	Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}

public interface Interceptor extends Advice {}

 

예제코드

이제 프록시 팩토리 기능1,2를 볼 수 있는 예제코드를 만들어 보겠습니다.

[타겟 코드]

public interface ServiceInterface {
    void save();
    void find();
}

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

@Slf4j
public class ConcreteService {
    public void call() {
        log.info("ConcreteService 호출");
    }
}

[Advice 코드]

@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={}", resultTime);

        return result;
    }
}

[테스트 코드]

@Test
@DisplayName("프록시팩토리 예제코드")
void proxyFactory() {
    ServiceInterface target1 = new ServiceImpl();
    ConcreteService target2 = new ConcreteService();

    ProxyFactory proxyFactory1 = new ProxyFactory(target1);
    ProxyFactory proxyFactory2 = new ProxyFactory(target2);

    proxyFactory1.addAdvice(new TimeAdvice());
    proxyFactory2.addAdvice(new TimeAdvice());

    ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
    ConcreteService proxy2 = (ConcreteService) proxyFactory2.getProxy();
    
    // 프록시 팩토리 기능1 확인
    log.info("proxyClass={}", proxy1.getClass());
    log.info("proxyClass={}", proxy2.getClass());
    
    // 프록시 팩토리 기능2 확인
    proxy1.find();
    proxy2.call();
}

먼저 테스트 코드에서 프록시 팩토리의 사용방법을 알아 보려고 합니다.

  • new ProxyFactory(target) : 프록시 팩토리를 생성할 때, 인자값으로 프록시로 생성할 대상을 넘겨 주게 됩니다. 프록시 팩토리는 넘어온 target을 바탕으로 프록시를 생성하게 되는데, target이 인터페이스가 있으면 JDK 동적 프록시를 반환하고 인터페이스가 없으면 CGLIB 프록시를 반환하게 됩니다.
  • proxyFactory1.addAdvice(new TimeAdvice()) : 만들어진 프록시가 사용하게 될 부가기능(Advice)를 설정합니다.
  • proxyFactory1.getProxy() : 프록시 객체를 생성합니다.

코드를 실행해보면 아래와 같은 결과가 나온다는 것을 볼 수 있습니다.

즉, 개발자는 스프링이 제공하는 프록시 팩토리를 사용함으로써 별도 코드의 추가없이 자동으로 프록시 타입을 반환받고, 부가기능 로직을 Advice에 구현함으로써 원하는 위치에 적용 할 수 있게 되었습니다.

'Java and Spring' 카테고리의 다른 글

[Spring] 스프링 AOP  (0) 2021.12.15
[Spring] 빈 후처리기  (0) 2021.12.11
[Java] CGLIB(Code Generator Library)  (0) 2021.12.04
[Java] 동적 프록시(Dynamic Proxy)  (0) 2021.12.01
[Java] ThreadLocal 이해 및 활용  (0) 2021.11.13