이번에는 프록시 패턴의 활용 예제를 살펴보겠다.

앞서 포스팅한 내용 중에 가상 프록시(Virtual Proxy), 보호 프록시(Protection Proxy) 에 대해 

간단히 언급했는데 하나씩 자세히 살펴보겠다.


그 전에 Proxy 패턴의 클래스 다이어그램은 중요하니 다시 복습하자.


앞에서 포스팅했던 다이어그램 그림과는 조금 차이가 있지만 결국 같은 말이다.



1. 가상 프록시 (Virtual Proxy)

- 생성하기 힘든 자원에 대한 접근을 제어할 수 있다.


여기서 생성하기 힘든 자원이란 예를 들어 Image 같이 객체 생성 시간이 긴 녀석들을 의미한다.

예제 코드로는 이미지 뷰어를 만들어 볼 것이다. 


가상 프록시가 백그라운드에서 서버로부터 이미지를 불러들이는 동안 

화면에서는 로딩중이라는 메시지를 보여주게 할 것이다.


설계는 다음과 같다.


Icon 인터페이스와 ImageIcon 클래스는 javax.swing 을 이용할 것이다.


그러면 ImageProxy 코드를 보자. 앞서 먼저 본 원격 프록시(Remote Proxy)와 헷갈리지 말자.

class ImageProxy implements Icon
{
    ImageIcon imageIcon;    // RealSubject
    URL imageURL;
    Thread retrievalThread;
    boolean retrieving = false;

    public ImageProxy(URL url) { imageURL = url; }

    public int getIconWidth() {
        if ( imageIcon != null ) { return imageIcon.getIconWidth(); }
            else { return 800; }
    }

    public int getIconHeight() { // getIconWidth() 와 비슷 }

    public void paintIcon(final Component c, Graphics g, int x, int y) {
        if ( imageIcon != null ) {
            imageIcon.paintIcon(c, g, x, y);
        } else {
            g.drawString("Loading Image, please wait...", x+300, y+200);
            if ( !retrieving ) {
                retrieving = true;
                retrievalThread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            imageIcon = new ImageIcon(imageURL, "Image");
                            c.repaint();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                retrievalThread.start();
            }
        }
    }
}

메소드 별로 if, else 문으로 나뉘는데 10장에서 본 State 패턴을 이용해

좀 더 깔끔한 코드를 만들 수 있다.


다음은 ImageProxy 를 감싸줄 Component 와 테스트 코드이다.

class ImageComponent extends JComponent 
{
    private Icon icon;

    // ImageComponent(Icon icon), setIcon() 생략

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        // 기타 인자값 생략
        icon.paintIcon(this, g, x, y);
    }
}
public class ImageProxyTestDrive 
{
    ImageComponent imageComponent;
    public static void main(String[] args) throws Exception {
        ImageProxyTestDrive testDrive = new ImageProxyTestDrive();
    }

    public ImageProxyTestDrive() throws Exception {
        // 프레임 및 기타 설정 코드 생략

        Icon icon = new ImageProxy(initialURL);
        imageComponent = new ImageComponent(icon);
        frame.getContextPane().add(imageComponent);
    }
}


Q. 전에 가져왔던 이미지를 캐시에 저장해 두는 ImageProxy 와 비슷한 객체를 구현할 수 있지 않나?

- 가상 프록시의 변종 가운데 하나인 캐싱 프록시(Caching Proxy) 라는 것이 있다.

캐싱 프록시는 기존에 생성했던 객체들을 캐시에 저장해 뒀다가 재요청이 들어왔을 때,

상황에 따라 캐시에 저장해두었던 객체를 리턴할 수 있다.


Q. 원격 프록시와 가상 프록시가 같은 패턴에 속하는 게 맞나?

- 프록시 패턴는 다양하게 활용된다. 활용된 그 모든 녀석들의 공통점은

'클라이언트에서 실제 객체의 메소드를 호출하면, 그 호출을 중간에 가로챈다'는 점이다.


이렇게 간접적으로 작업을 처리하면 

요청 내역을 원격 시스템에 있는 객체에 전달할 수도 있고,

