Java InvocationHandler와 Proxy

서론
어떤 인터페이스의 다중 구현체의 실행 결과를 비교하는 기능을 구현하기 위해 JDK 동적 프록시 기술을 검토했다.
동적 프록시를 하기위한 핵심 인터페이스인InvocationHandler는 메서드 호출을 가로채 개발자가 정의한 로직을 주입할 수 있게 해준다.
이 글에서는 InvocationHandler의 javadoc 부터 실제 코드 구현까지, 공부한 내용을 핵심 위주로 공유해보려 한다.
InvocationHandler javadoc 살피기
InvocationHandler is the interface implemented by the invocation handler of
a proxy instance.
Each proxy instance has an associated invocation handler.
When a method is invoked on a proxy instance, the method invocation is
encoded and dispatched to the invoke method of its invocation handler.
Since:
1.3
See Also:
Proxy
Author:
Peter Jones
InvocationHandler 의 클래스단 javadoc 내용이다. Java 1.3 부터 내려온 역사있는 클래스임을 알 수 있다. 더 자세한 내용은 링크한 javadoc을 참고 해주시면 감사하겠다.
자바의InvocationHandler는 프록시 패턴을 구현할 때 핵심적인 역할을 하는 인터페이스다. 풀어 말하자면 ,객체의 메서드가 호출될 때 그 호출을 가로채서 개발자가 새로 정의한 로직을 실행할 수 있게 해주는 도구 인 것!
java.lang.reflect패키지에 포함되어 있으며, 주로 JDK 동적 프록시를 생성할 때 사용된다.
개인적으로 동적 프록시라는 말이 좀 생소했기에 좀 더 정리해 보자면, 개발자가 직접 프록시 클래스를 일일이 작성하지 않고,런타임에 프록시 객체를 동적으로 생성해주는 기술을 동적 프록시라 한다.
일반적인 프록시 패턴에서는 대상 클래스마다 대응하는 프록시 클래스를 새로 만들어야 하지만, 동적 프록시를 사용하면 공통 로직을 한 곳에 모아두고 여러 대상에 한꺼번에 적용할 수 있다.
동작 과정
일반적인 객체 호출은 클라이언트 -> 객체로 직접 이어지지만, InvocationHandler를 사용하면 중간에 거름망을 하나 두는 것과 같다.
어떤 메서드가 호출되든 일단 InvocationHandler의 invoke메서드로 전달 된다.
실제 비즈니스 로직의 앞뒤로 로깅, 대상 로직 실행 시간 측정, 권한 확인 등의 처리 작업을 추가 할 수 있다.
method.invoke(target, args); 이게 실제 객체의 메서드를 실행하는 코드인데 이 코드 앞 뒤로 원하는 코드를 두거나, 아예 실제 객체의 실행을 안하는것도 가능하다.
이렇듯 InvocationHandler로 프록시를 두면, 특정 인터페이스를 구현하는 모든 클래스에 대해 공통적인 로직을 한 번에 적용 가능하다.
구현 방법
InvocationHandler를 구현체는 invoke메서드 딱 하나만 오버라이드하면 된다.
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;proxy: 메서드가 호출된 프록시 객체 자신method: 호출된 메서드에 대한 정보(이름, 리턴 타입 등)를 담고 있는Method객체args: 메서드에 전달된 인자값들의 배열
예시 코드
// 1. 인터페이스 정의. 이 인터페이스가 프록시의 대상이다.
interface Hello {
void sayHello(String name);
}
// 2. InvocationHandler 구현
class MyHandler implements InvocationHandler {
private final Object target;
public MyHandler(Object target) {
this.target = target;
}
// 로깅하는 용도로 쓸 수 있다.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("------ 메서드 호출 전: 로그 기록 ------");
Object result = method.invoke(target, args); // 실제 객체의 메서드 실행
System.out.println("------ 메서드 호출 후: 마무리 작업 ------");
return result;
}
// 실제 객체의 메서드 실행시간을 측정 할 수도 있다.
//@Override
//public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// long start = System.nanoTime();
// Object result = method.invoke(target, args); // 실제 객체의 메서드 실행
// System.nanoTime() - start;
// System.out.println("Executing {} finished in {} ns", method.getName(), elapsed);
// return result;
//}
}
// 3. Proxy 실제 사용
public class StudySpringBootApplication {
public static void main(String[] args) {
Hello realObject = (name) -> System.out.println("안녕, " + name);
Hello proxyInstance = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(),
new Class<?>[]{Hello.class},
new MyHandler(realObject)
);
proxyInstance.sayHello("Java");
}
}
실행 시키면
------ 메서드 호출 전: 로그 기록 ------
안녕, Java
------ 메서드 호출 후: 마무리 작업 ------
이렇게 MyHandler에 작성해 놓은 System.out.println가 실행됨을 확인 할 수 있다.