주요글: 도커 시작하기
반응형
어플리케이션에 AOP 개념을 추가하기 위해 프록시 체인을 만드는 방법을 살펴본다.

정적인 방식의 프록시와 다이나믹 프록시

 자바월드(http://www.javaworld.com)에 실린 'Generically chain dynamic proxies' 글을 바탕으로 필자가 필요한 부분을 재구성고 편집하여 본 글을 작성하였다.

최근에, POJO(plain-old java object)를 사용한 프로그래밍 기법이 많이 사용되고 있다. EJB의 엔티티빈과 세션빈을 대신하여 Hibernate와 Spring과 같은 프레임워크가 사용되는데, 이들 프레임워크는 POJO를 지원하고 있다. POJO를 사용하면 EJB를 사용하는 것보다 쉽게 OOP를 적용할 수 있다. 하지만, 때로는 POJO를 사용한 비즈니스 객체에 새로운 관점(aspect) 또는 기능을 삽입해야 할 때가 있다. 예를 들어, 대부분의 비즈니스 객체에 보안이나 로깅을 추가하해야 하는 경우가 있을 수 있는데, 이를 POJO 자체로 구현하는 것은 쉽지 않다. (모든 비즈니스 객체에 보안 관련 코드를 넣는다고 생각해보라!) 이런 문제를 쉽게 해결해주는 것이 있는데, 그것이 바로 다이나믹 프록시이다.

다이나믹 프록시의 기본 개념은 기존에 존재하는 객체의 코드나 인터페이스를 변경하지 않은 채로 새로운 기능을 동적으로 추가한다는 것이다. GoF의 Decorator 패턴을 사용하면 객체의 코드 변경없이 객체에 새로운 행동을 추가하거나 새로운 관점을 추가할 수 있다. 하지만, 정적인 방식으로 Decorator 패턴을 구현하게 되면 몇가지 문제가 발생하게 되는데, 이에 대해서는 뒤에서 설명할 것이다.

코드 생성 도구를 사용해서 정적으로 Decorator 패턴이 구현된 클래스의 소스 코드를 생성하기도 하지만, 이는 코드 생성을 위한 추가적인 작업을 필요로 할 뿐만 아니라 생성된 코드를 관리해야 하는 부담도 발생한다. 이런 문제를 해결할 수 있는 방법이 있는데, 그것이 바로 다이나믹 프록시를 사용하는 것이다.

다이나믹 프록시가 어떻게 동작하는 지 살펴보기 전에, 다이나믹 프록시를 사용하지 않고 정적인 방식으로 Decorator 패턴과 프록시 체인에 대해서 먼저 살펴보도록 하자. 이를 통해 정적인 방식의 구현에서 발생할 문제점과 이를 다이나믹 프록시를 사용하여 해결하는 과정을 앞으로 보게 될 것이다.

정적인 방식의 Decorator 및 프록시 구현

다음과 같은 비즈니스 인터페이스가 있다고 하자.

    public interface IMyBusinessObject {
        public String doExecute(String in);
    }

그리고, 위 비즈니스 인터페이스를 구현한 클래스의 구현체가 아래와 같다고 하자.

    public class MyBusinessObject implements IMyBusinessObject {
        public String doExecute(String in) {
            System.out.println("Here in MyBusinessObject doExecute: input :" + in);
            return in;
        }
    }

우리가 하고자 하는 건 doExecute() 메소드 앞뒤로 특정한 기능(예를 들어, 로깅)을 추가하는 것이다. Decorator 패턴을 사용하면 이 기능을 쉽게 추가할 수 있다.

Decorator 패턴을 사용하기 위해서 다음과 같은 추상 Decorator 클래스를 작성하였다.

    public abstract class ADecorator implements IMyBusinessObject {
        protected IMyBusinessObject target;
        
        // target은 이 객체가 처리할(decorate) 대상으로서
        // 실제 비즈니스 객체가 대상이 된다.
        public void setTarget(IMyBusinessObject target_) {
            this.target = target_;
        }
        
        public ADecorator(){}
        
        public ADecorator(IMyBusinessObject target_) {
            setTarget(target_);
        }
    }

이제 ADecorator 추상 클래스를 구현한 DebugConcreteDecorator 클래스를 작성해보자. 이 클래스는 실제 비즈니스 클래스 객체의 메소드 호출 앞뒤로 디버그 메시즈를 출력하는 기능을 제공한다.

    public class DebugConcreteDecorator extends ADecorator {
        public String doExecute(String in) {
            System.out.println("DebugConcreteDecorator: before method : doExecute ");
            
            String ret = target.doExecute(in);
            System.out.println("DebugConcreteDecorator: after method : doExecute ");
            return ret;
        }
    }

이제, 클라이언트 코드는 다음과 같은 코드를 사용해서 비즈니스 객체를 호출하면 된다.

    // 원래 비즈니스 객체
    IMyBusinessObject aIMyBusinessObject = new MyBusinessObject();
    // 비즈니스 객체를 Decorator함
    IMyBusinessObject wrappedObject =
       new DebugConcreteDecorator(aIMyBusinessObject);
    // wrappedObject를 통해서 원래 객체를 호출
    wrappedObject.doExecute("Hello World");

위 코드에서 DebugConcreteDecorator 객체는 원본 비즈니스 객체를 감싼다. DebugConcreteDecorator의 deExecute() 메소드를 호출하게 되면 DebugConcreteDecorator.doExecute() 메소드가 실행된다. DebugConcreteDecorator의 doExecute() 메소드는 '호출 전 디버그 메시지 출력 > 원본 객체의 doExecute() 호출 > 호출 후 디버그 메시지 출력' 순서대로 실행된다. 실제 위 코드의 실행 결과는 아래와 같다.

    DebugConcreteDecorator: before method : doExecute
    Here in MyBusinessObject doExecute: input :Hello World
    DebugConcreteDecorator: after method : doExecute

이 결과로부터, 우리는 비즈니스 메소드 호출 앞뒤로 디버그 메시지를 추가할 수 있다는 것을 확인할 수 있다.

하나의 Decorator가 또 다른 Decorator를 호출하도록 함으로써 Decorator의 체인을 만들 수도 있다. Decorator의 체인에 대해 설명하기 위해서 또 다른 Decorator 구현체를 만들어보자.

    public class AnotherConcreteDecorator extends ADecorator {
        public String doExecute(String in) {
            System.out.println("AnotherConcreteDecorator: Going to execute method : doExecute");
            // 파라미터 값을 변경해서 원본 비즈니스 객체에 전달
            in = in + " Modified by AnotherConcreteDecorator"; 
           String ret = target.doExecute(in);
            System.out.println("AnotherConcreteDecorator: After execute method : doExecute");
            return ret;
        }
    }

위 Decorator는 전달받은 문자열 파라미터에 새로운 문장(" Modified by AnotherConcreteDecorator")을 추가하여, 원본 비즈니스 객체에 변경한 파라미터값을 전달한다.

DebugConcreteDecorator와 AnotherConcreteDecorator를 체인으로 연결하기 위해 클라이언트는 다음과 같은 코드를 사용하게 될 것이다.

    IMyBusinessObject aIMyBusinessObject = new MyBusinessObject();
    IMyBusinessObject wrappedObject =
       new AnotherConcreteDecorator(
          new DebugConcreteDecorator(aIMyBusinessObject));
    wrappedObject.doExecute("Hello World");

AnotherConcreteDecorator 인스턴스를 생성할 때 실제 비즈니스 객체 대신 DebugConcreteDecorator 인스턴스를 전달함으로써 두 Decorator의 체인을 생성하게 된다. 따라서 wrappedObject.doExecute()를 실행하면 먼저 AnotherConcreteDecorator.doExecute() 메소드가 실행되고, 이 메소드에서는 순차적으로 DebugConcreteDecorator.doExecute()를 호출한다. 그리고 DebugConcreteDecorator.doExecute() 에서는 MyBusinessObject.doExecute()를 호출하게 된다.

실제 실행해보면 다음과 같은 결과가 출력된다. 이 출력 결과를 보면 체인이 어떻게 순차적으로 실행되는 지 알 수 있을 것이다.

    AnotherConcreteDecorator: Going to execute method : doExecute
    DebugConcreteDecorator: before method : doExecute
    Here in MyBusinessObject doExecute: input :Hello World Modified by AnotherConcreteDecorator
    DebugConcreteDecorator: after method : doExecute
    AnotherConcreteDecorator: After execute method : doExecute

지금까지 살펴본 정적인 방식의 Decorator 패턴의 클래스 다이어그램은 다음과 같다.


정적인 방식의 Decorator 구현의 큰 문제점은 메소드 호출이 하드코딩으로 처리된다는 점이다. 두개의 Decorator 클래스(DebugConcreteDecorator 및 AnotherConcreteDecorator)의 doExecute() 메소드를 보면 직접 비즈니스 객체의 doExecute() 메소드를 호출하고 있다. 메소드 이름이 변경되거나 또는 비즈니스 객체에 새로운 메소드가 추가될 경우, 관련 Decorator 클래스의 코드도 함께 작업해주어야 한다.

다이나믹 프록시를 사용하면 하드코딩된 메소드 호출 때문에 발생하는 문제를 없앨 수 있다. 더불어, 비즈니스 인터페이스의 각각의 메소드를 Decorator 클래스 안에 구현하거나 오버라이딩 할 필요가 없어진다.

J2SE 1.3의 다이나믹 프록시 예제

J2SE 1.3부터 추가된 다이나믹 프록시는 런타임에 지정한 인터페이스를 구현한 클래스를 생성할 수 있도록 해 준다. java.lang.reflect.Proxy 클래스를 사용해서 지정한 인터페이스와 관련된 프록시 인스턴스를 생성할 수 있다. 이때, 각각의 프록시 인스턴스는 연관된 java.lang.reflect.InvocationHandler 객체를 갖고 있으며, 이 InvocationHandler 객체를 통해서 Decorator 패턴을 구현하게 된다.

앞서 작성했던 IMyBusinessObject 비즈니스 인터페이스와 MyBusinessObject 비즈니스 클래스의 Decorator였던 DebugConcreteDecorator 클래스를 InvocationHandler를 사용하여 구현하면 다음과 같다.

    public class MyDebugInvocationHandler
    implements java.lang.reflect.InvocationHandler {
    
        private Object target = null;

        public void setTarget(Object target_) {
            this.target = target_;
        }
        
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                System.out.println("Going to execute method : " + method.getName);
                Object retObject = method.invoke(target, args);
                System.out.println("After execute method : " + method.getName());
                return retObject;
            } catch(InvocationTargetException e) {
                throw e.getTargetException();
            } catch(Exception e) {
                throw e;
            }
        }
    }

