이번 장도 방대한 양을 자랑하는 덕분에^^.. 개념/활용으로 나눠서 포스팅하겠다.


10장에서 보았던 상황 설정에 이어서 추가 요청이 들어왔다고 한다.

각지에 설치된 껌볼기계에 대한 위치, 재고, 상태(State) 를 클라이언트에서 

모니터링 할 수 있도록 설계해야 된다고 한다.


우선 모니터링용 코드를 작성해보자.

public class GumballMachine 
{
    // 기타 인스턴스 변수
    String location;

    public GumballMachine(String location, int count) {
        // 기타 생성자 코드
        this.location = location;
    }

    public String getLocation() { return location; }

    // 기타 메소드
}
public class GumballMonitor
{
    GumballMachine machine;

    public GumballMonitor(GumballMachine machine) {
        this.machine = machine;
    }

    public void report() {
        // machine 객체의 Getter 메소드 이용해 위치, 재고, 상태 출력
    }
}


이제 각지의 껌볼기계에 있는 객체와 통신을 해서 현황에 대한 정보를 받아야 되는데,

이를 위해 '원격 프록시' 를 이용할 것이다.


위 그림에서 Local Heap 안의 Gumball Monitor 가 Client,

Remote Heap 안의 Gumball Machine 이 원격 객체(Remote object)에 해당한다.


Client 객체에서는 원격 객체의 메소드를 호출하는 것처럼 행동하지만

실제로는 Local Heap 의 프록시(Proxy) 객체의 메소드를 호출한다.


네트워크 통신과 관련된 저수준 작업은 이 프록시 객체에서 처리해준다.


'원격 프록시는 원격 객체에 대한 로컬 대변자 역할을 한다'고 생각하면 된다.



나는 '그럼 통신 관련 코드도 짜야되는 거임? 제길ㅡㅡ?' 이라고 생각했지만

JAVA 에 내장되어있는 원격 호출 기능인 RMI 을 활용한다고 한다.


'RMI' 란 'Remote Method Invocation' 의 약자로 번역하면 '원격 메소드 호출' 이다.

그러면 원격 메소드의 기초와 RMI에 대해 알아보자.


지저분하게 설명이 붙어있긴 하지만 하나씩 보겠다.

우선 원격 메소드의 기초를 이해하기 위해 점선 화살표부터 보자.


호출 과정은 다음과 같다. (보조 객체는 Helper 라고 생각하면 된다)


① 클라이언트 객체에서 클라이언트 보조 객체의 어떤 메소드를 호출한다.

② 클라이언트 보조 객체에서는 메소드 호출에 대한 정보(메소드 이름, 인자 등)을 잘 포장해서

네트워크를 통해 서비스 보조 객체에게 이를 전달한다.

③ 서비스 보조 객체에서는 전달받은 정보를 해석해 어떤 메소드를 호출할 지 알아낸 다음,

서비스 객체의 진짜 메소드를 호출한다.

④ 서비스 객체의 메소드가 호출되고 실행이 끝나면 서비스 보조 객체에게 결과를 리턴한다.

⑤ 서비스 보조 객체는 결과를 잘 포장해서 네트워크를 통해 클라이언트 보조 객체에게 전달한다.

⑥ 클라이언트 보조 객체는 이를 해석해 클라이언트 객체에게 리턴한다.

클라이언트 객체 입장에서는 메소드 호출이 어디로 전달되고 어디에서 왔는지 전혀 알 수 없다.


RMI 에서는 위 그림에서 클라이언트 보조 객체, 서비스 보조 객체를 만들어준다.

또한 클라이언트에서 원격 객체를 찾아서 그 원격 객체에 접근하기 위해 쓸 수 있는

룩업(Lookup) 서비스도 제공해준다.


그러니 네트워크 및 입출력 관련 코드를 직접 작성할 필요는 없다.

하지만 네트워크나 입출력 기능을 쓸 때는 위험이 따르는 법이니 예외 처리에 대비해야 한다는 걸 명심하자.


RMI 용어 : 클라이언트 보조 객체는 스터브(Stub), 서비스 보조 객체는 스켈레톤(Skeleton) 이라 부른다.



