[java] 리플렉션(Reflection) 3 : 프록시

반응형

프록시

프록시란 인터페이스 구현 클래스 및 그 인터페이스를 실행할 때 작성하는 기술 또는 그 작성된 인터페이스를 말한다.

 

JDK 동적 프록시

프록시를 적용하기 위해 적용 대상의 숫자만큼 프록시 클래스를 만드는 것은 매우 힘든 일이다. 이 문제를 해결하는 것이 동적 프록시 기술이다. 동적 프록시로 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 프록시 객체를 동적으로 런타임에 개발자를 대신하여 만들어준다. 또 동적 프록시에 원하는 실행 로직을 지정할 수 있다.

 

예제코드

1. 인터페이스와 그 인터페이스를 구현하는 구현체 작성 : CommandA, CommandAImpl, CommandB, CommandBImpl

// CommandA 인터페이스와 그 구현체
public interface CommandA {
    int execute(String command);
}

public class CommandAImpl implements CommandA {
    @Override
    public int execute(String command) {
        if ("NORTH".equals(command)) {
            return 0;
        } else if ("SOUTH".equals(command)) {
            return 1;
        } else {
            throw new IllegalArgumentException("Arg must be 'NORTH' or 'SOUTH'");
        }
    }
}

// CommandB 인터페이스와 그 구현체
public interface CommandB {
    int execute(String command);
}

public class CommandBImpl implements CommandB {
    @Override
    public int execute(String command) {
        if ("EAST".equals(command)) {
            return 2;
        } else if ("WEST".equals(command)) {
            return 3;
        } else {
            throw new IllegalArgumentException("Arg must be 'EAST' or 'WEST'");
        }
    }
}

2. JDK 동적 프록시에 적용할 로직 : CommandTimeInvocationHandler

JDK 동적 프록시가 제공하는 InvocationHandler 인터페이스를 구현해서 작성한다.

@Slf4j
public class CommandTimeInvocationHandler implements InvocationHandler {

    // 동적 프록시가 호출할 대상
    private final Object target;

    // 생성자
    public CommandTimeInvocationHandler(Object target) {
        this.target = target;
    }

    /**
     * JDK 동적 프록시가 제공하는 InvocationHandler
     * @param proxy 프록시 자신
     * @param method 호출한 메소드
     * @param args 메소드를 호출할 때 전달한 인수
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        // 리플렉션을 사용해서 target 인스턴스의 메소드를 실행, args 는 메소드 호출시 넘겨줄 인수
        Object result = method.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;

        log.info("TimeProxy 종료 resultTime = {}", resultTime);
        return result;
    }
}

3. JDK 동적 프록시 사용 (테스트 코드)

@Slf4j
public class ProxyReflectionTest {

    @Test
    void commandATest() {
        CommandA target = new CommandAImpl();
        // 동적 프록시에 적용할 핸들러 로직
        CommandTimeInvocationHandler handler = new CommandTimeInvocationHandler(target);
        // java.lang.reflect.Proxy 로 동적 프록시 생성
        // 인수 : 클래스 로더 정보, 인터페이스, 핸들러 로직
        CommandA proxy = (CommandA) Proxy.newProxyInstance(
                CommandA.class.getClassLoader(), new Class[]{CommandA.class}, handler);
        int result = proxy.execute("SOUTH");
        log.info("proxy.execute() result = {}", result);
        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());
    }

    @Test
    void commandBTest() {
        CommandB target = new CommandBImpl();
        CommandTimeInvocationHandler handler = new CommandTimeInvocationHandler(target);
        CommandB proxy = (CommandB) Proxy.newProxyInstance(
                CommandB.class.getClassLoader(), new Class[]{CommandB.class}, handler);
        int result = proxy.execute("WEST");
        log.info("proxy.execute() result = {}", result);
        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());
    }
}

4. 출력결과

commandATest() 결과

CommandTimeInvocationHandler - TimeProxy 실행
CommandTimeInvocationHandler - TimeProxy 종료 resultTime = 0
ProxyReflectionTest - proxy.execute() result = 1
ProxyReflectionTest - targetClass = class hello.proxy.reflection.jdkproxy.CommandAImpl
ProxyReflectionTest - proxyClass = class com.sun.proxy.$Proxy12

commandBTest() 결과

CommandTimeInvocationHandler - TimeProxy 실행
CommandTimeInvocationHandler - TimeProxy 종료 resultTime = 1
ProxyReflectionTest - proxy.execute() result = 3
ProxyReflectionTest - targetClass = class hello.proxy.reflection.jdkproxy.CommandBImpl
ProxyReflectionTest - proxyClass = class com.sun.proxy.$Proxy13

 

결과 로그를 확인하면 동적으로 프록시가 각각 생성된 것을 알 수 있다.

 

proxyClass = class com.sun.proxy.$Proxy12   // commandATest() 결과

proxyClass = class com.sun.proxy.$Proxy13   // commandBTest() 결과

 

이렇게 생성된 프록시는 CommandTimeInvocationHandler 로직을 실행한다. 즉, 프록시를 직접 작성하지 않고 JDK 가 동적으로 생성해주며, CommandTimeInvocationHandler 는 공통으로 사용한다.

 

 

실행 순서

  1. 클라이언트는 JDK 동적 프록시의 execute() 를 실행한다.
  2. JDK 동적 프록시는 InvocationHandler.invoke() 를 호출하고 구현체인 CommandTimeInvocationHandler.invoke() 가 호출된다.
  3. CommandTimeInvocationHandler 에 작성된 내부 로직을 수행하고, method.invoke(target, args) 를 호출해서 target 인 실제 객체 CommandAImpl 을 호출한다.
  4. CommandAImpl 인스턴스의 execute() 이 실행되고 실행이 끝나면 CommandTimeInvocationHandler 로 돌아와 실행시간 로그를 출력하고 그 결과를 반환한다.

 

JDK 동적 프록시 기술 덕분에 프록시 적용 대상 수 만큼 프록시를 직접 작성하지 않아도 된다. 또한 같은 부가기능(예제에서 실행시간 측정) 로직을 하나만 작성하여 공통으로 적용할 수 있다. 동적 프록시를 통해서 생성하고, 각각 필요한 InvocationHandler 를 구현해주면 되는 것이다.

 

 

JDK 동적 프록시의 한계

JDK 동적 프록시는 인터페이스가 필수이다.

인터페이스 없이 구현클래스만 존재하는 경우, 동적 프록시를 적용하려면 CGLIB 라는 바이트 코드를 조작하는 라이브러리를 사용해야 한다.

 

반응형