신입사원 시절 회사에서 사용하기 위해 Mybatis를 공부하다가 문득 의문점이 생겼습니다.
@Mapper Annotaion을 붙인 Interface의 Instance는 어떻게 생기는걸까?
Mapper Interface를 출력하는 테스트코드를 작성해보니 다음과 같이 출력되었습니다.
org.apache.ibatis.binding.MapperProxy@6958d5d0
출력된 결과인 MapperProxy를 찾아 소스를 열어보니 다음과 같이 invoke 메서드가 있었습니다.
public class MapperProxy<T> implements InvocationHandler, Serializable {
// --- 중략 ---
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
// --- 중략 ---
}
MapperProxy는 완전히 Dynamic Proxy로만 만들어지진 않았지만,
InvocationHandler, invoke 등 Dynamic Proxy에서 사용되는 용어들이 보입니다.
https://en.wikipedia.org/wiki/Proxy_pattern
Dynamic Proxy는 지정된 하나 이상의 인터페이스를 구현하는 클래스를 런타임에 생성할 수 있게 해주는 클래스입니다.
Dynamic Proxy는 java.lang.reflect.Proxy클래스의 newProxyInstance Method를 사용해서 만들 수 있습니다.
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
| Parameter | Description |
|---|---|
| ClassLoader loader | Proxy 클래스를 정의할 클래스 로더 |
| Class<?>[] interfaces | Proxy 클래스가 구현할 인터페이스들 |
| InvocationHandler h | Method호출을 전달하기 위한 Handler |
예시로 직원이 드나들 수 있는 SpeedGate를 만들어 보겠습니다.
직원이 들어가고 나가는 SpeedGate Interface를 다음과 같이 만들었습니다.
public interface SpeedGate {
String in(User user); // 직원이 들어간다.
String out(User user); // 직원이 나간다.
}
단순히 직원이 들어가고 나가는 DefaultSpeedGate를 만들었습니다.
public class DefaultSpeedGate implements SpeedGate{
@Override
public String in(User user) {
return user.getName() + "(이)가 들어왔습니다.";
}
@Override
public String out(User user) {
return user.getName() + "(이)가 나갔습니다.";
}
public static void main(String[] args) {
SpeedGate defaultSpeedGate = new DefaultSpeedGate();
User lkh = new User("이경훈");
System.out.println(defaultSpeedGate.in(lkh));
System.out.println(defaultSpeedGate.out(lkh));
}
}
/*
main method 실행 결과:
이경훈(이)가 들어왔습니다.
이경훈(이)가 나갔습니다.
*/
Dynamic Proxy를 활용해 들어오고 나갈 때, 시간을 함께 출력하도록 변경해보겠습니다.
public class TimeLoggingSpeedGate implements InvocationHandler {
private Object defaultSpeedGate;
private TimeLoggingSpeedGate(Object defaultSpeedGate) {
this.defaultSpeedGate = defaultSpeedGate;
}
public static Object newInstance(Object defaultSpeedGate){
return Proxy.newProxyInstance(
defaultSpeedGate.getClass().getClassLoader(),
defaultSpeedGate.getClass().getInterfaces(),
new TimeLoggingSpeedGate(defaultSpeedGate)
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
LocalDateTime now = LocalDateTime.now();
Object invoke = method.invoke(defaultSpeedGate, args);
return now + " - " + invoke;
}
public static void main(String[] args) {
SpeedGate defaultSpeedGate = new DefaultSpeedGate();
SpeedGate timeLoggingSpeedGate = (SpeedGate) TimeLoggingSpeedGate.newInstance(defaultSpeedGate);
User lkh = new User("이경훈");
System.out.println(timeLoggingSpeedGate.in(lkh));
System.out.println(timeLoggingSpeedGate.out(lkh));
}
}
/*
main method 실행 결과:
2024-01-15T21:43:12.776076 - 이경훈(이)가 들어왔습니다.
2024-01-15T21:43:12.783697 - 이경훈(이)가 나갔습니다.
*/
매개변수로 받는 Method를 활용해 들어올 때만 시간을 체크하는 SpeedGate를 만들어보겠습니다.
public class InTimeLoggingSpeedGate implements InvocationHandler {
private Object defaultSpeedGate;
private InTimeLoggingSpeedGate(Object defaultSpeedGate) {
this.defaultSpeedGate = defaultSpeedGate;
}
public static Object newInstance(Object defaultSpeedGate){
return Proxy.newProxyInstance(
defaultSpeedGate.getClass().getClassLoader(),
defaultSpeedGate.getClass().getInterfaces(),
new InTimeLoggingSpeedGate(defaultSpeedGate)
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("in".equals(method.getName())) {
LocalDateTime now = LocalDateTime.now();
Object invoke = method.invoke(defaultSpeedGate, args);
return now + " - " + invoke;
} else{
return method.invoke(defaultSpeedGate, args);
}
}
public static void main(String[] args) {
SpeedGate defaultSpeedGate = new DefaultSpeedGate();
SpeedGate timeLoggingSpeedGate = (SpeedGate) InTimeLoggingSpeedGate.newInstance(defaultSpeedGate);
User lkh = new User("이경훈");
System.out.println(timeLoggingSpeedGate.in(lkh));
System.out.println(timeLoggingSpeedGate.out(lkh));
}
}
/*
main method 실행 결과:
2024-01-15T21:48:15.586882 - 이경훈(이)가 들어왔습니다.
이경훈(이)가 나갔습니다.
*/
Class<?>[] interfaces 에 null 이 들어가면 NullPointerException이 발생합니다. public static Object newInstance(Object defaultSpeedGate){
return Proxy.newProxyInstance(
defaultSpeedGate.getClass().getClassLoader(),
new Class[]{SpeedGate.class, null},
new InTimeLoggingSpeedGate(defaultSpeedGate)
);
}
// NullPointerException 발생!
Class<?>[] interfaces 에 같은 class가 중복되면 IllegalArgumentException이 발생합니다. public static Object newInstance(Object defaultSpeedGate){
return Proxy.newProxyInstance(
defaultSpeedGate.getClass().getClassLoader(),
new Class[]{SpeedGate.class, SpeedGate.class},
new InTimeLoggingSpeedGate(defaultSpeedGate)
);
}
// IllegalArgumentException 발생!
Class<?>[] interfaces 에 Interface가 아닌 class나 primitive type이 들어가면 IllegalArgumentException이 발생합니다. public static Object newInstance(Object defaultSpeedGate){
return Proxy.newProxyInstance(
defaultSpeedGate.getClass().getClassLoader(),
new Class[]{DefaultSpeedGate.class},
new InTimeLoggingSpeedGate(defaultSpeedGate)
);
}
// IllegalArgumentException 발생!