개발을 할 때 기존 기능을 보존 하면서, 확장 해야 하는 경우가 있다. 원본을 어쩔 수 없이 수정 해야 할 때는 피할 수 없지만, 확장만 한다면, Proxy 를 사용 하는 경우 쉽게 확장이 가능 하다. 사용 방법을 알아 보자.


Proxy 는 기존 에 만들었던 Class 를 새로운 Class로 포장 했다고 생각 하면 쉽다.


public class Packing {

    private Target target;

    public Packing(Target target) {
        this.target = target;
    }

    // doing...

}


위 소스를 주목 하자. 포장지 에 기존 Class 를 담았다. 하지만 먼가가 부족 하다. 함수를 연결해줄 고리가 필요 하다. 그것이 InvocationHandler 이다. java.lang.reflect 패키지에서 제공 하고 있다.


포장지에 연결 고리를 달아보자.


public class Packing implements InvocationHandler {

    // doing...

    @Override
    public Object invoke(Object paramObject, Method paramMethod, Object[] paramArrayOfObject) throws Throwable {
        return paramMethod.invoke(paramObject, paramArrayOfObject);
    }

}


invoke 라는 함수를 볼 수 있다. 포장지와, 기존 Class 를 연결 시켜주는 역할을 한다.


paramObject: targetClass (기존 Class) 를 넣는다.
paramMethod: targetClass 에서 실행 하고 싶은 메소드를 넣는다.
paramArrayOfObject: targetClass 에서 실행하고 싶은 메소드의 파라미터 값을 넣는다.


위 3가지 설정을 끝내면 준비 작업은 끝났다. 3가지 정보에 따라 포장지에서 기존 Class 함수를 실행 하게 된다. 샘플을 만들어 보자.


Github: https://github.com/whitelife/whitelife-sample/tree/master/src/main/java/kr/co/whitelife/sample/proxy


  • Target
public class Target {

    public void doProcess() {
        System.out.println("doProcess.....");
    }

    public String doStringProcess(String string) {
        System.out.println("doStringProcess.....");
        return string;
    }

}

  • Packing
public class Packing implements InvocationHandler {

    private Target target;

    public Packing(Target target) {
        this.target = target;
    }

    public void doProcess() {
        try {
            System.out.println("Packing doProcess start.....");

            this.invoke(this.target, this.target.getClass().getDeclaredMethod("doProcess", new Class[]{}), new Object[]{});

            System.out.println("Packing doProcess end.....");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    public void doStringProcess(String string) {
        try {
            System.out.println("Packing doStringProcess start.....");

            String result = (String) this.invoke(this.target, this.target.getClass().getDeclaredMethod("doStringProcess", new Class[]{String.class}), new Object[]{string});
            System.out.println(result);

            System.out.println("Packing doStringProcess end.....");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object invoke(Object paramObject, Method paramMethod, Object[] paramArrayOfObject) throws Throwable {
        return paramMethod.invoke(paramObject, paramArrayOfObject);
    }
}

  • ProxyTest
public class ProxyTest {

    public static void main(String[] args) {
        Packing packing = new Packing(new Target());

        packing.doProcess();
        System.out.println();

        packing.doStringProcess("doString");
    }

}


샘플 작성이 끝났다면 실행 해 보자. 아래와 같은 출력을 볼 수 있다.


Packing doProcess start.....
doProcess.....
Packing doProcess end.....

Packing doStringProcess start.....
doStringProcess.....
doString
Packing doStringProcess end.....


여기 까지 왔다면, 성공 한 것이다. 기존에 각자 사용하던 Class를 Target 으로 생각하고, 포장지를 만들어서 사용하면 된다.


만약 Spring Framework 를 사용하는 경우, 직접 구현할 필요 없이 Spring AOP Around 기능을 사용하면 쉽게 처리가 가능 할 것이다. 위 샘플 코드와 비슷 하기 때문이다.


Spring MVC 사용 시 SimpleMappingExceptionResolver 설정을 해놓았다. Controller 에서 공통으로 필요한 부분을 AOP 로 만들어서 사용 하는대, Exception 발생 시 AOP Proxy 에서 예외가 발생 했다. 정작 필요한 에러 메시지는 볼 수 없었다.


일반적으로 AOP 설정을 하는 경우 아래와 같이 선언을 했었다.


<aop:config>    
    // Doing...
</aop:config>


<aop:config> 에 아무런 설정 값을 주지 않은 경우 기본적으로 JDK Proxy 로 동작 한다. Spring 에서 제공 하는 InvocableHandlerMethod.invoke(Object[] arg) 메소드를 사용 하여 실제 targetClass를 호출 한다. Java Reflection 기반 이라고 생각 하면 된다.


에러 메시지의 일부 이다. invoke 호출 시 예외가 발생 한다. targetClass 가 Interface 를 구현하고 있는 경우 instance 를 인식하지 못해 예외가 발생 하는 것 같다.


java.lang.IllegalStateException: The mapped controller method class 'com.xxx.xxx' is not an instance of the actual controller bean instance '$Proxy22'. If the controller requires proxying (e.g. due to @Transactional), please use class-based proxying.
HandlerMethod details: 
Controller [$Proxy22]
Method [public org.springframework.web.servlet.ModelAndView com.xxx.xxx.xxxxxx() throws java.lang.Exception]
// 생략...


문제 해결을 위해서는 Class 기반 proxy 사용을 해야 한다.
please use class-based proxying. 이 메시지가 친절하게 방법을 알려 준다.


CGLIB Proxy 를 사용하려면 아래와 같이 설정 값을 변경 한다.


<aop:config proxy-target-class="true">
    // Doing...
</aop:config>


테스트를 해보자. 정상적으로 동작 할 것 이다.


참고 사이트


+ Recent posts