따봉도치야 고마워

Head First Design Patterns : (11)프록시 패턴 본문

프로그래밍/공부

Head First Design Patterns : (11)프록시 패턴

따봉도치 2020. 10. 5. 18:40

프록시 패턴

  • 어떤 객체에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴

  • 원격 객체 / 생성하기 힘든 객체 / 보안이 필요한 객체에 대한 대변자 객체 생성

  • 프록시 패턴의 수많은 변종 - "접근을 제어하는 방법"에서 차이가 있음

    1. 원격 프록시를 사용해 원격 객체에 대한 접근 제어

    2. 가상 프록시를 사용해 생성하기 힘든 자원에 대한 접근 제어

    3. 보호 프록시를 사용해 접근 권한이 필요한 자원에 대한 접근 제어

 

 

 

프록시

 

  • 원격 객체에 대한 로컬 대변자

  • 원격 객체 : 다른 JVM에 있는 객체 ex) 뽑기기계

  • 로컬 대변자 : 로컬 대변자의 메소드를 호출하면 원격 객체의 메소드를 호출하는 것처럼 행동

 

 

=

1. 클라이언트는 원격 서비스에 있는 메소드를 호출한다고 생각하고 작업 처리
2. 실제로는 클라이언트 보조 객체(프록시)가 서버에 연락을 취하고, 메소드에 대한 정보를 전달 및 서버 응답을 기다림.
3. 서버 쪽에선 서비스 보조 객체가 있어 요청을 받고 해석해 실제 서비스 객체에 있는 메소드 호출, 리턴 값 전송

 

 

 


자바 RMI

  • 클라이언트 서비스 보조 객체를 만들어줌(stub) + 서비스 보조객체(skeleton)

  • 직접 네트워킹 및 입출력 관련 코드를 작성하지 않아도 됨

  • 클라이언트에서 원격 객체에 접근할 수 있는 룩업(lookup)서비스 제공

  • 클라이언트 입장에선 로컬 메소드 호출과 똑같은 방식으로 사용하면 되지만, 실제론 네트워킹 및 입출력 기능이 활용되고 있으므로 예외처리 중요

 


원격 서비스 만들기

 

  1. 원격 인터페이스 생성

    • 클라이언트에서 원격으로 호출할 수 있는 메소드를 정의
    • 해당 인터페이스를 서비스의 클래스 형식으로 사용 -> 추후 스터브와 서비스가 모두 구현
  2. 서비스 구현 클래스 생성

    • 실제 작업을 하는 클래스

    • 1번의 인터페이스를 구현 ex) GumballMachine

  3. rmic를 이용해 스터브와 스켈레톤 만들기

    • rmic 툴을 사용해(JDK에 포함) 두 개의 클래스 생성 - stub, skeleton

    • ex) rmic GumballMachine

  4. RMI 레지스트리 실행

    • rmiregistry 명령어 실행

    • 클라이언트는 해당 레지스트리로부터 프록시를 받아갈 수 있음

  5. 원격 서비스 시작

    • 서비스를 구현한 클래스의 인스턴스를 만들고 RMI 레지스트리에 등록

    • 등록 후엔 클라이언트에서 사용 가능


EX1) 원격 인터페이스 생성

  • java.rmi.Remote 를 상속하는(확장) 원격 인터페이스 정의
    • 해당 인터페이스에서 원격 호출을 지원할 것임을 알려주는 역할
  • 모든 메소드를 RemoteException을 던지는 메소드로 선언
    • 네트워킹, 입출력 작업 중 생길 수 있는 예외 처리를 위함
  • 인자와 리턴값은 반드시 원시형식(primitive) 또는 Serializable 형식으로 선언
    • 메소드의 인자들은 네트워크를 통해 전달되어 직렬화를 통해 포장될 것
    • 즉, 원시 형식이 아닌 직접 만든 형식을 사용한다면 클래스를 만들 때 Serialize 인터페이스 구현

2. 서비스 구현 클래스 생성

  • 원격 인터페이스를 구현, UnicastRemoteObject 상속
    • 원격 서비스 객체 역할을 하려면 필요한 기능을 추가해야함
    • 가장 간단한 방법이 java.rmi.server 패키지에 있는 UnicastRemoteObejct 상속
  • RemoteException을 선언하는 인자없는 생성자 만들기
    • 수퍼 클래스인 UnicastRemoteObject 생성자에서 RemoteException를 던지기 때문에 똑같은 생성자 필요
  • 서비스를 RMI 레지스트리에 등록
    • java.rmi.Naming 에 있는 rebind() 메소드를 통해 등록
    • RMI 레지스트리를 통해 서비스를 검색할 때 여기서 등록한 이름으로 사용
