본문 바로가기

java

[Java] Dynamic Proxy란?

n개의 클래스에 대해 n개의 프록시 클래스를 만들어주어야 할까?
부가적인 기능을 추상화하여 공통 코드로 만들어야 한다.
서로 다른 클래스의 Proxy 클래스 구현을 추상화해야 한다.
마치며

 

이전 글 Proxy란?에서 Proxy를 만들어 구현 코드를 수정하지 않고 부가적인 기능을 추가시켰다. 부가적인 기능을 추가시키고 싶은 클래스의 Proxy 클래스를 직접 만들어 클라이언트가 proxy 클래스를 호출하도록 했다. 이 때 이런 문제가 있었다.

 

'n개의 클래스에 대해 n개의 프록시 클래스를 만들어주어야 할까?'

이 문제를 해결하기 위해 문제를 추상화해보자.

1. 부가적인 기능을 추상화하여 공통 코드로 만들어야 한다.

  (단순 Proxy 클래스 구현에서는 모든 Proxy 기능에 부가 기능 코드를 구현해야 했다)

2. 서로 다른 클래스의 Proxy 클래스 구현을 추상화해야 한다.

  (단순 Proxy 클래스 구현에서는 모든 Proxy 클래스를 구현해야 했다)

 

문제를 푸는 방법은 다양하다. 실제로 스프링에서는 cglib을 이용하여 프록시를 구현하고 있다(java.lang 패키지에서 제공하는 dynamic proxy는 인터페이스에 의존하기 때문에 구체 클래스나 상속이 불가능한 final class, final method는 프록시 생성이 불가능한 단점이 있다). 또는 byte buddy와 같은 라이브러리를 사용하거나 직접 바이트 코드를 조작하여 런타임에 프록시 클래스를 생성해줄 수도 있다.

이번 글에서는 java.lang 패키지 내의 Proxy 클래스를 이용해 동적 프록시를 구현하는 법을 다루고자 한다. 이제 위에서 말한 문제들을 추상화해보자.

 

부가적인 기능을 추상화하여 공통 코드로 만들어야 한다.

추상화 하기 전 A, B 클래스가 있다고 하고 각각 transferA(), transferB() 메서드를 호출한다고 해보자. 다음과 같이 각 클래스에서 각 메서드를 호출한다.

@Test
void 추상화하지않음() {
    new A().transferA();
    new B().transferB();
}

class A {
    void transferA() {
        log.info("call transferA");
    }
}

class B {
    void transferB() {
        log.info("call transferB");
    }
}

 

각 메서드명이 다르기 때문에 추상화를 해보고 싶다. 어떤 인스턴스가 있다면 다음과 같이 같은 메서드를 호출하게 하면 될 것 같다.

@Test
void 추상화() {
    //new A().transferA();
    //new B().transferB();
    new 인스턴스.추상화메서드();
}

리플렉션을 이용해 target class와 호출하고자 하는 method name을 파라미터로 넘기는 getDeclaredMethod()로 만들어 추상화하였다. 이제 getDeclaredMethod()로 메서드 정보를 받아 invoke() 할 수 있도록 공통 메서드가 제공되었다. 공통 로직을 처리하였으니 invoke 메서드를 호출할 때 공통 로직을 같이 추가해주면 될 것 같다.

@Test
void reflectionMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Method transferA = getMethod(A.class, "transferA");
    Method transferB = getMethod(B.class, "transferB");
    transferA.invoke(new A());
    transferB.invoke(new B());
}

private Method getMethod(Class<?> clazz, String methodName) throws NoSuchMethodException {
    return clazz.getDeclaredMethod(methodName);
}

 

이러한 처리를 java에서 편리하게 처리할 수 있도록 java.lang.reflection API가 제공해주는 InvocationHandler 인터페이스가 있다. 해당 인터페이스의 invoke() 메서드를 구현하고, 처리하고자 하는 코드를 실제 호출 대상이 되는 target method 전/후로 구현한다. 테스트를 해보니 invoke 메서드가 정상적으로 호출된다.

@Test
void invoke() throws Throwable {
    InvocationHandler handler = new TransactionInvocationHandler(new A());
    handler.invoke(null, A.class.getDeclaredMethod("transferA"), null);
}