위 코드에서 중요한건 invoke() 메소드이다. invoke() 메소드는 java.lang.reflect.Proxy 클래스에 의해서 호출된다. invoke() 메소드는 여분의 처리를 수행한 뒤 실제 대상 객체(예제의 경우에는 MyBusinessObject가 될것이다)에 처리를 전달한다.

클라이언트는 다음과 같이 MyDebugInvocationHandler 클래스와 JDK의 다이나믹 프록시를 사용하여 프록시 객체를 생성하고 호출할 수 있다.

    IMyBusinessObject bo = new MyBusinessObject(); // 실제 비즈니스 객체
    MyDebugInvocationHandler aMyDebugInvocationHandler = new MyDebugInvocationHandler();
    aMyDebugInvocationHandler.setTarget(bo);
    
    IMyBusinessObject proxyObject =
       (IMyBusinessObject) Proxy.newProxyInstance  // 프록시 객체 생성
          (IMyBusinessObject.class.getClassLoader(),
          new Class[] { IMyBusinessObject.class }, // 프록시할 인터페이스
          aMyDebugInvocationHandler); // InvocationHandler
    
    // 프록시 객체 사용
    System.out.println(proxyObject.doExecute("Hello World"));

위 코드를 보면, MyBusinessObject를 생성한 후, 이 객체를 MyDebugInvocationHandler 객체에 전달한다. (aMyDebugInvocationHandler.setTarget(bo) 부분) 이후, Proxy.newProxyInstance를 사용하여 IMyBusinessObject에 대한 프록시 객체를 생성하게 된다. Proxy.newProxyInstance를 사용하여 생성한 프록시 객체인 proxyObject의 doExecute() 메소드를 호출하면 InvocationHandler의 invoke() 메소드가 호출된다. 따라서 위 코드에서는 MyDebugInvocationHandler의 invoke() 메소드가 메소드 호출을 처리하게 된다.