try{
    MyRemote service = new MyRemoteImpl();
    Naming.rebind("RemoteHello", service);
}catch(Exception e){}

 

3. rmic를 돌려 스터브와 스켈레톤 생성

  • 원래 클래스 이름에 _Skel, _Stub가 붙어 생성됨

4. rmiregistry 실행 - 터미널을 새로 띄워 클래스에 접근할 수 있는 디렉토리에서 실행 (classes)

 

5. 서비스 가동

 

6. 클라이언트에서 스터브 객체 찾기 (lookup)

  • lookup 메소드를 통해 레지스트리에서 스터브 객체 리턴 받음

  • 스터브에 대해 메소드 호출

MyRemote service = (MyRemote)Naming.lookup("rmi://127.0.0.1/RemoteHello");
//항상 서비스 형식을 원격 인터페이스 형식으로 (MyRemote)
//서비스가 돌아가고 있는 IP주소/호스트 주소 + 등록시 지정한 이름

 

 

+

클라이언트에서 스터브 클래스를 가져오는 법

1. 간단한 시스템은 그냥 복사해도됨

2. 동적 다운로딩(dynamic class downloading)

: 직렬화된 객체에 클래스 파일 위치 URL이 내장됨.

역직렬화 과정에서 로컬 시스템에서 해당 클래스 파일을 찾지 못하면 URL로 GET 요청을 보내 가져옴

 


 

EX2) 뽑기 기계 원격 조정

- 해당 예제는 스테이트 패턴과 이어지는 코드입니다 (bb-dochi.tistory.com/83)

 

1. GumballMachine용 원격 인터페이스 정의

import java.rmi.*;
//원격 인터페이스
public interface GumballMachineRemote extends Remote{
    public int getCount() throws RemoteException;
    public String getLocation() throws RemoteException;
    public State getState() throws RemoteException;
}

 

2. 인터페이스의 모든 형식이 직렬화 되는지 확인

  • State 형식은 직렬화가 안되므로 수정이 필요함

    • Serializable 상속해 해결

  • 또한 State 클래스에 GumballMachine에 대한 레퍼런스가 들어있는데, 이를 포함해 직렬화하는 것은 바람직하지 않음

    • 각 State에 있는 GumballMachine 인스턴스 변수에 transient 키워드 추가

    • 이렇게 하면 해당 필드는 직렬화되지 않음 + 하지만 직렬화해 받은 뒤 호출하게 되면 문제가 발생할 수 있음

//Serializable 인터페이스를 확장하면 직렬화 가능
import java.io.*;

public interface State extends Serializable{
	.
    .
}



public class NoQuarterState implements State{
    //transient 키워드로 해당 필드는 직렬화 되지 않도록 함
    transient GumballMachine gumballMachine;
    .
    .
}

 

3. 구상 클래스에서 인터페이스를 구현

  • GumballMachine이 서비스 역할과 네트워크 요청을 처리할 수 있도록 수정

import java.rmi.*;
import java.rmi.server.*;

public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote{
    .
    .
    public GumballMachine(String location, int numberGumballs) throws RemoteException{
        //수퍼클래스에서 RemoteException 예외를 던질 수 있기 때문에
        //해당 생성자에서도 던질 수 있도록 수정
    }
    .
    .

 

4. RMI 레지스트리 등록

5. 클라이언트 코드 수정

  • GumballMachine -> GumballMachineRemote

  • 뽑기 기계의 인스턴스를 만들 땐 항상 try/catch문으로 감싸야함

 


가상프록시

  • 생성하는데 많은 비용이 드는 객체를 대변, 실제로 진짜 객체가 필요하기 전까지 객체 생성을 미루는 기능 제공

  • 객체 생성 전, 또는 생성 도중에 객체를 대신하기도 함

  • 객체 생성이 완료 되면 그냥 RealSubject에 요청을 직접 전달

+ 원격프록시 : 다른 JVM에 있는 객체의 대변인, 프록시 메소드 호출 시 네트워크를 통해 원격 객체의 메소드 호출

 

EX) CD 커버 뷰어

: CD 커버를 보여주는 뷰어를 만들 때, 네트워크 상태와 연결 속도에 따라 이미지를 가져오는데 시간이 걸릴 수 있으므로 기다리는 동안 화면에 다른 것을 보여주고, 전체 어플리케이션이 멈추지 않도록 하는 가상 프록시를 만들어보자

 

 

  1. ImageProxy에선 우선 ImageIcon을 생성하고, 네트워크 URL로부터 이미지 불러옴
  2. 이미지 로딩 도중 간단한 메시지 화면에 표시
  3. 로딩이 완료되면 모든 메소드 호출을 ImageIcon 객체에 넘김
  4. 사용자가 새로운 이미지를 요청하면 프록시를 새로 만들고 위 과정을 진행