생성하는 데 많은 비용이 드는 객체를 대변해줄 수도 있고,

클라이언트 별로 호출할 수 있는 메소드를 제한하는 보디가드 역할을 하는 것도 가능하다.


이 장에서 배우는 프록시 패턴은 시작에 불과하다.

이제 보호 프록시에 대해 알아보고 그 외에 활용되는 방법들을 더 알아볼 것이다.



2. 보호 프록시 (Protection Proxy)

- 접근 권한이 필요한 자원에 대한 접근을 제어할 수 있다.


- Java 에는 java.lang.reflect 패키지에 프록시 기능이 내장되어 있다. 이 패키지를 이용하면

즉석에서 1개 이상의 인터페이스를 구현하고, 메소드 호출을 지정해 준 클래스에 전달할 수 있는

프록시 클래스를 만들 수 있다. 실제 프록시 클래스는 실행중에 생성되기 때문에 

이러한 자바 기술을 '동적 프록시(dynamic proxy)' 라고 부른다.


이번에는 이 동적 프록시를 활용해서 보호 프록시를 만들어 볼 것이다.

보호 프록시를 만들기 전에 동적 프록시가 어떤 식으로 돌아가는지 클래스 다이어그램을 보자.


Proxy 패턴의 공식적인 다이어그램과는 조금 차이가 있다.


동적 프록시의 경우, Proxy 클래스를 Java 에서 만들어 주기 때문에 Proxy 클래스에게

무슨 일을 할지 알려주기 위한 방법이 필요하다. 

하지만 Proxy 클래스를 직접 만들지 않기 때문에 필요한 코드를 InvocationHandler 에 집어넣어 

InvocationHandler 가 Proxy 에 대해서 호출되는 모든 메소드에 대해 응답하는 역할을 맡도록 한다.



예제로는 결혼 정보 서비스를 만들 것이다.

고객들이 서로 상대방에 대한 선호도 점수를 줄 수 있는 기능을 추가할 것이고,

이 서비스는 어떤 사람에 대한 정보를 가져오거나 설정할 수 있게끔 'PersonBean' 을 중심으로 삼는다.


본인이라면 당연히 내 정보를 설정할 수 있고, 타인이 내 정보를 설정하게 하는 건 불가해야 한다.

본인이라면 내 선호도 점수를 매기는 건 불가하고, 타인이라면 내 선호도 점수를 매길 수 있다.

여기서 접근 권한에 따른 제어가 필요하다는 것을 느낄 것이다.


먼저 Subject 인 'PersonBean' 과 RealSubject 인 'PersonBeanImpl' 부터 코드를 살펴보자.

public interface PersonBean 
{
    // String getName(), getGender(), getInterests() 선언
    int getHotOrNotRating();    // 선호도의 평균 점수를 리턴

    // void setName(), setGender(), setInterests() 선언
    void setHotOrNotRating(int rating);
}
public class PersonBeanImpl implements PersonBean 
{
    String name, gender, interests;
    int rating, ratingCount;

    // getName(), getGender(), getInterests(), getHotOrNotRating() 구현

    // setName(), setGender(), setInterests(), setHotOrNotRating() 구현
}


< PersonBean 용 동적 프록시 만들기 3 단계 >

① InvocationHandler 를 만든다.

- 프록시의 메소드가 호출되었을 때 실제 할 일을 지정해 주는 핸들러만 만들면 된다.

② 동적 프록시를 생성하는 코드를 작성한다.

- 프록시 클래스를 생성하고 그 인스턴스를 만들기 위한 코드를 말한다.

③ PersonBean 객체를 적절한 프록시로 감싼다.

- 적절함의 척도는 본인(Owner)이냐 본인이 아니냐(Non-Owner) 이다.

PersonBean 객체를 사용하고자 하는 객체에 따라 적절한 프록시를 생성해주자.



① InvocationHandler 를 만든다.

- 호출핸들러는 본인을 위한 핸들러, 타인을 위한 핸들러 이렇게 2개를 만든다.

- 이 핸들러에는 invoke() 하나 뿐이다. 코드 보기 전에 어떻게 돌아가는지 살펴보자.

