Spring 핵심원리 고급편 - Dynamic Proxy 1

목차

JDK 동적 프록시

JDK 동적 프록시인터페이스 를 기반으로 프록시 를 동적으로 만들어준다.
JDK 동적 프록시는 자바 리플렉션(Reflection) 을 이용하여 런타임 시에 인터페이스를 구현하는 프록시 객체 를 생성하는 기술

동적 프록시 기술을 사용하면 개발자가 직접 Proxy 클래스를 생성할 필요가 없이 런타임시 리플랙션의 Proxy 클래스가 동적으로 생성해준다는 장점이 있다. 하지만, 문제가 발생하면 런타임 에러 가 발생하므로 컴파일 시 오류를 찾기가 어려운 문제점 또한 존재한다.

  • 동적 프록시 기술을 사용하면 런타임시 프록시 객체 를 생성해준다.
  • 리플렉션 을 이용해 프록시를 생성한다.
  • 타겟 인터페이스와 동일한 형태로 생성
  • FactoryBean 을 통해서 생성

프록시 객체는 원본 객체의 대리자 역할을 하며, 클라이언트가 프록시 객체를 통해 원본 객체에 접근하면, 프록시 객체가 요청을 가로채서 필요한 전처리나 후처리를 수행한 후, 최종 결과를 반환합니다.

동적 프록시는 프로그래머가 직접 코드를 작성하지 않아도 인터페이스의 메서드 호출을 가로채서 처리할 수 있어서 매우 유용합니다. 예를 들어, AOP(Aspect Oriented Programming)에서는 동적 프록시를 이용해서 메서드 호출 전후에 로그를 남기거나, 보안 검사를 수행하거나, 트랜잭션 관리를 수행할 수 있습니다.

JDK에서는 java.lang.reflect 패키지에서 Proxy 클래스 를 제공합니다. 이 클래스의 정적 메서드인 newProxyInstance() 메서드를 이용하면, 인터페이스와 InvocationHandler 인터페이스를 구현한 클래스를 전달하여 동적 프록시 객체를 생성할 수 있습니다. 이때, InvocationHandler 인터페이스를 구현한 클래스에서는 invoke() 메서드를 구현하여 원본 객체의 메서드 호출을 가로채서 필요한 작업을 수행하도록 구현합니다.

newProxyInstance

Proxy 클래스 내 정적 메소드로 newProxyInstance 를 이용해 런타임 시 동적으로 프록시 객체 를 생성 후 반환한다.

전달 받은 인터페이스 를 기반으로 Proxy 클래스를 생성한 후 JVM 에 올리고 Proxy 객체를 반환한다.

newProxyInstance 메소드는 세 개의 인자를 받습니다. 첫 번째 인자는 동적 프록시 객체를 생성할 클래스 로더(ClassLoader) 입니다. 두 번째 인자는 동적 프록시 객체가 구현해야 할 인터페이스 목록 입니다. 세 번째 인자는 InvocationHandler 인터페이스를 구현한 객체 입니다.

newProxyInstance 메소드는 프록시 객체를 생성하여 반환합니다. 이 프록시 객체는 인터페이스 목록에 선언된 모든 메소드를 구현하며, 실제 호출 시 InvocationHandler에서 정의한 로직이 수행됩니다.

  • ClassLoader
    • Proxy 클래스를 JVM 으로 로드할 클래스 로더, null 을 전달할 시 기본 클래스 로더 사용
  • Class<?>
    • 프록시 클래스가 구현할 인터페이스 목록(배열)
  • InvocationHandler
    • 메서드가 호출되었을때 실행될 Handler
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

InvocationHandler

InvocationHandler 를 이용해 JDK 동적 프로시에 적용할 공통 로직 을 개발한다.

InvocationHandler 인터페이스는 invoke 메소드 하나만을 가지고 있으며, 이 메소드는 동적 프록시 객체가 처리해야 할 모든 메소드 호출을 가로채서 처리할 수 있습니다.

invoke 메소드는 세 개의 인자를 받습니다. 첫 번째 인자는 동적 프록시 객체 자체이며, 두 번째 인자는 원본 객체의 메소드입니다. 세 번째 인자는 원본 객체의 메소드 호출 시 전달된 인자 배열입니다.

InvocationHandler를 구현한 객체에서는 invoke 메소드를 구현하여 프록시 객체가 요청을 가로챌 때 처리해야 할 로직을 구현합니다. 예를 들어, 메소드 호출 전후에 로그를 남기거나, 메소드 호출 시간을 측정하거나, 트랜잭션을 관리하는 등의 작업을 수행할 수 있습니다.

InvocationHandler.java

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

Dynamic Proxy 생성

InvocationHandler 인터페이스를 이용해 프록시 객체 생성시 추가할 로직을 정의 한다.

TimeInvocationHandler 클래스는 Target 클래스를 호출할 때 시작과 종료 로그를 찍어주는 공통 로직을 가지고 있다.

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

// Reflection 을 이용해 target 인스턴스의 메서드를 실행한다. args 는 메서드 호출시 넘겨줄 인자
Object result = method.invoke(target, args);

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

return result;
}
}

newProxyInstance 를 이용해 동적으로 Proxy 객체를 만들어준다.

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

newProxyInstance 를 이용해 동적으로 Proxy 객체를 생성

newProxyInstance 메소드를 이용해 실제 객체와 동일한 인터페이스부가로직 이 추가된 프록시 객체를 생성한다.

newProxyInstance 메소드는 AInterface 인터페이스를 이용해 시작과 종료 로그 를 찍어주는 프록시 객체를 생성한 후 반환한다. 반환된 Proxy 객체는 AInterface 인터페이스를 구현한 객체로 실제 객체와 동일한 메소드를 이용해 호출했을때 시작, 종료 로그가 찍히는 것을 확인할 수 있다.

@Test
void dynamicA(){
AInterface target = new AImpl();
// 동적 Proxy 에 적용할 Handler 로직
TimeInvocationHandler handler = new TimeInvocationHandler(target);

// Proxy 객체 생성
AInterface proxy = (AInterface) Proxy
// 현재 Thread 의 Context Loader, Interface 배열, InvocationHandler 객체를 인자로 넣어준다.
.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);

// Proxy 객체 호출
// Client 에서는 동일한 메소드를 사용하면 된다.
proxy.call();

log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
}

프록시 객체 호출 결과

프록시 객체를 호출했을 때 AImpl 객체를 호출하기 전에 시작, 종료 로그가 찍히는 것을 확인할 수 있다.

17:43:18.808 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 실행
17:43:18.811 [Test worker] INFO hello.proxy.jdkdynamic.code.AImpl - A 호출
17:43:18.811 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 종료 resultTime = 0
17:43:18.812 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - targetClass = class hello.proxy.jdkdynamic.code.AImpl
17:43:18.813 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - proxyClass = class com.sun.proxy.$Proxy12

Dynamic Proxy 호출

public interface  BInterface {
String call();
}
@Slf4j
public class BImpl implements BInterface{
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
@Test
void dynamicB(){
BInterface target = new BImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);

// Proxy 객체 생성
BInterface proxy = (BInterface) Proxy
.newProxyInstance(BInterface.class.getClassLoader(), new Class[]{BInterface.class}, handler);

proxy.call();
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
}
17:49:03.329 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 실행
17:49:03.331 [Test worker] INFO hello.proxy.jdkdynamic.code.BImpl - B 호출
17:49:03.331 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 종료 resultTime = 0
17:49:03.333 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - targetClass = class hello.proxy.jdkdynamic.code.BImpl
17:49:03.333 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - proxyClass = class com.sun.proxy.$Proxy12
Share