이 코드에서 중요한 점은 InvocationHandler.invoke() 메소드는 java.lang.reflect.Method를 인자로 전달받기 때문에 비즈니스 인터페이스에 의존적이지 않다는 점이다. 또한, 비즈니스 인터페이스의 모든 메소드를 구현하지 않아도 비즈니스 객체의 모든 메소드의 앞뒤로 새로운 기능을 추가할 수 있다.

프록시 체인을 만들고 싶다면 setTarget() 메소드에 원본 객체 대신 프록시 객체를 전달하면 된다. 예를 들어, 앞서 작성한 MyDebugInvocationHandler와 MyAnotherInvocationHandler를 체인으로 형성하고 싶다면 앞서 작성했던 코드에 다음의 코드를 추가로 작성하면 된다.

    MyAnotherInvocationHandler aMyAnotherInvocationHandler = new MyAnotherInvocationHandler ();
    
    //실제 비즈니스 객체 대신에 프록시 객체를 전달
    aMyAnotherInvocationHandler.setTarget(proxyObject);
    
    IMyBusinessObject nextProxyObject =
       (IMyBusinessObject) Proxy.newProxyInstance
          (IMyBusinessObject.class.getClassLoader(),
          new Class[] { IMyBusinessObject.class },
          aMyAnotherInvocationHandler);
    
    System.out.println(nextProxyObject.doExecute("Hello World"));

지금까지 살펴본 예제를 통해서, 다이나믹 프록시를 사용하면 정적인 Decorator 체인을 사용하는 경우보다 적은 분량의 코드로 런타임에 동적으로 기능을 추가할 수 있다는 것을 알게 되었다. 하지만, 다이나믹 프록시를 사용하더라도 여전히 문제가 남아 있다. 그것은 바로 체인을 형성하려면 많은 코드를 작성해야만 한다는 점이다. 또한 일반 객체를 생성하기 위해 Proxy 클래스를 사용하는 것은 일반적인 사용자에게는 친숙한 방법이 아니며, 매번 위와 같은 코드가 반복된다는 것도 바람직하지 않다.

본 글의 나머지 부분에서는 이런 문제를 해결하면서, 간단한 API를 사용해서 쉽게 다이나믹 프록시 체인을 만들 수 있는 범용적인 프록시 팩토리 클래스를 작성해볼 것이다.

범용 다니아믹 프록시 체인 구현: 방식1

다음과 같은 코드를 사용해서 비즈니스 객체에 대한 프록시 체인을 만들 수 있다면 얼마나 좋을까?

    String[] interceptorClasses = {"MyDebugInterceptor",
                                  "MyAnotherInterceptor"};
    
    IMyBusinessObject aIMyBusinessObject =
       (IMyBusinessObject)MyProxyFactory.getProxyObject
          ("MyBusinessObject", interceptorClasses);
    
    String ret = aIMyBusinessObject.doExecute("Hello World");  

위 코드에서 핵심은 생성할 비즈니스 클래스의 이름과 프록시로 사용할 클래스의 이름을 String으로 MyProxyFactory 클래스에 전달하면, 프록시 체인 객체를 생성해준다는 점이다. MyProxyFactory가 생성한 프록시 객체의 메소드를 호출하면 체인으로 연결된 프록시가 차례대로 실행되고 최종적으로 원본 비즈니스 객체에 전달된다.

프록시 체인을 형성할 MyDebugInterceptor와 MyAnotherInterceptor가 비즈니스 인터페이스인 IMyBusinessObject에 의존적이지 않고 범용적이라면 더욱 좋을 것이다.

이를 구현하기 위해서 먼저 비즈니스 메소드의 호출 앞뒤에 독립적으로 메소드 가로채기를 할 수 있다고 가정하자. 다음과 같이 가로채기를 위한 인터페이스를 정의할 수 있을 것이다.

    public interface IMethodInterceptor {
        
        Object interceptBefore(Object proxy, Method method, Object[] args, Object realtarget);
        
        void interceptAfter(Object proxy, Method method, Object[] args, 
            Object realtarget, Object retObject, Object interceptBeforeReturnObject);
    }

interceptBefore() 메소드는 비즈니스 메소드가 호출되기 이전에 실행되고, interceptAfter() 메소드는 비즈니스 메소드가 성공적으로 실행된 이후에 실행된다.