public class TransactionInvocationHandler implements InvocationHandler {

    private final Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        log.info("transaction start");
        Object result = method.invoke(target, args);
        log.info("transaction end");

        return result;
    }
}

 invoke() 라는 공통 메서드를 추출하였으니 이제는 프록시 객체를 추상화하여 해당 proxy가 invoke() 함수를 호출할 수 있게 해야 한다.

 

서로 다른 클래스의 Proxy 클래스 구현을 추상화해야 한다.

추상화 하기 전 TransferServiceProxy 클래스는 다음과 같이 별도로 구현을 해주어야 했다.

public class TransferServiceProxy implements TransferService {

    private final TransferService target;

    public TransferServiceProxy(TransferService target) {
        this.target = target;
    }

    @Override
    public void transfer() {
        log.info("transaction start");
        target.transfer();
        log.info("transaction end");
    }
}

 

transfer() 메서드는 InvocationHandler를 통해 추상화 했으니 이제는 해당 프록시 클래스 생성 로직을 추상화해야 한다. 이것이 Dynamic Proxy이고, java.lang.reflect에서 제공하는 Proxy의 newProxyInstance() 메서드로 구현할 수 있다. proxy 클래스를 정의하기 위한 classLoader, proxy 클래스가 구현하고자 하는 인터페이스 목록, 그리고 우리가 위에서 구현한 InvocationHandler 인스턴스를 파라미터로 전달한다. proxyService를 반환하여 우리가 호출하고자 하는 transfer() 메서드를 호출하게 하면, 실제로 invoke() 메서드가 호출된다.

@Test
void proxyTest() {
    TransferService proxyService = (TransferService) Proxy.newProxyInstance(TransferService.class.getClassLoader(),
            new Class[]{TransferService.class},
            new TransactionInvocationHandler(new TransferServiceImpl()));
    proxyService.transfer();
}

 

위 코드는 TransferService와 TransferServiceImpl에 의존적인 것으로 보인다. 이를 다시 한 번 더 추상화해본다. getTransactionDynamicProxyService() 메서드는 인터페이스 정보와 인터페이스를 구현하는 실제 인스턴스 정보를 파라미터로 받아 proxy 클래스를 반환해준다. proxyTest()의 구현 코드를 보자. 클라이언트는 트랜잭션서비스를 주입받기만 하면, 실제로 proxy 클래스의 invoke() 메서드를 호출할 수 있게 된다.

@Test
void proxyTest() {
    TransferService 사실은프록시 = 트랜잭션서비스주입();
    사실은프록시.transfer();
}

private TransferService 트랜잭션서비스주입() {
    TransferService 이미등록되어있는서비스 = new TransferServiceImpl(); // 이미 알고 있는 구현 클래스와 인터페이스 타입
    return getTransactionDynamicProxyService(TransferService.class, 이미등록되어있는서비스);
}

private <T, U extends T> T getTransactionDynamicProxyService(Class<T> clazz, U target) {
    return (T) Proxy.newProxyInstance(clazz.getClassLoader(),
            new Class[]{ clazz },
            new TransactionInvocationHandler(target));
}

 

마치며

마지막에 트랜잭션서비스를 주입받기만 하면 proxy 클래스의 invoke() 메서드가 호출된다고 하였다. 어떤 프레임워크가 떠오르지 않는가? Spring 프레임워크에서는 bean으로 등록을 해두기만 하면 알아서 의존성을 주입해준다. 클라이언트는 TransferService 타입 인스턴스의 transfer() 메서드를 호출하는 정보만을 알고 있다. 하지만 스프링이 내부적으로 Proxy와 InvocationHandler를 생성하여 클라이언트가 알지도 못하는 사이 해당 proxy 클래스의 invoke() 메서드를 호출하고 있는 것이다.

'도대체 @Transactional은 어떻게 부가 기능이 동작하도록 구현되어 있는데?' 라는 질문에는, 조금 더 많은 프레임워크의 처리가 들어가 있다. 하지만 이러한 처리도 결국에는 동적 Proxy라는 큰 개념으로부터 시작한다. proxy에 대한 이해를 바탕으로 추후에는 세부 로직에 대해서도 학습을 해봐야겠다.

 

[이전 글]

[Java] Proxy란?