스프링/스프링 핵심 원리 - 고급편

36. JDK 동적 프록시

sdafdq 2024. 1. 20. 18:02

동적 프록시,

 

동적으로 프록시를 만들어 주는 것이다. 프록시 객체를 새로 만들어 주는 것 이다.

 

우리가 프록시 적용해야할 것이 100개 클래스면 100개 다 만들어야 했었는데,

 

이거는 이제 자동으로 그 클래스에 따라 만들어 주게끔 하는 것이 가능하다.

 

 

JDK 동적 프록시는 자바에서 공식적으로 지원해 주는 것.

 

근데 이것은 인터페이스가 필수이다.

 

프록시로 쓸 로직은 인터페이스에 넣어놔야 한다.

 

 

 

public interface AInterface {
    String call();
}

이렇게 비즈니스 로직용 인터페이스 만들어 주고

 

@Slf4j
public class AImpl implements AInterface{
    @Override
    public String call() {
        log.info("A 호출");
        return "A";
    }
}

구현

 

 

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

InovocationHandler가 자바에서 제공해주는 JDK 동적 프록시를 만드는 데 필요한 handler이다. 여기에다가 프록시 로직과 비즈니스 로직을 어떻게 할 건지 핸들링 해 준다.

 

invoke, invocation은 직역으로 호출, 메소드 호출과 같은 뜻 같다. 즉 호출 핸들러.

 

여기서 먼저, 

 

@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();
    log.info("targetClass={}", target.getClass());
    log.info("proxyClass={}", proxy.getClass());
}

이렇게 타겟을 구현체를 호출 핸들러에 넣으면서 핸들러를 생성해 주고,

 

자바의 동적 프록시 생성 기술인 newProxyInstance 하면서 프록시 객체를 생성해 준다.

 

인자로는 클래스 로더(클래스 로더란 런타임에 동적으로 클래스를 JVM에 가져오는.),

인터페이스나 클래스들의 배열(여러개를 상속받은 걸 수도 있으니까),

핸들러.

 

이렇게 클래스 로더로 클래스 정보를 메모리로 가져오고, 부모클래스들 정보까지 가져오고, 

핸들러 까지 넣어주면, 이제 그걸 토대로 첫번째 인자를 상속받은 프록시 객체를 만들어 주는거임.(여기서는 AInterface)

 

그리고, 그것의 메소드를 호출하면,

 

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

이 호출 핸들러의 invoke가 호출되는거임.

그런데, 인자로 프록시 객체가, 호출한 메소드의 메타정보가, 호출한 메소드의 인자들이 들어감.

 

즉, 

프록시 객체는 AInterface를 상속받고 그에 따라 AInterface가 가지고 있던 call()메소드를 호출 했을 시,

 

이 프록시는 핸들러에 있는 invoke()를 호출하는데, 거기에 인자로

 

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

이렇게 만든 프록시와,

proxy.call() 해서 call() 메소드의 메타 정보와,

call() 할 때 넣었던 인자들이 들어감. 지금은 call()에 인자가 없어서 null이 들어가긴 할건데,

 

만약 

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

이렇게 되면, 내부적으로

 

invoke(AInterface proxy객체, call()의메타정보, [arg1, arg2])

이렇게 호출 되는 거임.

 

 

 

 

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

여기 보면 method.invoke(타겟, 인자들)

즉 메소드 메타정보.호출() 이란 거임. 저기가 call() 부분.