이제 IMethodInterceptor 인터페이스를 구현한 두개의 클래스를 작성하자.

    public class MyDebugInterceptor implements IMethodInterceptor {
    
        public Object interceptBefore(Object proxy, Method method,
        Object[] args, Object realtarget) {
    
            System.out.println("MyDebugInterceptor: Going to execute method : ");
            return null;
        }
        
        public void interceptAfter(Object proxy, Method method, Object[] args,
        Object realtarget, Object retObject, Object interceptBefore) {
    
            System.out.println("MyDebugInterceptor: After execute method : " );
        }
    }

첫번째 구현 클래스는 비즈니스 객체의 대상 메소드가 메소드가 호출되기 전과 호출된 후에 단순히 콘솔에 디버깅을 위한 메시지를 출력한다.

    public class MyAnotherInterceptor implements IMethodInterceptor {
    
        public Object interceptBefore(Object proxy, Method method,
        Object[] args, Object realtarget) {
        
            System.out.println("MyAnotherInterceptor: Going to execute method : ");
            if ( method.getName().equals("doExecute") &&
                 args != null && args.length >= 1 ) {
            
                if ( args[0] instanceof String ) {
                    args[0] = args[0] +
                              " Modified by MyAnotherInterceptor";
                }
                return null;
            }
        }
    
        public void interceptAfter(Object proxy, Method method, Object[] args,
        Object realtarget, Object retObject, Object interceptBefore) {
        
            System.out.println("MyAnotherInterceptor: After execute method : ");
        }
    }

두번째 구현체인 MyAnotherInterceptor의 interceptBefore()는 대상 객체의 메소드를 호출하기 전에, 메소드의 이름이 doExecute이고 파라미터 길이가 1인 경우, 첫번째 파라미터의 값을 변경한다.

프록시 체인에서 사용할 IMethodInterceptor 구현체를 작성했으니, 이제 앞서 작성한 MyAnotherInterceptor 구현체를 사용하는 범용 InvocationHandler를 작성해보자.

    public class GenericInvocationHandler
            implements java.lang.reflect.InvocationHandler {
    
        private Object target = null;
        public void setTarget(Object target_) {
            this.target = target_;
        }
        
        private Object realtarget = null;
        public void setRealTarget(Object realtarget_) {
            this.realtarget = realtarget_;
        }
        
        IMethodInterceptor methodInterceptor = null;
        public void setMethodInterceptor(IMethodInterceptor methodInterceptor_) {
            this.methodInterceptor = methodInterceptor_;
        }
        
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            try {
                Object interceptBeforeReturnObject = null;
                if ( methodInterceptor != null ) {
                    interceptBeforeReturnObject =
                        methodInterceptor.interceptBefore
                          (proxy, method, args, realtarget );
                }
                Object retObject = method.invoke(target, args);
                if ( methodInterceptor != null ) {
                    methodInterceptor.interceptAfter
                        (proxy, method, args, realtarget,
                        retObject, interceptBeforeReturnObject );
                }
                return retObject;
            }
            catch(InvocationTargetException e) {
                throw e.getTargetException();
            }
    
            catch(Exception e) {
                throw e;
            }
        }
    }

GenericInvocationHandler의 invoke() 메소드를 보면 대상 객체의 메소드를 호출하기 이전에 그리고 호출한 다음에 각각 IMethodInterceptor의 interceptBefore() 메소드와 interceptAfter() 메소드를 호출하는 것을 알 수 있다.

이제 다음에 할일은 범용적인 프록시 체인을을 생성해주는 MyProxyFactory 클래스를 작성하는 것이다. 이 클래스를 통해서 여러분은 다이나믹 프록시를 사용하여 여러분 자신만의 프레임워크를 만드는 데 필요한 아이디어를 얻을 수 있을 것이다.

    public class MyProxyFactory {
    
        public static Object getProxyObject( String className,
            String[] interceptors ) throws Throwable {
            // 대상 클래스의 객체를 생성
            Object inputObject = getTargetObject(className);
            if ( interceptors != null && interceptors.length > 0  ) {
    
                Object inputProxiedObject = inputObject;
                // for 구문에서 프록시의 체인을 생성한다.
                for ( int i=0; i < interceptors.length; i++ ) {
                    inputProxiedObject =
                        getProxyObject(inputObject, interceptors[i],
                            inputProxiedObject);
                }
                return inputProxiedObject;
            }
            else {
                return inputObject;
            }
        }
    
        private static Object getProxyObject(Object inObject,
            String interceptor,Object inProxiedObject) throws Throwable {
    
            GenericInvocationHandler invocationHandler =
                new GenericInvocationHandler();
            IMethodInterceptor interceptorObject =
                (IMethodInterceptor)getInterceptor(interceptor);
            if ( interceptor == null ) {
                return inProxiedObject;
            }
            invocationHandler.setTarget(inProxiedObject);
            invocationHandler.setRealTarget(inObject);
            invocationHandler.setMethodInterceptor(interceptorObject);
    
            return Proxy.newProxyInstance
                        (inObject.getClass().getClassLoader(),
                        inObject.getClass().getInterfaces(),
                        invocationHandler) ;
        }
    
        private static Object getInterceptor( String interceptors )
            throws Exception {     
            //...
            //클래스 이름으로부터 인스턴스를 생성해서 리턴
            //Class.forName() 과 newInstance() 를 사용해서
            //인스턴스를 생성하고 리턴한다.
        }
        private static Object getTargetObject( String className )  
            throws Exception {
            //...
            //클래스 이름으로부터 인스턴스를 생성해서 리턴
            //Class.forName() 과 newInstance() 를 사용해서
            //인스턴스를 생성하고 리턴한다.
        }
    }

public static Object getProxyObject(String className, String[] interceptors) 메소드는 private static getProxyObject 메소드를 사용하여 interceptors로 전달한 프록시 객체를 생성하고, 각각의 프록시 체인을 연결하여 체인을 형성한다. private getProxyObject 메소드는 GenericInvocationHandler를 생성하고 setTarget, setRealTarget, setMethodInterceptor를 호출하여 프록시할 대상 객체, 실제 원본 비즈니스 객체, 그리고 IMethodInterceptor 객체를 지정한 뒤, Proxy.newProxyInstance를 사용하여 프록시 객체를 생성한다.

