본문 바로가기

Java and Spring

[Java] 동적 프록시(Dynamic Proxy)

이번 글에서는 동적 프록시(Dynamic Proxy)가 왜 필요하고 어떻게 사용되는지 알아보려 합니다.

 

동적 프록시(Dynamic Proxy)와 프록시(Proxy)

동적 프록시가 왜 필요한지 알기위해서는 그전에 프록시가 사용되는 이유에대해 먼저 알아야 합니다.

프록시는 타겟 코드의 수정없이 접근제어 혹은 부가기능을 추가하기 위해 주로 사용됩니다.

하지만, 프록시를 사용하기 위해서는 대상 클래스 수만큼의 프록시클래스를 하나하나 만들어줘야하고 그 안에 들어가는 반복되는 코드때문에 코드중복이라는 단점이 있습니다.

이러한 단점들을 보완하여 컴파일 시점이아닌, 런타임 시점에 프록시 클래스를 만들어주는 방식이 동적 프록시입니다.

그럼 동적 프록시는 어떻게 만들까요??

 

newProxyInstance()

Java에서 제공해주는 reflection API의 newProxyInstance() 메서드를 사용하면, 런타임 시점에 프록시 클래스를 만들어 주기 때문에 대상 클래스 수만큼 프록시 클래스를 만들어야하는 첫번째 단점을 해결해 줍니다.

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)

newProxyInstance() 메서드를 구성하는 인자를 살펴보면,

  • ClassLoader : 프록시 클래스를 만들 클래스로더
  • Class : 프록시 클래스가 구현할 인터페이스 목록(배열)
  • InvocationHandler : 메서드가 호출되었을때 실행될 핸들러

3가지 인자로 구성되어 있는걸 알 수있습니다.

다음으로는 중복코드의 단점을 해결해주는 InvocationHandler에 대해 알아보도록 하겠습니다.

 

InvocationHandler

InvocationHandler는 invoke() 메서드만 가지고 있는 인터페이스 입니다.

invoke() 메서드는 런타임 시점에 생긴 동적 프록시의 메서드가 호출되었을때, 실행되는 메서드이고, 어떤 메서드가 실행되었는지 메서드 정보와 메서드에 전달된 인자까지 invoke()메서드의 인자로 들어오게 됩니다.

또한, invoke() 메서드에 프록시만 사용할 당시, 프록시 클래스마다 들어간 반복된 코드를 한번만 작성함으로써 두번째 단점을 해결해 줍니다.

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

newProxyInstance() 메서드를 구성하는 인자를 살펴보면,

  • Object : 프록시 객체
  • Method : 호출한 메서드 정보
  • Object[] : 메서드에 전달된 파라미터

3가지 인자로 되어있는 것을 볼 수 있습니다.

 

예제코드

클래스가 늘어남에따라 일일히 프록시 클래스를 만들지 않고, 동적 프록시를 활용한 예제코드를 보도록 하겠습니다.

@Test
void dynamicA() {
    AInterface target = new AImpl();
    TimeInvocationHandler handler = new TimeInvocationHandler(target);

    AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(),
                                                           new Class[]{AInterface.class}, 
                                                           handler);

    proxy.call();
}

@Test
void dynamicB() {
    BInterface target = new BImpl();
    TimeInvocationHandler handler = new TimeInvocationHandler(target);

    BInterface proxy = (BInterface) Proxy.newProxyInstance(BInterface.class.getClassLoader(), 
                                                           new Class[]{BInterface.class}, 
                                                           handler);

    proxy.call();
}
public interface AInterface {
    String call();
}

@Slf4j
public class AImpl implements AInterface{

    @Override
    public String call() {
        log.info("A 호출");
        return "a";
    }
}

------------------------------------------------------
public interface BInterface {
    String call();
}

@Slf4j
public class BImpl implements BInterface {

    @Override
    public String call() {
        log.info("B 호출");
        return "b";
    }
}

------------------------------------------------------
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
    private final Object target;

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

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

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

        return result;
    }
}

dynamicA 테스트 코드의 실행 순서는 다음과 같다.

  1. 동적 프록시로 만들어진 proxy.call()을 실행한다.
  2. invoke() 메서드가 호출되는데, 3번째 인자인 handler의 구현체가 TimeInvocationHandler로 되어있음으로 TimeInvocationHandler의 invoke() 메서드가 실행된다.
  3. invoke() 메서드 안에 작성한 공통로직이 실행되고, method.invoke()메서드를 통해 타겟의 실제 객체인 AImpl의 call() 메서드가 실행된다.
  4. AImpl의 실행이 종료되고 값이 반환된다.

클래스 의존관계

런타임 의존관계

dynamicA와 dynamicB 테스트 코드를 실행하면 아래와 같은 결과가 나오게 됩니다.

 

 

즉, 동적 프록시를 사용하면 프록시를 사용할때처럼 클래스마다 프록시 클래스를 만들지 않고, 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용하여 프록시 객체를 런타임 시점에만 생성해주면 됩니다.

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

[Spring] 빈 후처리기  (0) 2021.12.11
[Spring] 프록시 팩토리  (0) 2021.12.08
[Java] CGLIB(Code Generator Library)  (0) 2021.12.04
[Java] ThreadLocal 이해 및 활용  (0) 2021.11.13
[Spring] Lombok 사용법  (0) 2021.11.07