Dynamic Proxy of Java

KyeongHoon Lee·2024년 1월 15일

Dynamic Proxy

Mybatis와 Proxy

Mybatis의 Mapper

신입사원 시절 회사에서 사용하기 위해 Mybatis를 공부하다가 문득 의문점이 생겼습니다.

@Mapper Annotaion을 붙인 Interface의 Instance는 어떻게 생기는걸까?

Mapper Interface를 출력하는 테스트코드를 작성해보니 다음과 같이 출력되었습니다.

org.apache.ibatis.binding.MapperProxy@6958d5d0

MapperProxy

출력된 결과인 MapperProxy를 찾아 소스를 열어보니 다음과 같이 invoke 메서드가 있었습니다.

public class MapperProxy<T> implements InvocationHandler, Serializable {

// --- 중략 ---

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }
    
// --- 중략 ---

}

MapperProxy는 완전히 Dynamic Proxy로만 만들어지진 않았지만,
InvocationHandler, invokeDynamic Proxy에서 사용되는 용어들이 보입니다.

참고) Proxy Pattern

https://en.wikipedia.org/wiki/Proxy_pattern

Dynamic Proxy

Dynamic Proxy란?

Dynamic Proxy는 지정된 하나 이상의 인터페이스를 구현하는 클래스를 런타임에 생성할 수 있게 해주는 클래스입니다.

Dynamic Proxy 만들기

Dynamic Proxyjava.lang.reflect.Proxy클래스의 newProxyInstance Method를 사용해서 만들 수 있습니다.

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

newProxyInstance 메서드의 Parameter

ParameterDescription
ClassLoader loaderProxy 클래스를 정의할 클래스 로더
Class<?>[] interfacesProxy 클래스가 구현할 인터페이스들
InvocationHandler hMethod호출을 전달하기 위한 Handler

예시) SpeedGate

예시로 직원이 드나들 수 있는 SpeedGate를 만들어 보겠습니다.

SpeedGate Interface

직원이 들어가고 나가는 SpeedGate Interface를 다음과 같이 만들었습니다.

public interface SpeedGate {
    String in(User user);	// 직원이 들어간다.
    String out(User user); // 직원이 나간다.
}

DefaultSpeedGate 만들기

단순히 직원이 들어가고 나가는 DefaultSpeedGate를 만들었습니다.

public class DefaultSpeedGate implements SpeedGate{
    @Override
    public String in(User user) {
        return user.getName() + "(이)가 들어왔습니다.";
    }

    @Override
    public String out(User user) {
        return user.getName() + "(이)가 나갔습니다.";
    }

    public static void main(String[] args) {
        SpeedGate defaultSpeedGate =  new DefaultSpeedGate();
        User lkh = new User("이경훈");
        System.out.println(defaultSpeedGate.in(lkh));
        System.out.println(defaultSpeedGate.out(lkh));
    }
}
/* 
main method 실행 결과: 
이경훈(이)가 들어왔습니다.
이경훈(이)가 나갔습니다.
*/

시간을 함께 출력하는 TimeLoggingSpeedGate 만들기

Dynamic Proxy를 활용해 들어오고 나갈 때, 시간을 함께 출력하도록 변경해보겠습니다.

public class TimeLoggingSpeedGate implements InvocationHandler {

    private Object defaultSpeedGate;

    private TimeLoggingSpeedGate(Object defaultSpeedGate) {
        this.defaultSpeedGate = defaultSpeedGate;
    }

    public static Object newInstance(Object defaultSpeedGate){
        return Proxy.newProxyInstance(
            defaultSpeedGate.getClass().getClassLoader(),
            defaultSpeedGate.getClass().getInterfaces(),
            new TimeLoggingSpeedGate(defaultSpeedGate)
        );
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        LocalDateTime now = LocalDateTime.now();
        Object invoke = method.invoke(defaultSpeedGate, args);
        return now + " - " + invoke;
    }

    public static void main(String[] args) {
        SpeedGate defaultSpeedGate = new DefaultSpeedGate();
        SpeedGate timeLoggingSpeedGate = (SpeedGate) TimeLoggingSpeedGate.newInstance(defaultSpeedGate);

        User lkh = new User("이경훈");
        System.out.println(timeLoggingSpeedGate.in(lkh));
        System.out.println(timeLoggingSpeedGate.out(lkh));
    }
}
/*
main method 실행 결과: 
2024-01-15T21:43:12.776076 - 이경훈(이)가 들어왔습니다.
2024-01-15T21:43:12.783697 - 이경훈(이)가 나갔습니다.
*/

Reflection으로 들어올 때만 시간을 출력하는 SpeedGate 만들기

매개변수로 받는 Method를 활용해 들어올 때만 시간을 체크하는 SpeedGate를 만들어보겠습니다.

public class InTimeLoggingSpeedGate implements InvocationHandler {

    private Object defaultSpeedGate;

    private InTimeLoggingSpeedGate(Object defaultSpeedGate) {
        this.defaultSpeedGate = defaultSpeedGate;
    }

    public static Object newInstance(Object defaultSpeedGate){
        return Proxy.newProxyInstance(
                defaultSpeedGate.getClass().getClassLoader(),
                defaultSpeedGate.getClass().getInterfaces(),
                new InTimeLoggingSpeedGate(defaultSpeedGate)
        );
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if("in".equals(method.getName())) {
            LocalDateTime now = LocalDateTime.now();
            Object invoke = method.invoke(defaultSpeedGate, args);
            return now + " - " + invoke;
        } else{
            return method.invoke(defaultSpeedGate, args);
        }
    }
    
    public static void main(String[] args) {
        SpeedGate defaultSpeedGate = new DefaultSpeedGate();
        SpeedGate timeLoggingSpeedGate = (SpeedGate) InTimeLoggingSpeedGate.newInstance(defaultSpeedGate);

        User lkh = new User("이경훈");
        System.out.println(timeLoggingSpeedGate.in(lkh));
        System.out.println(timeLoggingSpeedGate.out(lkh));
    }
}
/*
main method 실행 결과: 
2024-01-15T21:48:15.586882 - 이경훈(이)가 들어왔습니다.
이경훈(이)가 나갔습니다.
*/

Proxy 생성 시 주의사항

  • Class<?>[] interfaces 에 null 이 들어가면 NullPointerException이 발생합니다.
    public static Object newInstance(Object defaultSpeedGate){
        return Proxy.newProxyInstance(
                defaultSpeedGate.getClass().getClassLoader(),
                new Class[]{SpeedGate.class, null},
                new InTimeLoggingSpeedGate(defaultSpeedGate)
        );
    }
// NullPointerException 발생!
  • Class<?>[] interfaces 에 같은 class가 중복되면 IllegalArgumentException이 발생합니다.
    public static Object newInstance(Object defaultSpeedGate){
        return Proxy.newProxyInstance(
                defaultSpeedGate.getClass().getClassLoader(),
                new Class[]{SpeedGate.class, SpeedGate.class},
                new InTimeLoggingSpeedGate(defaultSpeedGate)
        );
    }
// IllegalArgumentException 발생!
  • Class<?>[] interfaces 에 Interface가 아닌 class나 primitive type이 들어가면 IllegalArgumentException이 발생합니다.
    public static Object newInstance(Object defaultSpeedGate){
        return Proxy.newProxyInstance(
                defaultSpeedGate.getClass().getClassLoader(),
                new Class[]{DefaultSpeedGate.class},
                new InTimeLoggingSpeedGate(defaultSpeedGate)
        );
    }
// IllegalArgumentException 발생!

참고

Oracle - Proxy documentation

profile
Back-end Engineer입니다.

0개의 댓글