위 코드에서 여러분은 두가지 아이디어를 얻을 수 있다. 한가지는 클라이언트 코드가 아닌 MyProxyFactory 클래스에서 프록시 체인을 형성한다는 점과, 체인에 있는 IMethodInterceptor 개수만큼 GenericInvocationHandler 인스턴스를 생성한다는 점이다.

예제에서 String으로 클래스 이름을 전달했고 Class.forName()과 Class.newInstance()를 사용하여 대상 클래스와 IMethodInterceptor 구현 클래스의 인스턴스를 생성했다. 하지만, 실제 구현에서 프록시 필요할 때 마다 매번 Class.forName과 newInstance()를 사용하는 것은 문제가 될 수 있다. 예를 들어, 메소드를 호출하기 이전에 비즈니스 클래스나 인터셉터에서 어떤 변수를 설정하고 싶을 수가 있다. 또는 비즈니스 클래스나 인터셉터를 싱글톤으로 정의하고 싶을 수도 있다. 이런 세밀한 부분은 실제 프록시 체인에서 고려되어야 한다. 방식3에서 이런 부분을 처리할 수 있는 방법을 살펴볼 것이다.

방식1의 결과는 앞서 작성한 정적 방식의 decorator의 결과와 동일하다. 하지만, 정적인 방식에 비해 매우 큰 유연함을 갖고 있다. MyProxyFactory와 GenericInvocationHandler는 범용적이기 때문에, 이 클래스를 어떤 비즈니스 객체에든지 적용할 수 있다. 물론, 비즈니스 객체가 미리 정의된 인터페이스를 구현해야 하지만, 인터페이스를 사용하는 것은 좋은 프로그래밍 습관이다. (CGLIB를 사용하면 인터페이스 없이 다이나믹 프록시를 구현할 수 있는데, 이에 대한 내용은 자바캔의 'CGLIB를 이용한 프록시 객체 만들기' 글을 참고하기 바란다.)

MyDebugInterceptor와 MyAnotherInterceptor는 범용적으로 사용될 수 있기 때문에, 각각의 비즈니스 인터페이스 및 메소드를 위해서 별도로 작성할 필요가 없다. 만약 비즈니스 메소드를 실행하기 전에 보안 검사를 하고 싶다면, interceptBefore() 메소드에서 보안 검사를 수행할 수 있다. 보안 인터셉터는 데이터베이스나 파일로부터 보안 설정을 읽어와 보안 검사를 수행할 것이다. 다음과 같은 XML 파일을 사용해서 보안 설정 정보를 저장할 수 있다.

    <security>
        <businessClass>
            <classname>MyBusinessObject<classname>
            <methodPermission>
    
                <method>doExecute</method>
                <validRoles>admin,manager</validRoles>
            </methodPermission>
        </businessClass>
    </security>

보안 인터셉터는 이 설정 파일로부터 정보를 읽어와 메소드의 이름과 역할을 검사하여, 메소드를 실행하기 전에 보안 검사를 수행할 수 있을 것이다.

보안 인터셉텨의 interceptBefore() 메소드는 다음과 같은 코드를 가질 것이다.

    public Object interceptBefore(Object proxy, Method method,
            Object[] args, Object realtarget) {
            
        String nameOfMethod = method.getName();
        String targetClassName = realtarget.getClass().getName();
        MethodPermissions mPerm =
            SecurityFactory.getPermission(targetClassName);
        If ( !mPerm.isAuthorized(MyThreadLocalRoleStore.getRole(),
                                        nameOfMethod ) ) {
            throw new RuntimeException("User not authorized");
        }
        return null;
    }

방식의 클래스 다이어그램은 다음 그림과 같다.


위 클래스 다이어그램에서 눈여겨 볼점은 프록시 체인과 관련된 인터페이스 및 클래스가 비즈니스 인터페이스와 전혀 연관을 맺고 있지 않다는 것이다. (정적인 방식에서는 비즈니스 인터페이스와 Decorator가 연관되어 있었던 점을 기억하자.)

범용 다니아믹 프록시 체인 구현: 방식2

방식2는 방식1과 동일한 클래스를 사용하며, 클라이언트 입장에서 보면 동일하다. 코드는 GenericInvocationHandler와 MyProxyFactory에서만 발생한다. 앞서 방식1에서는 인터셉텨의 개수가 증가하는 만큼 GenericInvocationHandler 인스턴스와 프록시 인스턴스의 개수가 증가했다. 방식2에서는 이 문제를 해결하면서 방식1과 거의 같은 효과를 내도록 구현해볼 것이다.

따라서, 클라이언트에서 사용하는 코드는 방식1과 동일하다.

    String[] interceptorClasses = {"MyDebugInterceptor", "MyAnotherInterceptor"};
    IMyBusinessObject aIMyBusinessObject =
        (IMyBusinessObject)MyProxyFactory.getProxyObject
            ("MyBusinessObject", interceptorClasses);
    String ret = aIMyBusinessObject.doExecute("Hello World");