<< 원격 서비스 만들기 5 단계 >>

1) 원격 인터페이스 만들기 : ex) MyService.java

- 클라이언트에서 원격으로 호출할 수 있는 메소드를 정의한다.

- 스터브와 실제 서비스에서는 모두 이 인터페이스를 구현해야 한다.

2) 서비스 구현 클래스 만들기 : ex) MyServiceImpl.java

- 실제 작업을 처리하는 클래스이다. 1)에서 정의한 원격 메소드를 실제로 구현한 코드가 있는 부분.

3) rmic 를 이용해 스터브와 스켈레톤 만들기 : ex) MyServiceImpl_Stub.java, MyServiceImpl_Skel.java

- 클라이언트 및 서비스 보조 객체를 생성한다.

- 터미널에서 'rmic MyServiceImple(2)에서 작성한 클래스)' 라고 명령어만 치면 끝.

4) RMI 레지스트리(Registry) 를 실행

- 터미널에서 클래스에 접근할 수 있는 디렉토리로 이동한 후에 'rmiregistry' 명령어 실행 

(단, 3)에서 입력한 명령어와 다른 터미널에서 실행)

5) 원격 서비스 시작 :

- 다른 터미널을 열고 원격 서비스의 객체 인스턴스를 만든 클래스를 실행시킨다.

명령어는 잘 알다시피 'java (클래스이름)'



위 5 단계를 따라서 GumballMachine 에 적용해보겠다.


1. 원격 인터페이스 만들기

import java.rmi.*;

public interface GumballMachineRemote extends Remote {
    public int getCount() throws RemoteException;
    public String getLocation throws RemoteException;
    public State getState() throws RemoteException;
}

주의할 점은 원격 메소드의 인자 및 리턴값은 반드시 Primitive 형식 or Serializable 형식 이어야 한다.

Primitive 형식이나 String 또는 API 에서 많이 쓰이는 형식(배열, 컬렉션 등)은 괜찮지만

직접 만든 형식일 경우 Serializable 인터페이스를 구현해야 한다.


원격 메소드의 인자는 모두 직렬화(Serializable)를 통해 포장된 다음 네트워크를 통해 전달된다.

리턴 값도 마찬가지이다.


그러므로 위 코드에서 State 라는 형식이 있으니 약간 수정이 필요하다.

import java.io.*;

public interface State extends Serializable {
    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();
}

참 쉽죠잉?


하지만 하나 더 고려해야할 점이 있다.

모든 State 객체에는 뽑기 기계(GumballMachine)에 대한 레퍼런스가 들어있는데

State 객체가 리턴될 때 이 뽑기 기계까지 직렬화해서 보내는 건 바람직하지 못하다.

그러니 다음과 같이 해주자.

public class NoQuarterState implements State 
{
    transient GumballMachine gumballMachine;

    // 기타 메소드
}

'transient' 라는 키워드를 붙이면 JVM 에서는 이 필드를 직렬화하지 않는다.

이 키워드를 붙였는데 억지로 저 필드를 호출하면 좋지 못할 일이 생기니 기억해두자.



2. 서비스 구현 클래스 만들기


GumballMachine 이미 10장에서 구현했지만 서비스 역할과 네트워크를 통해 들어온 요청을

처리하기 위해 약간 수정해주자.

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

public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote
{
    // 인스턴스 변수들

    public GumballMachine(String location, int numberGumballs) throws RemoteException {
        // 생성자 코드
    }

    // getCount(), getState(), getLocation() 등 기타 메소드
}

원격 객체가 되기 위한 'UnicastRemoteObejct' 를 확장하였지만 주의해야 할 점이 있다.

UnicastRemoteObject 의 생성자에서 RemoteException 을 던진다.

어떤 클래스가 생성될 때 그 수퍼클래스의 생성자도 반드시 호출되기 때문에

서브클래스의 입장에서도 RemoteException 을 선언해야 한다.


그리고 클라이언트에서 인터페이스에 있는 메소드를 호출할테니

GumballMachineRemote 를 구현해주자.


