Dynamic proxy
반복을 단순하게
kotlin 1.3.50, java 1.8 기준으로 작성하였습니다.
Android 의 유명한 라이브러리 retrofit2 는 interface 를 선언하고, 이 interface 를 마치 concrete class 의 method 처럼 사용한다.
interface 는 구현체가 없기 때문에, 구현을 대행해야 하는데 바로 이 때 사용할 수 있는 클래스가 java.lang.reflect.Proxy 이다.
간단히 String 을 반환하는 interface 를 구성하고 이를 proxy 를 통해 대행하여 문자열을 구성하는 예제를 작성해 보면 아래와 같다.
interface 정의가 다음과 같을 때
interface Contract {
fun first(value: Int): String
fun second(value1: Int, value2: Int): String
fun third(): String
}
원하는 결과는 다음과 같다.
method : first, args : 1
method : second, args : 3, 4
method : third, args : null
interface 를 대행해야 하기 때문에 Proxy 객체를 우선 생성해야 한다. Proxy 객체는 아래와 같은 함수를 통해 쉽게 생성할 수 있다.
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
ClassLoader, Interface array, 그리고 Invocation handler 를 인자로 받아 Proxy 객체를 생성한다. ClassLoader, Interfaces array 를 기초로 Proxy 객체를 통해 method 가 수행되면 Invocation Handler 가 호출되는 형태이다.
따라서 아래와 같이 작성할 수 있다.
class ContractProxy1 {
val contract: Contract = Proxy.newProxyInstance(
Contract::class.java.classLoader,
arrayOf(Contract::class.java)
) { _, method, args ->
"method : ${method.name}, args : ${args?.joinToString()}"
} as Contract
}
그리고 호출은 다음과 같이 한다.
val contracts1 = ContractProxy1().contracts
println(contracts1.first(1))
println(contracts1.second(3, 4))
println(contracts1.third())
ContractProxy1().contracts.first 등으로 method 를 수행하면 Invocation Handler 가 호출하고 원하는 결과가 정상적으로 출력된다.
위 예제의 ContractProxy1 은 Invocation handler 가 매우 간단한 동작만 수행하기 때문에 추가 관리 소요가 적지만, 보통은 그 안에서 객체를 생성하거나 복잡한 동작을 하기 때문에 (annotation 처리 혹은 객체 생성 등) cache layer 를 두고 처리 객체를 나누는 형태로 작성한다.
예를 들어 invocation handler 처리를 전담하는 class 를 아래와 같이 구성한다.
// annotation 을 처리하거나 객체를 생성하거나 등등 오래 걸리는 작업들을 담당
class ContractInterpreter(private val method: Method) {
fun interpret(args: Array<Any>?): String {
return "method : ${method.name}, args : ${args?.joinToString()}"
}
}
그리고 객체를 매번 생성하지 않도록 cache 를 구현한다.
class ContractProxy2 {
private val cache = ConcurrentHashMap<Method, ContractInterpreter>()
val contracts: Contract = Proxy.newProxyInstance(
Contract::class.java.classLoader,
arrayOf(Contract::class.java)
) { _, method, args ->
cache.computeIfAbsent(method) { ContractInterpreter(it) }.interpret(args)
} as Contract
}
method 를 key 로 cache 가 없으면 수행하고 ConcurrentHashMap 에 저장한다. 이 때 key 와 ContractInterpreter 를 잘 선택해서 cache 가 유효하고 오류가 없도록 주의한다.
val contracts2 = ContractProxy2().contracts
println(contracts2.first(1))
println(contracts2.second(3, 4))
println(contracts2.third())
결과는 ContractProxy1 과 동일하지만, 확장이 더 쉽고, cache 를 통해 더 효율적으로 수행할 수 있다.