방식2에서 핵심은 GenericInvocationHandler와 프록시의 인스턴스를 단 한개만 사용한다는 점이다. 새롭게 작성한 GenericInvocationHandler 코드는 다음과 같다.

    public class GenericInvocationHandler
            implements java.lang.reflect.InvocationHandler {
    
        private Object realtarget = null;
        public void setRealTarget(Object realtarget_) {
            this.realtarget = realtarget_;
        }
        Object[] methodInterceptors = null;
        public void setMethodInterceptors
            (Object[] methodInterceptors_) {
            this.methodInterceptors = methodInterceptors_;
        }
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            try {
                Object[] retInterceptBefore = null;
                if ( methodInterceptors != null &&
                    methodInterceptors.length > 0 ) {
    
                    retInterceptBefore = new Object[methodInterceptors.length];
                    for ( int i= methodInterceptors.length - 1; i >= 0; i-- ) {
                        if ( methodInterceptors[i] != null ) {
                            retInterceptBefore[i] =
                                ((IMethodInterceptor)methodInterceptors[i]).
                                    interceptBefore(proxy,
                                        method,args, realtarget );
                        }
                    }
                }
                Object retObject = method.invoke(realtarget, args);
                if ( methodInterceptors != null ) {
                    for ( int i= 0; i < methodInterceptors.length; i++ ) {
                        if ( methodInterceptors[i] != null ) {
                     ((IMethodInterceptor)methodInterceptors[i]).
                         interceptAfter(proxy, method, args, realtarget,
                             retObject, retInterceptBefore[i] );
                        }
    
                    }
                }
                return retObject;
            }
            catch(InvocationTargetException e) {
                throw e.getTargetException();
            }
            catch(Exception e) {
                throw e;
            }
        }
    }

GenericInvocationHandler는 인터셉터의 배열을 입력받고, 비즈니스 객체의 메소드를 호출하기 전에 각 인터셉터의 interceptBeofore() 메소를 역순으로 실행한다. 비슷하게, 비즈니스 객체의 메소드를 호출한 뒤 인터셉터의 interceptAfter() 메소드를 순차적으로 실행한다. (방식은 한개의 인터셉터만 입력받는 것과 대조된다.)

MyProxyFactory의 경우 getInterceptor()와 getTargetObject() 메소드를 제외한 나머지 코드가 일부 바뀐다. 다음은 새롭게 작성한 MyProxyFactory 코드의 일부이다.

    public static Object getProxyObject( String className,
        String[] interceptors ) throws Throwable {
    
    
        Object inputObject = getTargetObject(className);
        if ( interceptors != null && interceptors.length > 0  ) {
            return getProxyObject(inputObject, interceptors);
        }
        else {
            return inputObject;
        }
    }
    private static Object getProxyObject(Object inObject,
        String[] interceptors) throws Throwable {
        
        // 단 한개의 GenericInvocationHandler 인스턴스
        GenericInvocationHandler invocationHandler =
            new GenericInvocationHandler();
        Object[] interceptorObjects = getInterceptors(interceptors);
        invocationHandler.setRealTarget(inObject);
        invocationHandler.setMethodInterceptors(interceptorObjects);
        
        return Proxy.newProxyInstance // 단 한개의 프록시 인스턴스
                        (inObject.getClass().getClassLoader(),
                        inObject.getClass().getInterfaces(),
                        invocationHandler) ;
    }
    private static Object[] getInterceptors(String[] interceptors)
        throws Exception {
    
        Object[] objInterceptors = new Object[interceptors.length];
        for ( int i=0; i < interceptors.length; i++ ) {
            objInterceptors[i] = getInterceptor(interceptors[i]);
        }
        return objInterceptors;
    }

위 코드를 보면 GenericInvocationHandler 인스턴스가 사용되는 것을 알 수 있다. 또한, 인터셉터의 개수에 상관없이 프록시 인스턴스도 단 한개만 생성되는 것을 알 수 있다. 따라서, 메모리 사용량이 적을 뿐 아니라, 더불어 방식1에 비해 수행속도도 약간 빨라진다. (뒤에서 간단한 테스트를 사용해서 각 방식의 수행속도를 비교해 볼 것이다.)

방식2에서 중요한 포인트는 MyProxyFactory가 아닌 GenericInvocationHandler에서 모든 인터셉터를 반복처리한다는 점이다. 또 한가지 중요한 포인트는 방식1과 방식에서 생성되는 인터셉터의 개수는 동일하지만, GenericInvocationHandler가 프록시 인스턴스의 개수가 다르다는 점이다. 방식1은 인터셉터의 개수만큼 GenericInvocationHandler과 프록시의 인스턴스가 생성되지만, 방식2는 인터셉터의 개수에 상관없이 각각 1개씩만 생성된다.

방식2의 클래스 다이어그램은 다음과 같다.


IMethodInterceptor 인터페이스에 메소드를 추가함으로써 더욱 유용하게 방식1과 방식2를 사용할 수 있다. (예를 들어, interceptException() 메소드). 또한, 각각의 메소드(interceptBefore(), interceptAfter(), interceptException())마다 서로 다른 인터페이스를 제공할 수도 있다. 원문 저자는 IMethodInterceptor 인터페이스를 작은 단위의 서로 다른 인터페이스로 나눌 것을 권하고 있다. 예를 들어, 메소드를 실행하기 이전에만 수행을 가로채고 싶다면, 오직 interceptBefore() 메소드만 사용될 뿐 나머지 메소드는 불필요하다.AOP(aspect-oriented programming)에서 프로그램의 서로 다른 단위를 가로채는 것을 advice 라고 표현한다. 그래서 before advice, after return advice, 또는 throws advice 를 위해 각각 별도의 인터페이스를 작성할 수 있다. 또 다른 타입의 advice는 범용적이고 유연한 advice로서 이런 모든 타입을 합친 것이 있다. 하지만, 범용적인 만큼 개발자의 책임이 커지게 된다. (개발자는 다음 대상에 대해서 메소드 호출이 전달되도록 해야 한다.) 이런 advice 타입을 around advice 라고 부른다. 방식 3에서는 AOP의 around advice와 비슷한 형태로 구현해볼 것이다.