1) proxy.setHotOrNotRating(10); 라고 프록시의 메소드가 호출되었다고 가정해보면,

2) 프록시는 핸들러의 invoke(Object proxy, Method method, Object[] args) 를 호출한다.

그러면 invoke() 에서는 1) 에서 호출된 메소드를 어떻게 처리할 것인지 구현하면 된다.

import java.lang.reflect.*;

public class OwnerInvocationHandler implements InvocationHandler 
{
    PersonBean person;

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

    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")) {
                throw new IllegalAccessException();
            } else if (method.getName().startsWith("set")) {
                return method.invoke(person, args);
            }
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}

위 코드는 보다시피 본인을 위한 핸들러이고, 타인을 위한 핸들러 NonOwnerInvocationHandler 는

try/catch 구문 안의 조건문만 조금 바꾸면 되니 생략하겠다.


② 동적 프록시를 생성하는 코드를 작성한다.

PersonBean getOwnerProxy(PersonBean person) 
{
    // 이 메소드에서는 RealSubject 를 인자로 받아오고, Proxy 를 리턴한다.
    // 'Proxy 의 인터페이스 = RealSubject 의 인터페이스' 이므로 리턴 타입은 PersonBean 이다.

    return (PersonBean) Proxy.newProxyInstance( 
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new OwnerInvocationHandler(person));
}

이렇게 newProxyInstance 의 마지막 인자 값만 바꿔서 getNonOwnerProxy() 메소드도 작성하면 된다.


③ PersonBean 객체를 적절한 프록시로 감싼다.

public class MatchMakingTestDrive 
{
    // 인스턴스 변수 선언

    public static void main(String[] args) {
        MatchMakingTestDrive test = new MatchMakingTestDrive();
        test.drive();
    }

    public MatchMakingTestDrive() {
        initializeDatabase();
    }

    public void drive() {
        PersonBean joe = getPersonFromDatabase("Joe JavaBean");

        // Owner 인 경우, 본인용 프록시 생성
        PersonBean ownerProxy = getOwnerProxy(joe);

        // ownerProxy.setHotOrNotRating() 대한 예외처리 필요

        // Non-Owner 인 경우, 타인용 프록시 생성
        PersonBean nonOwnerProxy = getNonOwnerProxy(joe);

        // nonOwnerProxy.setHotOrNotRating() 를 제외한 setter 메소드에 대한 예외처리 필요
    }

    // getOwnerProxy(), getNonOwnerProxy() 같은 메소드
}

위 코드는 테스트 코드이니 경우에 따라 어떤 프록시를 생성할 것인지만 살펴보면 된다.



Q. 동적 프록시에서 어느 부분이 '동적'이라는 거지?

- 프록시가 동적이라고 한 이유는 클래스가 실행중에 생성되기 때문이다.

실제로 코드가 실행되기 전까지는 프록시 클래스 자체가 없다. 

전달받은 인터페이스를 바탕으로 즉석에서 클래스가 생성된다. ->  단계 코드 참고


Q. InvocationHandler 는 특이한 프록시인건가?

InvocationHandler 자체는 프록시가 아니다. 

메소드 호출을 처리하기 위해 프록시에서 활용하는 클래스일 뿐이다.


Q. 어떤 클래스가 Proxy 클래스인지 알아낼 방법이 있나?

- Proxy 클래스에는 isProxyClass() 라는 정적 메소드가 있다. 

동적 프록시 클래스에 대해서 이 메소드를 호출하면 true 라고 리턴될 것이다.


Q. 왜 스켈레톤(Skeleton) 을 사용해야 되나?

- Java 1.2 부터는 RMI 런타임에서 클라이언트 호출을 reflection 을 이용해 직접

원격 서비스로 넘길 수 있게 되어서 사실 스켈레톤은 안 써도 된다.

하지만 이 장에서는 메커니즘을 이해하는 데 도움이 되기 때문에 스켈레톤을 언급했다.


Q. Java 5 부터는 스터브(Stub) 도 더 이상 만들 필요가 없다고 하는데 정말인가?