class ImageProxy implements Icon {
    ImageIcon imageIcon;
    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() {
        if (imageIcon != null) {
            return imageIcon.getIconHeight();
        } else {
            return 600;
        }
    }
     
    //이미지 아이콘 객체가 없다면 이미지를 가져오고
    //간단한 메시지를 출력해줌
    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 CD cover, please wait...", x+300, y+190);
            if (!retrieving) {
                retrieving = true;

                retrievalThread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            imageIcon = new ImageIcon(imageURL, "CD Cover");
                            //이미지가 완전히 로딩되어야 생성자에서 객체 리턴해줌
                            c.repaint();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                retrievalThread.start();
            }
        }
   }
}

 

//ImageProxyTestDrive.java
Icon icon = new ImageProxy(initialURL);

 

 

Q. 원격 프록시와 가상 프록시는 완전히 다른 것 같은데?

  • 클라이언트에서 실제 객체의 메소드를 호출하면, 그 호출을 중간에 가로채 접근 제어한다는 점에서 같음 -> 프록시 패턴

 

Q. ImageProxy 는 데코레이터로 볼 수 있지 않나?

  • 비슷해 보일 수 있지만 용도의 차이
  • 데코레이터는 클래스에 새로운 행동을 추가하기 위한 용도지만, 프록시는 접근 제어 용도
  • ImageProxy가 클라이언트와 ImageIcon을 분리시키면서 이미지 로딩 전에도 다른 작업들이 가능하게끔 해줌

 

Q. 어떤 식으로 클라이언트에서 진짜 객체가 아닌 프록시를 사용하도록 만드는지

  • 객체의 인스턴스를 생성해서 리턴하는 팩토리를 사용
  • 팩토리 메소드 내에서 진행되기 때문에 실제 객체를 프록시로 감싼 다음 리턴 할 수 있음

 

Q. ImageProxy는 이미지를 받아올 때마다 ImageIcon 객체를 새로 생성하던데 전에 가져왔던 이미지에 대해 캐싱은?

  • 가상 프록시의 변종인 캐싱 프록시를 사용해 캐시에 저장해뒀다 상황에 따라 리턴할 수 있음

 

Q. 프록시와 어댑터 패턴의 차이

  • 둘다 클라와 다른 객체 사이에서 전달해주는 역할을 하지만, 어댑터는 다른 객체의 인터페이스를 바꿔주고, 프록시는 똑같은 인터페이스 사용
  • 보호 프록시는 '상황에 따라 객체에 있는 특정 메소드에 대한 접근을 제어, 프록시에서 클라에게 인터페이스의 일부만 제공'할 수 있다는 점이 좀 더 유사함

 


자바 API를 이용한 보호프록시

  • java.lang.reflect 패키지에 프록시 기능이 내장되어있음
  • 이 패키지를 통해 즉석에서 한 개 이상의 인터페이스를 구현하고 메소드 호출을 지정한 클래스로 전달하는 프록시를 생성 가능
  • 실제 프록시 클래스는 실행중에 생성되므로 이런 기술을 '동적 프록시(dynamic proxy)' 라고 함

 

동적 프록시 다이어그램

 

- Proxy 클래스는 자바가 만들어주기 때문에 필요한 코드는 InvocationHandler에 넣어주면 됨

- InvocationHandler는 프록시에 대한 모든 호출에 응답하는 역할

 

 

EX) 결혼 정보 업체

: 결혼정보 업체에서 고객들의 정보와 선호도를 포함한 클래스를 생성함. getter/setter에 대한 보호가 없기 때문에 타인의 정보를 수정하거나, 자신의 선호도를 직접 수정하는 등의 문제가 발생함. 보호프록시를 사용해보자!

 

1. InvocationHandler 생성 - 프록시의 행동을 구현

  • 2 가지 (본인을 위한, 타인을 위한) 호출 핸들러를 만들어야함

+ 호출 핸들러 ?

  • 프록시의 메소드가 호출되면 프록시에선 그 호출을 핸들러에 넘김.
  • 핸들러엔 invoke() 메소드 밖에 없고 어떤 메소드를 호출하든 invoke()만 실행
  • 주어진 요청을 어떻게 처리할지 결정한 다음 상황에 따라 RealSubject에 요청 전달
