Tech & Programming/Pattern & Design

프록시 패턴(proxy pattern) 이란?

소스코드 요리사 2019. 1. 14. 16:41

이번에는 프록시 패턴(proxy pattern)에 대해서 알아보도록 하겠습니다.

 

프록시 패턴은 어떤 객체에 대한 접근을 제어하는 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴입니다.

 

주로 프록시 패턴은 RealSubject 가 원격 시스템에서 돌아가거나, 그 객체의 생성 비용이 많이 들어 실제 사용 시점에 객체를 생성하거나, 

실제 객체에 접근을 제한 및 제어를 해야 할 때 등 의 경우에 사용 됩니다.

 

프록시 패턴의 UML은 아래와 같습니다.

 

 

Proxy 에는 RealSubject에 대한 레퍼런스가 들어있습니다.

그리고, Proxy 와 RealSubject 는 똑같은 인터페이스(Subejct)를 구현하기 때문에 RealSubejct 객체가 들어갈 자리면 어디든지 Proxy 를 대신 쓸 수 있습니다. 

그래서, 클라이언트는 Proxy를 중간에 두고 Proxy를 통해서 RealSubject 하고 데이터를 주고 받습니다.

 

프록시 패턴의 종류

- 원격 프록시 : 원격 객체에 대한 접근 제어가 가능합니다.

- 가상 프록시 (Virtual Proxy) : 객체의 생성비용이 많이 들어 미리 생성하기 힘든 객체에 대한 접근 및 생성시점 등을 제어합니다.

- 보호 프록시 (Protection Proxy) : 객체에 따른 접근 권한을 제어해야하는 객체에 대한 접근을 제어할 수 있습니다.

- 방화벽 프록시 : 일련의 네트워크 자원에 대한 접근을 제어함으로써 주 객체를 '나쁜' 클라이언트들로부터 보호하는 역할을 합니다.

- 스마트 레퍼런스 프록시 (Smart Reference Proxy) : 주 객체가 참조될 때마다 추가 행동을 제공합니다. ex) 객체 참조에 대한 선 작업, 후 작업 등

- 캐싱 프록시 (Caching Proxy) : 비용이 많이 드는 작업의 결과를 임시로 저장 하고, 추후 여러 클라이언트에 저장된 결과를 실제 작업처리 대신 보여주고 자원을 절약하는 역할을 합니다.

- 동기화 프록시 (Synchronization Proxy) : 여러 스레드에서 주 객체에 접근하는 경우에 안전하게 작업을 처리할 수 있게 해줍니다. 주로 분산 환경에서 일련의 객체에 대한 동기화 된 접근을 제어해주는 자바 스페이스에서 쓰입니다.

- 복잡도 숨김 프록시 (Complexity Hiding Proxy) : 복잡한 클래스들의 집합에 대한 접근을 제어하고, 복잡도를 숨깁니다. 

- 지연 복사 프록시 (Copy-On-Write Proxy) : 클라이언트에서 필요로 할 때까지 객체가 복사되는 것을 지연시킴으로써 객체의 복사를 제어합니다. '변형된 가상 프록시'라고 할 수 있으며,

Java 5 의 CopyOnWriteArrayList 에서 쓰입니다.

 

아래 예제는 Head First 책에 나와 있는 예제로 보호 프록시의 구현 예 입니다.

Head First 책자의 프록시 예제 중 보호프록시 구현 예제를 구현예를 설명드리겠습니다.

결혼 정보회사의 회원 정보 관련 예제 입니다.

 

회원정보의 당사자인 경우에는 기본정보 사항에 대한 set, get 권한만을 주고, 일반 정보회사 이용자들에게는 get 권한과 해당 회원에 대한 점수만 메길 수 있도록 권한을 줍니다.

 

그리고, 이 예제에서는 직접 Proxy 클래스를 작성하는 것이 아니라 자바(java.lang.reflect)에서 기본적으로 제공하는 Procy 인터페이스를 이용해 동적프록시로 구현을 합니다.

실제 프록시 클래스는 실행중에 생성되기 때문에 동적프록시(dynamic proxy)라고 부릅니다.

 

자세한 사항은 UML을 보고 설명 드리겠습니다.

 

 

 

동적프록시의 경우는 Proxy클래스를 Java에서 만들어 주기 때문에 Proxy 클래스 내부를 직접 구현 할 수가 없습니다.

따라서, 무슨 일을 할지 알려주기 위한 방법이 필요합니다.