방식1과 방식2는 모두 한계를 갖고 있으며, 큰 수정 없이는 다이나믹 프록시의 모든 장점을 사용할 수 없다. 예를 들어, interceptBefore()와 interceptAfter()는 별도로 호출되기 때문에, interceptBefore()와 interceptAfter() 사이에 연결되는 것을 구현하는 것은 쉽지 않다. 예를 들어, 특정한 원본 비즈니스 객체를 호굴하기 전에 다음과 같이 동기화를 하고 싶다고 해 보자.

    synchronized(realtarget) {
        retObject = method.invoke(target, args);
    }

방식1과 방식2에서는 이를 구현하는 것이 어렵다. 이를 해결하기 위한 방법을 방식3에서 살펴보도록 하자.

범용 다니아믹 프록시 체인 구현: 방식3

방식3에서, 클라이언트는 방식1 및 방식2와 비슷한 방법을 프록시 객체를 사용한다.

    String[] invocationHandlers = {"MyDebugInvocationHandler",
        "MyAnotherInvocationHandler"};
    IMyBusinessObject aIMyBusinessObject =
        (IMyBusinessObject)MyProxyFactory.getProxyObject
            ("MyBusinessObject", invocationHandlers);
    String ret = aIMyBusinessObject.doExecute("Hello World");

이 방식에서, IMyInvocationHandler 인터페이스를 다음과 같이 정의하였다.

    public interface IMyInvocationHandler {
        void setTarget(Object target_);
        void setRealTarget(Object realtarget_);
    }

또한, IMyInvocationHandler 인터페이스와 java.lang.reflect.InvocationHandler 인터페이스를 구현한 추상클래스를 다음과 같이 정의하였다.

    public abstract class AMyInvocationHandler
        implements IMyInvocationHandler,
            java.lang.reflect.InvocationHandler {
    
        protected Object target = null;
        protected Object realtarget = null;
    
        public void setTarget(Object target_) {
            this.target = target_;
        }
        public void setRealTarget(Object realtarget_) {
            this.realtarget = realtarget_;
        }
    }

방식3은 GenericInvocationHandler 클래스나 IMethodInterceptor 인터페이스를 사용하지 않는다. 대신, 모든 인터셉터가 위의 AMyInvocationHandler 추상 클래스를 상속받는다. 방식1과 방식2에서 살펴봤었던 두개의 인터셉터를 작성해보도록 하자. 이 두 인터셉터는 AMyInvocationHandler를 상속받으며, Invocationhandler 인터페이스에 정의된 invoke() 메소드를 구현한다.

  1.     public class MyDebugInvocationHandler extends AMyInvocationHandler {
        
           public Object invoke(Object proxy, Method method, Object[] args)
              throws Throwable {
        
              try {
                 System.out.println("MyDebugInterceptor: Before execute method : "
                    + method.getName());  
                 Object retObject = method.invoke(target, args);
                 System.out.println("MyDebugInterceptor: After execute method : "
                    + method.getName());
                 return retObject;
              }
              catch(InvocationTargetException e) {
                 throw e.getTargetException();
              }
              catch(Exception e) {
                 throw e;
              }
           }
        }

  2.     public class MyAnotherInvocationHandler extends AMyInvocationHandler {
        
           public Object invoke(Object proxy, Method method, Object[] args)
              throws Throwable {
        
              try {
                 System.out.println("MyAnotherInvocationHandler: Before execute method : "
                    + method.getName());        
                 if ( method.getName().equals("doExecute")
                    && args != null && args.length >= 1 ) {
                    if ( args[0] instanceof String ) {
                       args[0] = args[0] + " Modified by MyAnotherInvocationHandler";
                    }
                 }
                 Object retObject = method.invoke(target, args);
                 System.out.println("MyAnotherInvocationHandler: After execute method : "
                    + method.getName());
                 return retObject;
              }
              catch(InvocationTargetException e) {
                 throw e.getTargetException();
              }
              catch(Exception e) {
                 throw e;
              }
           }
        }

MyDebugInvocationHandler 클래스와 MyAnotherInvocationHandler 클래스는 doExecute()를 실행하기 이전/이후에 호출되거나 예외가 발생할 때 호출될 별도의 메소드를 갖고 있지 않다. 방식3은 서블릿의 Filter 인터페이스가 doBefore()나 doAfter()가 없는 것과 유사하다.

방식3은 방식1 및 방식2와 달리 개발자에게 더 많은 유연함을 제공한다. 예를 들어, 방식3을 사용하면 앞서 말했었던 동기화를 수행할 수 있다. 단지 AMyInvocationHandler를 상속받고 invoke() 메소드에서 동기화 로직을 수행하기만 하면 된다. 예를 들면, 다음과 같이 구현할 수 있을 것이다.

    public class MySynchronizeInvocationHandler extends AMyInvocationHandler {
    
       public Object invoke(Object proxy, Method method, Object[] args)
          throws Throwable {
          
          Object retObject = null;
          synchronized(realtarget) {
             retObject = method.invoke(target, args);
          }
          return retObject;
       }
    }

