Programming

자바의 Dynamic Proxies

steloflute 2015. 9. 25. 23:30

http://www.javajigi.net/pages/viewpage.action?pageId=518

 

자바의 Dynamic Proxies

Dynamic Proxy 뭐하는 놈일까?

Proxy라는 놈이 들어갔으니까 대충 감으로는 때려 잡을 수 있겠지만 그건 단지 감이고, 한번 내부를 파봐야 뭔지 구체적으로 알거 같다. 우선 우리들이 일반적으로 흔히 알고 있는 Proxy라는 놈이 무엇인지 이해하고 넘어가야겠다. Proxy라고 하면 실제로 작동하는 A라는 놈과 같은 기능을 하면서 A인체 하는 것을 말한다. 외부에서 보기에는 A라는 클래스와 같은 인터페이스(프로그램적으로 봤을 때)를 제공하고 내부적으로 비슷한 기능을 하지만 실체를 까보면 A는 아니면서 A인체 하는 놈을 말한다. 여기까지 맞나? 지금까지 Proxy에 대하여 막연하게나마 가지고 있던 생각을 정리해봤다.

J2SE 1.3부터 제공하는 Dynamic Proxy도 추측해보면 특정 기능을 가진 인터페이스가 있을 경우 같은 인터페이스를 제공하면서 실제로 구현하는 클래스에 추가적인 기능을 가지면서 해당 클래스인양 속이는 놈이 아닐까? 한번 예제 소스를 보면서 이해해보자.

Dynamic Proxy 예제.

이 문서에서 사용한 예제는 Using java.lang.reflect.Proxy to Interpose on Java Class Methods에 있는 예제를 사용했습니다. 이 문서를 통하여 Dynamic Proxies의 더 깊은 곳까지 이해할 수 있을 겁니다.

Foo.java
    

package net.javajigi.oss.proxy;

public class Foo {
	public static void main(String[] args) {
		Bar bar = new BarImpl();
		bar.hello(2001, "xxx");
		bar.goodbye("yyy", 2002);
		System.out.println("======================================");
		Bar proxy = (Bar) TraceProxy.newInstance(new BarImpl());
		proxy.hello(2001, "xxx");
		proxy.goodbye("yyy", 2002);
	}
} 
Bar.java
    

package net.javajigi.oss.proxy;

public interface Bar {
    public void hello(int i, String s);
    public void goodbye(String s, int i);
} 
BarImpl.java
    

package net.javajigi.oss.proxy;

public class BarImpl implements Bar {
	public void hello(int i, String s) {
		System.out.println("   in net.javajigi.oss.proxy.Bar.hello");
	}

	public void goodbye(String str, int i) {
		System.out.println("   in net.javajigi.oss.proxy.Bar.goodbye");
	}
}
TraceProxy.java
    

package net.javajigi.oss.proxy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TraceProxy implements java.lang.reflect.InvocationHandler {

	private Object obj;

	public static Object newInstance(Object obj) {
		return java.lang.reflect.Proxy.newProxyInstance(obj.getClass()
				.getClassLoader(), obj.getClass().getInterfaces(),
				new TraceProxy(obj));
	}

	private TraceProxy(Object obj) {
		this.obj = obj;
	}

	public Object invoke(Object proxy, Method m, Object[] args)
			throws Throwable {
		Object result;
		try {
			System.out.print("begin method " + m.getName() + "(");
			for (int i = 0; i < args.length; i++) {
				if (i > 0)
					System.out.print(",");
				System.out.print(" " + args[i].toString());
			}
			System.out.println(" )");
			result = m.invoke(obj, args);
		} catch (InvocationTargetException e) {
			throw e.getTargetException();
		} catch (Exception e) {
			throw new RuntimeException("unexpected invocation exception: "
					+ e.getMessage());
		} finally {
			System.out.println("end method " + m.getName());
		}
		return result;
	}
}
Foo.java 실행한 결과값
    

   in net.javajigi.oss.proxy.Bar.hello
   in net.javajigi.oss.proxy.Bar.goodbye