- 그렇다. Java 5 부터는 RMI 와 동적 프록시가 결합되어서 스터브 마저도 동적 프록시를 통해

동적으로 생성된다. 따라서 'rmic' 을 전혀 쓰지 않아도 된다. 원격 객체의 메소드를

호출하고 그 결과를 리턴받는 작업이 전부 자동으로 처리되기 때문이다.



3. 기타 프록시 패턴의 활용

- 방화벽 프록시

일련의 네트워크 자원에 대한 접근을 제어함으로써 주 객체를 '나쁜' 클라이언트들로부터 보호해준다. 

기업용 방화벽 시스템에서 자주 쓰인다.

- 스마트 레퍼런스 프록시 (Smart Reference Proxy) :

주 객체가 참조될 때마다 추가 행동을 제공한다. ex) 객체에 대한 레퍼런스 갯수를 카운트.

- 캐싱 프록시 (Caching Proxy) :

비용이 많이 드는 작업의 결과를 임시로 저장해준다. 

여러 클라이언트에서 결과를 공유하게 해 줌으로써 계산 시간 or 네트워크 지연을 줄여주는 효과가 있다. 

웹 서버 프록시 또는 컨텐츠 관리 및 퍼블리싱 시스템에서 자주 쓰인다.

- 동기화 프록시 (Synchronization Proxy) :

여러 스레드에서 주 객체에 접근하는 경우에 안전하게 작업을 처리할 수 있게 해준다.

분산 환경에서 일련의 객체에 대한 동기화 된 접근을 제어해주는 자바 스페이스에서 쓰인다.

- 복잡도 숨김 프록시 (Complexity Hiding Proxy) :

복잡한 클래스들의 집합에 대한 접근을 제어하고, 복잡도를 숨겨준다. 

'퍼사드 프록시(Facade Proxy)' 라고 부르기도 한다.

- 지연 복사 프록시 (Copy-On-Write Proxy) :

클라이언트에서 필요로 할 때까지 객체가 복사되는 것을 지연시킴으로써 객체의 복사를 제어한다.

'변형된 가상 프록시'라고 할 수 있다.

Java 5 의 CopyOnWriteArrayList 에서 쓰인다.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).

7) 친한 친구들하고만 이야기한다(최소 지식 원칙).

8) 먼저 연락하지 마세요. 저희가 연락 드리겠습니다(헐리우드 원칙).

9) 어떤 클래스가 바뀌게 되는 이유는 한 가지 뿐이어야만 한다(단일 역할 원칙).



* 이 장에서의 정리 :

- Proxy Pattern 을 이용하면 어떤 객체에 대한 대변인을 내세워서 클라이언트의 접근을 제어할 수 있다.

- 원격 프록시는 클라이언트와 원격 객체 사이의 데이터 전달을 관리해준다.

- 가상 프록시는 인스턴스를 생성하는 데 비용이 많이 드는 객체에 대한 접근을 제어한다.

- 보호 프록시는 호출한 쪽의 권한에 따라 객체에 있는 메소드에 대한 접근을 제어한다.

- Decorator 패턴에서는 객체에 행동을 추가하지만, Proxy 패턴에서는 접근을 제어한다.

- 다른 Wrapper 를 쓸 때와 마찬가지로 프록시를 쓰면 디자인에 포함되는 클래스와 객체의 수가 늘어난다.



※ Proxy 의 사전적 의미 : 

1) 대리[위임](권)    2) 대리인    3) 대용물



# Pattern 짧게 복습 #

- Decorator : 다른 객체를 감싸서 새로운 행동을 추가해준다.

- Facade : 여러 객체를 감싸서 인터페이스를 단순화 시킨다.

- Adapter : 다른 객체를 감싸서 다른 인터페이스를 제공한다.

- Proxy : 다른 객체를 감싸서 접근을 제어한다.



이번 포스팅도 길었다, 휴 힘들어ㅡ_ㅡ


(이미지 출처 : http://www.jamessugrue.ie/softwaredev/design-patterns-uncovered-the-proxy-pattern)


by kelicia 2014. 6. 2. 00:04