1) 프록시의 setHotOrNotRating() 메소드 호출
proxy.setHotOrNotRating(9);

2) 프록시에선 InvocationHandler의 invoke() 호출
invoke(Object proxy, Method method, Object[] args)
//리플렉션 API getName()을 통해 어떤 메소드를 호출했는지 알 수 있음

3) 핸들러에선 주어진 요청을 어떻게 처리할지 결정 후 RealSubject에 요청 전달
return method.invoke(person, args);
//프록시에 대해 호출되었던 진짜 메소드 호출
//인자 값으로 진짜 객체와, 처음에 받은 인자 전달
//본인 호출에 대한 핸들러 
public class OwnerInvocationHandler implements InvocationHandler { 
//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);
            //setHotOrNot() 메소드인 경우 거부
            } 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;
    }
}

 

2. 동적 프록시를 생성하는 코드 작성

//PersonBean 객체를 인자로 받고, 프록시를 리턴함
PersonBean getOwnerProxy(PersonBean person){
    return (PersonBean) Proxy.newProxyInstance(
        person.getClass().getClassLoader(),
        person.getClass().getInterfaces(),
        new OwnerInvocationHandler(person));
    //newProxyInstance()로 프록시를 생성
    //프록시에서 구현해야하는 클래스로더, 인터페이스를 인자로 전달
    //호출핸들러도 전달, 핸들러의 인자로 person을 전달
}

 

3. 클라이언트 테스트

PersonBean joe = getPersonFromDatabase("Joe Javabean"); 
PersonBean ownerProxy = getOwnerProxy(joe);

//ownerProxy 객체론 자신의 선호도 점수 수정이 불가능

 

 

Q. 어떤 클래스가 Proxy 클래스인지 알 수 있는지?

  • Proxy 클래스에는 isProxyClass() 라는 정적 메소드가 있음. (이를 제외하면 실제 클래스와 동일)

 

Q. newProxyInstance()를 호출해 인자로 전달할 수 있는 인터페이스 형식에 제한이 있는지

  • 일단 항상 인터페이스의 배열을 인자로 전달해야 함. 클래스는 x
  • 또한 public으로 지정되지 않은 인터페이스의 경우 같은 패키지 내 인터페이스만 가능.
  • 같은 이름을 가진 인터페이스를 여러개 전달하는 것도 불가능 + 등등

 

Q. 왜 스켈레톤을 사용해야 하는지?

  • 자바 1.2부터는 RMI 런타임에서 클라이언트 호출을 리플렉션을 이용해 직접 원격 서비스로 넘길 수 있게 되었음
  • 여기선 스터브와 원격서비스 간 통신 메커니즘 설명을 위해 사용

 

Q. 자바5부턴 스터브도 필요가 없다는데?

  • 자바5부터 RMI와 동적 프록시가 결합해 스터브마저도 동적으로 생성됨
  • java.lang.reflect.Proxy 인스턴스로 자동으로 생성 -> rmic  사용 안해도됨

 

 

 


 

 

기타 변종 프록시

 

방화벽 프록시

  • 일련의 네트워크 자원에 대한 접근을 제어
  • 주 객체를 나쁜 클라이언트로부터 보호
  • ex. 기업용 방화벽 시스템

 

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

  • 주 객체가 참조될 때마다 추가 행동을 제공
  • ex. 객체 참조에 대한 선/후 작업

 

캐싱 프록시(Caching Proxy)

  • 비용이 많이 드는 작업의 결과를 임시로 저장함
  • 여러 클라에서 결과를 공유하게 해줌으로 계산 시간 또는 네트워크 지연을 줄여주는 효과
  • ex. 웹 서버 프록시 or 컨텐츠 관리 및 퍼블리싱 시스템

 

동기화 프록시(Synchronization Proxy)

  • 여러 스레드에서 주 객체에 접근하는 경우에 안전하게 작업을 처리할 수 있게 해줌
  • ex. 분산 환경에서 일련의 객체에 대한 동기화 된 접근을 제어해주는 자바 스페이스

 

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

  • 복잡한 클래스들의 집합에 대한 접근을 제어하고 복잡도를 숨겨줌
  • 퍼사드 프록시(Facade Proxy)라고 부르기도 함.
  • 이 프록시와 퍼사드 패턴의 차이는 프록시는 접근을 제어하지만, 퍼사드는 대체 인터페이스만 제공한다는 점

 

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

  • 클라이언트에서 필요로 할 때까지 객체가 복사되는 것을 지연시킴으로 객체의 복사를 제어
  • 변형된 가상 프록시
  • ex. 자바5의 CopyOnWriteArrayList

 

 

Comments