======================================
begin method hello( 2001, xxx )
   in net.javajigi.oss.proxy.Bar.hello
end method hello
begin method goodbye( yyy, 2002 )
   in net.javajigi.oss.proxy.Bar.goodbye
end method goodbye

이와 같은 결과가 어떻게 나오게 되었는지를 확인하기 위하여 먼저 Foo.java를 살펴보자. Foo.java에서 BarImpl클래스를 직접 생성한 다음 hello, goodbye 메써드를 하면 당연히 예상되는 결과가 나온다. 그런데 TraceProxy를 이용하여 Bar 인스턴스를 생성한 다음 hello, goodbye 메써드를 호출할 경우 추가적인 메세지가 출력되는 것을 확인할 수 있다.

도대체 어떤 일이 일어난걸까? 역시 앞에서 예상한대로 Proxy라는 놈이 이상한 결과를 만들어 낸거 같다. 분명 인터페이스는 Bar로 튀어나왔는데 내부적으로 처리한 결과는 달라졌다. 어떻게 이런일이..

이것처럼 실제로 구현하는 클래스와 같은 기능을 하면서 자신이 추가적인 기능을 가지고 있는 놈이 Proxy라는 놈이다. 분명 Bar인터페이스의 기능을 구현하는 놈은 BarImpl인데 BarImpl이 하는 일을 자기 마음대로 바꿔 버렸다. 이 작업은 어디에서 했을까? 뭐 위 예제에서 보나마나 TraceProxy밖에 더 있겠는가? 근데 이 놈이 InvocationHandler 인터페이스를 구현하고 있네.

그렇다면 InvocationHandler 인터페이스가 가지고 있는 invoke 메써드와 TraceProxy의 newInstance 메써드에서 사용하고 있는 java.lang.reflect.Proxy 놈이 관련성이 있는거 같다. 어 처음보는 놈들이다. 지금까지 이런 놈 사용해 본 적이 없는데..이런..또 봐야할 놈이 하나 더 생겼나보다. 근데 J2SE 5.0도 아닌 1.3부터 이런 기능이 제공되고 있었는데 왜 몰랐지..? 역시 아직도 난 멀은거 같다. 근데 Java는 또 왜 이리 빨리 발전하는거야. 정말 따라가기조차 벅차군..

TraceProxy를 찬찬히 분석해 보면 Proxy의 newProxyInstance 메써드에 인자를 주고 Proxy라고 하는 Bar 인터페이스를 생성했더니 InvocationHandler 인터페이스를 상속하는 TraceProxy의 invoke 메써드가 호출되었네. 위 결과를 보면 Proxy로 만들어진 Bar 인터페이스의 메써드가 호출될 때마다 invoke 메써드가 호출되네. 이런 신기할 때가. 자식들 좀 잘 만들었네. 그런데 이런게 어디 필요해서 만들었지? 암튼 똑똑한 Sun 개발자들이 만들었으니 분명 필요성은 있을 거라 생각된다.

필요성을 찾기 전에 InvocationHandler의 invoke가 호출되는 메커니즘을 이해하고 싶은데 생각보다는 쉽지 않을거 같은데..오늘은 퇴근해야 되서 찾기 힘들거 같다. 이 건 시간이 많이 걸리거 같고..다음에 시간이 날 때 찾아보자. 혹 이미 찾은 사람 있으면 Comment 날려주면 좋을텐데.

Proxy를 사용하는 입장에서만 본다면 invoke가 호출되면서 인자로 실제 인스턴스(위 예에서는 BarImpl), 호출된 Method, Method 인자가 배열로 전달되는군. 그렇다면 여기서 자바의 reflection을 이용하여 실제 Bar 인터페이스에 대한 구현을 하고 있는 BarImpl 메써드를 호출하는 것이 가능하다는 이야기.. 역시나 Method.invoke를 이용하여 실제 구현 클래스인 BarImpl의 메써드들을 호출하는 군.

아 그거군. Proxy가 BarImpl의 메써드가 호출될 때 그 앞에서 Intercept해서 다른 추가적인 작업을 진행할 수 있다는 것이군. Proxy 자식 중간에서 특정 기능을 가로채는 놈이네.