필요한 코드를 InvocationHandler를 implements 하여 InvocationHandler의 invoke() 메소드에 

Proxy로 호출되는 모든 메소드에 대해 응답하는 역할을 맡도록 구현 후  Proxy.newProxyInstance의 파라메터로 넘겨줍니다.

 

소스코드를 간단히 설명해보겠습니다.

 

먼저, PersonBean 인터페이스입니다. 그냥 보시면 각 메소드들이 어떤 역할을 하게 될 지 아실 수 있을겁니다.

 
public interface PersonBean {
    String getName();
    String getGender();
    String getInterest();
    int getHotOrNotRating();
    void setName(String name);
    void setGender(String gender);
    void setInterest(String interest);
    void setHotOrNotRating(int rating);
}
 
다음은 중요한 InvocationHandler 인터페이스 구현한 NonOwnerInvoctionHandler와 OwnerInvoctionHandler 입니다.
두 개가 필요한 이유는 회원정보 당사자 용 프록시와 결혼 정보회사 이용자 용 프록시로 나뉘기 때문입니다.

 

public class NonOwnerInvoctionHandler implements InvocationHandler {
    PersonBean person;

    public NonOwnerInvoctionHandler(PersonBean person) {
        this.person = person;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
        try {
            if (method.getName().startsWith("get") == true) {
                return method.invoke(person, args);
            } else if (method.getName().equals("setHotOrNotRating") == true) {
                return method.invoke(person, args);
            } else {
                System.out.println("IllegalAccessException");
                return new IllegalAccessException();
            }
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
} 
public class OwnerInvoctionHandler implements InvocationHandler {
    PersonBean person;

    OwnerInvoctionHandler(PersonBean personBean) {
        this.person = personBean;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
        try {
            if (method.getName().startsWith("get")) {
                return method.invoke(person, args);
            } else if (method.getName().equals("setHotOrNotRating")) {
                System.out.println("IllegalAccessException");
                new IllegalAccessException();
            } else if (method.getName().startsWith("set")) {
                return method.invoke(person, args);
            }
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
} 
 
앞에서 잠깐 설명한 것과 같이 프록시에서 함수를 호출하면, 핸들러인 InvocationHandler 의  
Object invoke(Object proxy, Method method, Object[] args) 메소드를 호출합니다.
 
아래 코드의 getNonOwnerProxy(), getOwnerProxy() 를 보시면 프록시를 동적으로 생성하기 위해서 무엇이 필요한지 알 수 있습니다.
그리고, 프록시는 PersonBean인터페이스를 전달 받기 때문에  ownerProxy.setName("홍길동") 과 같이 PersonBean 의 메소드를 그대로 쓸 수 있습니다.
public static void main(String[] args)
{
    Main main = new Main();
    PersonBean person1=new PersonBeanImpl();
    PersonBean ownerProxy = main.getOwnerProxy(person1);
    ownerProxy.setName("홍길동");
    ownerProxy.setGender("남자");
    ownerProxy.setInterest("무협소설 읽기");
    ownerProxy.setHotOrNotRating(10);
} // ownerProxy 이기 때문에 실제 실행이 안되는 것을 볼 수 있다.          
// PersonBean nonOwnProxy = main.getNonOwnerProxy(person1);
// nonOwnProxy.setName("박길동");  
// nonOwnProxy 이기 때문에 setName은  실행이 안되는 것을 볼 수 있다.
// ....     }      
// PersonBean getOwnerProxy(PersonBean person) { 
//  return (PersonBean) Proxy.newProxyInstance(
//      person.getClass().getClassLoader(),
//      person.getClass().getInterfaces(),
//      new OwnerInvoctionHandler(person));
// }      
// PersonBean getNonOwnerProxy(PersonBean person) { 
//      return (PersonBean) Proxy.newProxyInstance(
//          person.getClass().getClassLoader(),
//          person.getClass().getInterfaces(),
//          new NonOwnerInvoctionHandler(person));     
// }  
main 함수를 실행시켜 실행결과를 보면 proxy 에 따라 메소드 실행여부가 달라지는 것을 볼 수 있습니다.
이렇게 자바의 프록시를 이용해 보호 프록시를 구현해 보았습니다. 
인터넷에 검색해보시면 다양한 프록시 예제들이 많으니 참고하시면 더 많은 공부가 될 것입니다.
 

데코레이터 패턴 VS 프록시 패턴

데코레이터(Decorator) 패턴에서는 객체에 행동을 추가하지만, 프록시(Proxy)패턴에서는 접근을 제어합니다.