뽑기 기계의 원격 서비스 부분은 다 끝났지만 클라이언트에서 이 서비스를 쓸 수 있도록

서비스의 객체 인스턴스를 만든 후, RMI 레지스트리에 등록하기만 하면 된다.


서비스를 구현한 객체를 등록하면 RMI 시스템에서는 스터브만 레지스트리에 등록한다.

클라이언트에서는 스터브만 필요하기 때문이다.


레지스트리 등록을 처리해주는 간단한 테스트용 클래스(GumballMachineTestDrive.java)를 만든 후 

메인함수에 다음과 같은 코드를 넣어주자.

try {
    gumballMachine = new GumballMachine(args[0], count);    //args[0] 은 Location 정보
    Naming.rebind("(서비스 이름)", gumballMachine);
} catch (Exception e) {
    e.printStackTrace();
}

try/catch 구문으로 감싼 이유는 객체 인터스턴스를 생성할 때 생성자에서

예외를 던질 수 있으니(RemoteException) 감싸주었다.


그리고 Naming.rebind() 를 살펴보면,

클라이언트는 '서비스 이름' 을 써서 레지스트리를 검색하기 때문에 이름을 지정해주어야 한다.

주의해야 할 점은 레지스트리 등록 코드가 들어있는 클래스가 실행될 때 

(원격 서비스 만들기의 4단계에 해당되는) RMI 레지스트리가 돌아가고 있어야 한다는 점이다.


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


터미널에서 rmic GumballMachine 을 때려주면 JDK 에 포함되어 있는 rmic 툴이 

서비스를 구현한 클래스를 받아서 스터브와 스켈레톤 이렇게 2개의 클래스를 생성해준다.


4. RMI 레지스트리 실행


터미널에서 rmiregistry 명령어를 입력해주자.


5. 원격 서비스 시작


터미널에서 java GumballMachineTestDrive seattle.mightygumball.com 100 라고 입력해주면 된다.

'GumballMachineTestDrive' 은 테스트용 코드 작성한 클래스 이름이고,

'seattle.mightygumball.com' 은 뽑기 기계가 위치한 장소, '100' 은 껌볼 갯수 입력해준 것이니 신경끄자.



휴, 여기까지가 서버측에서 해야할 일이다.

이제 GumballMonitor 클라이언트를 살펴보자.

import java.rmi.*;

public class GumballMonitor
{
    GumballMachineRemote machine;

    public GumballMonitor(GumballMachineRemote machine) {
        this.machine = machine;
    }

    public void report() {
        try {
            // machine 객체의 Getter 메소드 이용해 위치, 재고, 상태 출력
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1) GumballMachine 구상 클래스 대신 원격 인터페이스를 사용했다.

2) RemoteException 클래스를 사용해야 되서 rmi 를 import 했고, machine 객체의 Getter 메소드를 

네트워크를 통해 호출해야 하므로 RemoteException 에 대비해서 try/catch 구문으로 잡아주었다.



서버와 클라이언트 모두 준비 완료다.

평소 테스트 코드는 잘 안 넣는 편인데 이번에 넣어보겠다.

import java.rmi.*;

public class GumballMonitorTestDrive
{
    public static void main(String[] args) {
        String location = "rmi://seattle.mightygumball.com/gumballmachine";

        try {
            GumballMachineRemote machine = (GumballMachineRemote) Naming.lookup(location);
            GumballMonitor monitor = new GumballMonitor(machine);
        } catch (Exception e) {
            e.printStackTrace();
        }

        monitor.report();
    }
}

참고로 아까 GumballMachineTestDrive 클래스에서 서비스를 레지스트리에 등록할 때,

"(서비스이름)" 이라고 했는데 실은 서비스 이름을 '//seattle.mightygumball.com/gumballmachine' 라고 등록했다.


Naming.lookup() 은 RMI 패키지에 있는 정적 메소드로 서비스 이름을 받아

그 이름을 통해 RMI 레지스트리에서 룩업 작업을 수행해준다.


RMI 레지스트리에서 프록시를 가져오고 나면 그 프록시를 클라이언트인 GumballMonitor 에게 넘겨준다.



* RMI 사용할 때 가장 흔하게 범하는 실수 :

1) 원격 서비스를 돌리기 전에 rmiregistry 를 실행시키 않은 경우