위 예제로 봐서는 각 메써드가 호출되기 전과 후에 특정 메세지를 Logging할 때는 좋겠네. 우선 그러한 기능에 사용하면 될거 같네. 그 다음 또 사용할 때가 있나? 이론 무식해서인지 잘 모르겠다. 다른 문서 찾아봐야지..역시 다른 문서를 찾아보니 거기도 Logging에 좋을거라고 나오는군. 그 다음에 retry semantics, performance metrics, performance optimizations, test stubs, and caching, transaction 도 구현할 수 있다네. 메써드 호출 전/후의 Performance를 측정하는거 가능할 거라 생각이 되고, 나머지는 어떻게 구현해야 될지 잘 모르겠다.

Java Dynamic Proxies: One Step from Aspect-oriented Programming 문서 보니까 약간 힌트는 얻을 수 있네. 시간이 될 때 함 만들어봐야겠다.

근데 마지막으로 드는 의문점은 모든 프로그램이 그렇듯이 무엇인가 편하면 뒤따르는 단점이 있기 마련인데, 이 Proxy 기능 사용하면 실행속도에 문제될거 같은데.. 또 내부적으로 reflection 쓰니까 더 느릴거 같은데.. J2SE 1.4에서 reflection이 많이 빨라졌다고는 하는데 아무래도 실행속도에 문제가 될거 같다는 생각이 든다. 또한 이 기능이 Compile Time에 Proxy로 만들어지는 것이 아니라 Runtime시에 적용되는 것으로 보아서 더 느릴거 같은데.. 이거 사용할 때 실행속도에 어느 정도의 영향이 있는지 측정한 다음에 사용하는 것이 좋을거 같다.

어 근데 잠시 스치는 생각이 그렇다면 위 예제의 TraceProxy의 invoke 메써드에서 Method.invoke 메써드를 호출하지 않으면 BarImpl의 메써드가 아예 실행되지 않게 할 수도 있겠네. 테스트를 해보자.

수정된 TraceProxy.java
    

package net.javajigi.oss.proxy;

import java.lang.reflect.Method;

public class TraceProxy implements java.lang.reflect.InvocationHandler {

	private Object obj;

	public static Object newInstance(Object obj) {
		return java.lang.reflect.Proxy.newProxyInstance(obj.getClass()
				.getClassLoader(), obj.getClass().getInterfaces(),
				new TraceProxy(obj));
	}

	private TraceProxy(Object obj) {
		this.obj = obj;
	}

	public Object invoke(Object proxy, Method m, Object[] args)
			throws Throwable {
		Object result = null;
		try {
			System.out.print("begin method " + m.getName() + "(");
			for (int i = 0; i < args.length; i++) {
				if (i > 0)
					System.out.print(",");
				System.out.print(" " + args[i].toString());
			}
			System.out.println(" )");
			//result = m.invoke(obj, args);
		} catch (Exception e) {
			throw new RuntimeException("unexpected invocation exception: "
					+ e.getMessage());
		} finally {
			System.out.println("end method " + m.getName());
		}
		
		return null;
	}
}  
Foo.java 실행한 결과값
    

   in net.javajigi.oss.proxy.Bar.hello
   in net.javajigi.oss.proxy.Bar.goodbye
======================================
begin method hello( 2001, xxx )
end method hello
begin method goodbye( yyy, 2002 )
end method goodbye 

역시 예상한 대로 호출이 되지 않는군. 아예 BarImpl 클래스의 구현부까지 가로채어서 다른 일을 하도록 만들수도 있다는 말이네. 약간 위험할 수도 있겠네. 사용할 때 조심해야겠는걸.. 암튼 활용방안을 좀 더 깊이 있게 파본 다음에 사용해야겠다. 괜히 섣부르게 사용했다가 잘 돌아가는 프로그램 망칠수도 있겠는걸..그런데 잘 사용하면 기존에 중복해서 코딩해야했던 부분들을 상당히 줄여줄 수 있겠는 걸. 흥미가 가는 놈인데.

참고문서