프록시
프록시란 인터페이스 구현 클래스 및 그 인터페이스를 실행할 때 작성하는 기술 또는 그 작성된 인터페이스를 말한다.
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 는 공통으로 사용한다.
실행 순서
- 클라이언트는 JDK 동적 프록시의 execute() 를 실행한다.
- JDK 동적 프록시는 InvocationHandler.invoke() 를 호출하고 구현체인 CommandTimeInvocationHandler.invoke() 가 호출된다.
- CommandTimeInvocationHandler 에 작성된 내부 로직을 수행하고, method.invoke(target, args) 를 호출해서 target 인 실제 객체 CommandAImpl 을 호출한다.
- CommandAImpl 인스턴스의 execute() 이 실행되고 실행이 끝나면 CommandTimeInvocationHandler 로 돌아와 실행시간 로그를 출력하고 그 결과를 반환한다.
JDK 동적 프록시 기술 덕분에 프록시 적용 대상 수 만큼 프록시를 직접 작성하지 않아도 된다. 또한 같은 부가기능(예제에서 실행시간 측정) 로직을 하나만 작성하여 공통으로 적용할 수 있다. 동적 프록시를 통해서 생성하고, 각각 필요한 InvocationHandler 를 구현해주면 되는 것이다.
JDK 동적 프록시의 한계
JDK 동적 프록시는 인터페이스가 필수이다.
인터페이스 없이 구현클래스만 존재하는 경우, 동적 프록시를 적용하려면 CGLIB 라는 바이트 코드를 조작하는 라이브러리를 사용해야 한다.