2) 인자와 리턴 형식이 직렬화 가능한지 확인하지 않은 경우 (런타임에서 오류가 발생)

3) 클라이언트에 스터브 클래스를 건네주지 않은 경우


여기서 Client 는 Monitor, MyServiceImpl 는 Machine 이라는 게 파악이 될 것이다.


서버 쪽에 스터브가 있는 이유는 - 아까 파란색 글씨로 표시해두었지만 -

서비스를 구현한 클래스가 RMI 레지스트리에 등록되면, 자동으로 스터브 클래스가 대신 등록되기 때문이다.



※ 전문가 Tip ※  (내가 전문가라는 뜻이 절대 아님, 책에 이렇게 나와있어서 그냥 갖다쓴거지)

클라이언트에서는 lookup 을 하려면 (rmic으로 생성한) Stub 클래스를 보유하고 있어야한다.

간단한 시스템에서는 그냥 Stub 클래스를 직접 클라이언트로 ctrl + c,v 해주면 된다.

하지만 그런 방법보다 더 좋은 방법이 있다. 바로 '동적 클래스 다운로딩(Dynamic Class Downloading)'이다.


동적 클래스 다운로딩을 사용하면 직렬화된 객체에 그 객체의 클래스 파일이 있는 위치를 나타내는

URL 이 내장된다. 그리고 역직렬화 과정에서 로컬 시스템의 클래스 파일을 찾지 못하게 된다면

내장된 URL 로부터 HTTP GET 요청을 통해 클래스 파일을 가져온다.


따라서 클래스 파일을 보내줄 수 있는 웹 서버가 있어야 되고 클라이언트의 보안 매개변수를

변경해야 될 수도 있다. 그 밖에도 더 신경써야할 것이 있지만 여기까지만 언급한다고 하신다.



RMI 덕분에 포스팅이 겁나 길어졌다. 이제 정리 좀 해보자.


원격 프록시(Remote Proxy)는 일반적인 프록시 패턴을 구현하는 방법 가운데 하나이다.

이 외에도 여러가지 변형된 방법들이 많다. 그건 다음 포스팅에서 다루고 우선 프록시 패턴의 정의를 보자.


* Proxy Pattern :

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


정의 자체는 심플하지만 '접근을 제어하는 방법' 면에서 변형된 프록시 녀석들이 많다.


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

- 가상 프록시 (Virtual Proxy) : 생성하기 힘든 자원에 대한 접근을 제어할 수 있다.

- 보호 프록시 (Protection Proxy) : 접근 권한이 필요한 자원에 대한 접근을 제어할 수 있다.


이정도만 하고 프록시 패턴의 클래스 다이어그램으로 넘어가자.


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

Proxy 에서 RealSubject 를 생성하거나 제거하는 역할을 책임지는 경우도 있다. 


클라이언트는 항상 Proxy 를 통해서 RealSubject 하고 데이터를 주고 받는다. 

Proxy 와 RealSubject 는 똑같은 인터페이스(Subejct) 를 구현하기 때문에 

RealSubejct 객체가 들어갈 자리면 어디든지 Proxy 를 대신 집어넣을 수 있다


Proxy 는 RealSubject 에 대한 접근을 제어하는 역할도 맡게 된다.

RealSubject 가 원격 시스템에서 돌아가거나, 그 객체의 생성 비용이 많이 들거나, 어떤 방식으로든지 

RealSubject 에 대한 접근이 통제되어 있는 경우에 접근을 제어해주는 객체가 필요할 수 있다.



여기까지 1차적으로 포스팅을 마무리하고

활용 부분과 기타 정리할 부분은 2차 포스팅으로 넘어가겠다.


(이미지 출처 :

http://dlucky.tistory.com/231

http://darkmirr.egloos.com/m/1219935

http://blog.lukaszewski.it/2014/01/31/design-patterns-proxy/)


by kelicia 2014. 6. 1. 02:46