이제 방식3의 MyProxyFactory 코드를 살펴보자.

    public static Object getProxyObject( String className,
       String[] invocationHandlers ) throws Throwable {
    
       Object inputObject = getTargetObject(className);
       if ( invocationHandlers != null &&
          invocationHandlers.length > 0  ) {
    
          Object inputProxiedObject = inputObject;
          for ( int i=0; i < invocationHandlers.length; i++ ) {
             AMyInvocationHandler myInvocationHandler =
                (AMyInvocationHandler)getInvocationHandler
                   (invocationHandlers[i]);
             inputProxiedObject = getProxyObject(inputObject,
                myInvocationHandler, inputProxiedObject);
          }
          return inputProxiedObject;
       }
       else {
          return inputObject;
    
       }
    }
    public static Object getProxyObject( Object inputObject,
       Object[] invocationHandlers ) throws Throwable {
    
       if ( invocationHandlers != null
          && invocationHandlers.length > 0  ) {
    
          Object inputProxiedObject = inputObject;
          for ( int i=0; i < invocationHandlers.length; i++ ) {
             inputProxiedObject = getProxyObject(inputObject,
                (AMyInvocationHandler)invocationHandlers[i],
                   inputProxiedObject);
          }
    
          return inputProxiedObject;
       }
       else {
          return inputObject;
       }
    }
    private static Object getProxyObject(Object inObject,
       AMyInvocationHandler myInvocationHandler,
          Object inProxiedObject) throws Throwable {
    
       if ( myInvocationHandler == null ) {
          return inProxiedObject;
       }
       myInvocationHandler.setTarget(inProxiedObject);
       myInvocationHandler.setRealTarget(inObject);
    
       return Proxy.newProxyInstance
                      (inObject.getClass().getClassLoader(),
                      inObject.getClass().getInterfaces(),
                      myInvocationHandler) ;
    }

이 코드는 방식의 MyProxyFactory와 거의 유사하다. 위 코드는 또한 InvocationHandler의 배열을 순차적을 처리하고, InvocationHandler와 프록시 객체를 각각의 반복마다 한개씩 생성한다. 방식1에서는 GenericInvocationHandler 인스턴스, 인터셉터 인스턴스, 프록시 객체가 반복때마다 각각 한번씩 생성되었지만, 방식3에서는 InvocationHandler 인스턴스와 프록시 객체만 반복때마다 한번씩 생성된다. (인터셉터가 없으므로 그런 것이다.)

위 코드를 보면 MyProxyFactory 클래스에 다음과 같은 또다른 public static 메소드가 추가되었다.

    public static Object getProxyObject
       ( Object inputObject, Object[] invocationHandlers )

Class.forName()을 사용해서 비즈니스 원본 객체를 생성하게 되면 비즈니스 객체/InvocationHandler/인터셉터를 사용하기 전에 특정 처리를 수행할 수 없게 된다. 그래서, 그런 처리를 하기 위해 위와 같은 메소드를 추가하였다. (방식1과 방식2에서도 비슷한 메소드를 추가할 수 있다.) 이제, 기존에 존재하는 비즈니스 객체나 InvocationHandler/인터셉터를 MyProxyFactory에 전달하여 프록시 체인을 생성할 수 있다. 예를 들어, 다음과 같이 코드를 작성할 수 있게 된 것이다.

    MyDebugInvocationHandler dHandler = new MyDebugInvocationHandler();
    MyAnotherInvocationHandler aHandler = new MyAnotherInvocationHandler();
    IMyBusinessObject bo = new MyBusinessObject();
    
    //비즈니스 객체(bo)나 InvocationHandler(dHandler, aHander)에 대해
    //별도 처리를 수행할 수 있다. 또한 이들 객체를 싱글톤으로 구할수도 있다.
    
    Object[] invocationHandlers = { dHandler, aHandler };
       (IMyBusinessObject)MyProxyFactory.getProxyObject
          (bo , invocationHandlers);
    String ret = aIMyBusinessObject.doExecute("Hello World");

방식3에서 클래스 다이어그램은 다음과 같다.


 
각 방식의 성능 비교 및 결론

앞서 소개한 각각의 접근 방식의 수행속도의 차이를 비교하기 위해서 간단한 테스트를 실행해보았다. 테스트 기계(CPU 개수, CPU 속도, 메모리 사용 등)를 고려하지 않고 테스트하였으므로 확장성은 고려되지 않았다. 하지만, 단순한 테스트를 통해서 각 구현 방식의 성능에 대한 개략적인 아이디어를 얻을 수 있을 것이다.

테스트는 프록시 팩토리로 생성한 프록시 객체의 메소드를 천번 호출하는 것으로 진행하였다. 다음 정보는 천번 호출에 대한 평균을 1/1000초 단위로 정리한 것이다.

  • 정적 decorator 체인: 51.7
  • 방식1: 166.5
  • 방식2: 125.1
  • 방식3: 159.25
동적 프록시 체인에서 수행 속도가 중요하다면 방식2를 고려해 볼 수 있다는 것을 알 수 있다.

결론

이 글에서는 static 메소드를 사용하여 프록시 체인을 생성했는데, 만약 팩토리 클래스를 상속하거나 서로 다른 설정을 가진 팩토리 인스턴스가 필요한 경우에는 문제가 된다. 이런 경우에는 알맞은 팩토리 패턴을 사용해서 여러분 스스로 팩토리 클래스를 작성하기 바란다.

본 글에서는 자바로 다이나믹 프록시를 구현하는 방법을 살펴봄으로써 AOP의 일부 개념을 다뤄보았다. join point나 point cut과 같은 다른 AOP의 개념 역시 팩토리 클래스에서 구현할 수 있을 것이다. 이에 대한 자세한 내용은 하단의 '관련 링크'를 참고하기 바란다.

기존에 존재하는 AOP 프레임워크가 제공하는 모든 기능이 필요없고 일부의 기능만 필요하다면 여러분이 직접 범용 다이나믹 프록시를 사용하여 작은 AOP 프레임워크를 구현할 수 있게 되었다. 본 글에서 살펴본 내용이 여러분이 직접 AOP 프레임워크를 구현하는데 필요한 기본 아이디어를 제공했으리라 생각한다. 또한, 기존에 다이나믹 프록시 방식을 사용하는 AOP 프레임워크를 사용하고 있었다면, 본 글을 통해서 해당 AOP 프레임워크의 사용법에 대한 이해를 높일 수 있게 되었을 것이다.

관련링크:

+ Recent posts