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


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


이번에도 이해를 돕기 위한 상황설정 들어가겠다.

주로 문방구 앞에서 보이는 뽑기 기계(Gumball Machine)에 들어갈 코드를 작성해야 하는데

다음과 같은 상태 다이어그램(State Diagram)이 주어졌다고 하자.


보다시피 4개의 상태가 존재한다.

(No Quarter : 동전 없음, Has Quarter : 동전 있음, Gumball Sold : 껌볼 판매, Out of Gumballs : 매진)



상황 파악이 대충 되었으니 일단 코드를 작성해보자.

public class GumballMachine {
    final static int SOLD_OUT = 0;
    final static int NO_QUARTER = 1;
    final static int HAS_QUARTER = 2;
    final static int SOLD = 3;

    int state = SOLD_OUT;
    int count = 0;    // 껌볼 갯수

    public GumballMachine(int count) {
        this.count = count;
        if ( count > 0 ) { 
            state = NO_QUARTER; 
        }
    }

    public void insertQuarter() {
        if ( state == HAS_QUARTER ) { // 출력 : 동전은 한 개만 }
        else if ( state == NO_QUARTER ) { 
            state = HAS_QUARTER;
            // 출력 : 동전 받음
        } 
        else if ( state == SOLD_OUT ) { // 출력 : 매진 }
        else if ( state == SOLD ) { // 출력 : 껌볼 나오는 중 }
    }

    public void ejectQuater() { // 동전 반환시에 해야할 일 }
    public void turnCrank() { // 손잡이 돌릴 때 해야할 일 }
    public void dispense() { // 껌볼 내줄 때 해야할 일 }

    // 기타 메소드
}

코드 길게 넣는 거 안 좋아하지만... 나중에 나올 코드와 비교하기 좋게 일단 썼다.

(코드 자세히 읽다보면 가끔 뭐 할려고 했는지 까먹음ㅡ_ㅡ)


어쨌든 주목할 부분은

1) 상태 값 저장을 위한 인스턴스 변수

2) 각 메소드 별로 각각의 상태에 대해 덕지덕지 붙은 if 문


내가 프로그래밍 초창기 때 많이 써먹던 방법...이긴 하나

딱 봐도 이상적이지 못 하다는 느낌과 더불어 차후 요구사항 변경으로 인해

코드가 계속 지저분해질 것 같다는 걱정에 눈 앞이 깜깜해진다.


구체적으로 여러가지 문제점이 있다.

- OCP 를 지키고 있지 않다는 점

- 객체지향 디자인이라고 하기엔 무리가 있다

- 지저분한 조건문으로 인해 상태전환이 분명하게 드러나있지 않다

- 다른 기능을 추가하는 과정에서 기존 코드에 없던 새로운 버그가 생길 위험이 높다



문제점을 파악했으니 이제 State 패턴을 적용해 해결하자.

디자인에 들어있는 모든 상태를 캡슐화시켜 State 인터페이스를 구현하는 클래스를 만든다.



SoldState 클래스 코드를 간단히 보자.

public class SoldState implements State 
{
    GumballMachine gumballMachine;

    public SoldState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    public void insertQuarter() { // 출력 : 껌볼 나오는 중 }
    public void ejectQuarter() { // 출력 : 이미 껌볼 나왔음 }
    public void turnCrank() { // 출력 : 손잡이는 1번만 돌려 }

    public void dispense() {
        gumballMachine.releaseBall();
        if ( gumballMachine.getCount() > 0 ) {
            gumballMachine.setState(gumballMachine.getNoQuarterState());
        } else {
            // 출력 : 매진
            gumballMachine.setState(gumballMachine.getSoldOutState());
        }
    }
}


그리고 GumballMachine 클래스를 다시 작성해보자.

public class GumballMachine 
{
    State soldState;
    // 그 외 3가지 State 객체 선언

    State state = soldOutState;
    int count = 0;

    public GumballMachine(int numberGumballs) 
    {
        soldState = new SoldState(this);
        // 그외 3가지 State 객체 초기화

        this.count = numberGumballs;
        if ( numberGumballs > 0 ) {
            state = noQuarterState;
        }
    }

    public void insertQuarter() { state.insertQuarter(); }
    public void ejectQuarter() { state.ejectQuarter(); }
    public void turnCrank() { 
        state.turnCrank();
        state.dispense();
    }
    
    void setState(State state) { this.state = state; }
    void releaseBall() {
        // 출력 : 껌볼 굴러가는 중
        if ( count != 0 ) { count--; }
    }

    // Getter 메소드를 비롯한 기타 메소드
}


이렇게 패턴을 적용하고 나니 어떤 결과를 얻었을까?

- 각 State 의 행동(Behavior)을 별개의 클래스로 국지화 시킴

- 지저문한 if 문이 사라짐

- 각 State를 변경하는 것에 대해서는 닫혀있으면서 GumballMachine 자체는

  새로운 상태 클래스를 추가하는 확장에 대해서는 열려있도록 바뀜(OCP)

- 포스팅 상단에 있는 다이어그램에 훨씬 가까우면서도 이해하기 좋은 코드 베이스와 클래스 구조 생성



* State Pattern : 

객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다.

마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.



State 패턴 다이어그램을 살펴보자.



내가 1장을 너무 허접하게 포스팅하는 바람에

눈치를 못 챘는데 위 다이어그램은 1장에서 다뤘던 Strategy 패턴과 똑같다 !



Strategy 패턴, State 패턴의 다이어그램은 같지만 

이 2가지의 패턴은 용도에 있어서 차이점이 있다.


< Strategy 패턴 >

- 일반적으로 Client 에서 Context 객체한테 어떤 전략 객체를 사용할 지를 지정해준다.

주로 실행시에 전략 객체를 변경할 수 있는 유연성을 제공하기 위한 용도로 쓰인다.

- SubClass 를 만드는 방법을 대신하여 유연성을 극대화하기 위한 용도로 쓰인다.

상속을 이용해서 Class 의 행동을 정의하다보면 행동을 마음대로 변경하기가 힘들지만

Strategy 패턴을 사용하면 구성을 통해 행동을 정의하는 객체를 유연하게 바꿀 수 있다.


< State 패턴 >

- State 객체에 일련의 행동이 캡슐화 된다. 상황에 따라 Context 객체에서 여러 State 객체 중

하나의 객체에게 모든 행동을 맡기게 된다. Client 는 State 객체에 대해 자세히 몰라도 된다.

- Context 객체에 지저분하게 조건문을 집어넣는 대신에 사용할 수 있는 패턴이라 할 수 있다.

- Context 객체에서는 미리 정해진 State 전환 규칙을 가지고 자기 State 를 변환한다.




Q. 아까 코드를 보면 구상 State 클래스에서 GumballMachine(Context) 의 SetState() 를 이용해

다음 State 를 결정했는데 반드시 그래야 하나?


- 항상 그런 건 아니다. State 전환이 고정되어 있으면(정적일 경우) Context 에서 

State 전환의 흐름을 결정하는 코드를 작성해도 된다. 하지만 아까와 같이 실행 중에 껌볼의 갯수에 따라 

State 전환이 동적으로 결정되는 경우에는 State 클래스에서 처리하는 것이 좋다.


- State 전환 코드를 State 클래스에 집어넣으면 State 클래스들 사이에 의존성이 생긴다는 단점이 있다.

아까 코드에서는 GumballMachine 객체의 Getter 메소드를 써서 그나마 의존성을 최소화하려고 노력했다.



+ State 패턴을 이용하다 보면 디자인에 필요한 Class 의 갯수가 늘어나지만

유연성을 향상시키기 위해 지불해야 하는 비용이라고 생각하자.

개인적으로 지저분한 if 문은 사절이다.



* 객체지향의 원칙 :

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

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

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

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

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

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

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

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

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



* 이 장에서의 정리 :

- State 패턴을 사용하면 Procedure 형 State Machine 을 쓸 때와 달리 클래스를 이용하여 각 State 를 표현한다.

- 각 State 를 캡슐화함으로써 차후에 생길 변동사항을 국지화 시킬 수 있다.

- Strategy 패턴에서는 일반적으로 Context 클래스를 만들 때 행동 or 알고리즘을 설정한다.

- State 클래스를 여러 Context 객체의 인스턴스에서 공유하도록 디자인할 수도 있다.



# Pattern 짧게 복습 #

- State : 상태를 기반으로 하는 행동을 캡슐화하고 행동을 현재 상태한테 위임한다.

- Strategy : 바꿔 쓸 수 있는 행동을 캡슐화 한 다음, 실제 행동은 다른 객체에 위임한다.

- Template Method : 알고리즘의 각 단계를 구현하는 방법을 서브클래스에서 구현한다.



(이미지 및 기타 출처 :

http://www.cnblogs.com/zhuqiang/archive/2012/05/04/2482415.html

http://blog.lukaszewski.it/2013/12/29/design-patterns-state/

http://stevenjsmin.tistory.com/78)


by kelicia 2014. 5. 30. 21:24


다른 챕터에 비해 이번 챕터는 내용이 많다ㅡ_ㅡ. 휴


우선 어떤 상황을 가정하겠다.

서로 다른 클래스 A, B에서 각자의 방식으로 구현된 배열들을 Clinet 에서 출력하는 코드를 작성할 때, 

- A 클래스의 배열은 ArrayList, B 클래스의 배열은 기본 배열로 구현되었다고 예를 들겠다. -

다음과 같은 방법을 쓸 수 있다.

for(int i=0; i<arrayListItems.size(); i++){
	MenuItem menuItem = (MenuItem) arrayListItems.get(i);
	// 출력
}

for(int i=0; i<arrayItems.length; i++){
	MenuItem menuItem = arrayItems[i];
	// 출력
}

배열 타입 때문에 코드 자체는 조금 다르지만 명백하게 같은 일을 하고 있다.

이렇게 구상 클래스에 맞춰서 코딩하면 코드 중복 및 수정, no 캡슐화와 같은 폐해가 뒤따른다.


그래서 !

이를 해결하기 위해 '반복을 캡슐화'할 것이다.



1. Iterator 패턴


- Iterator (반복자) 라는 인터페이스를 통해 Array, List, HashTable 은 물론이고 

어떤 종류의 컬렉션(또는 집합체, Aggregate)에 대해서도 반복자를 구현할 수 있다.


다음과 같이 인터페이스를 직접 만들어도 되고,

public interface Iterator {
    boolean hasNext();
    Object next();
}

아니면 java.util.Iterator 를 사용해도 된다. (위의 인터페이스에서 void remove()만 추가하면 된다)

참고로 ArrayList 의 메소드 중에는 반복자를 리턴하는 iterator() 라는 메소드가 있다.


하지만 기본 Array 에서는 iterator()가 없으니 

아까 위에서 기본 배열 방식으로 구현한 B 클래스를 위한 IteratorB 클래스를 만든 후,

그 클래스에 java.util.Iterator 를 import 해서 Iterator 인터페이스를 구현해보겠다.

import java.util.Iterator;

public class IteratorB implements Iterator {
    MenuItem[] arrayItems;
    int position = 0;

    public IteratorB(MenuItem[] arrayItems) {
        this.arrayItems = arrayItems;
    }

    public Object next() {
        MenuItem menuItem = arrayItems[position];
        position = position + 1;
        return menuItem;
    }

    public boolean hasNext() {
        if ( position >= arrayItems.length || arrayitems[position] == null ) {
            return false;
        } else {
            return true;
        }
    }

    public void remove() {
        if ( position <= 0 ) {
            throw new IllegalStateException("can't remove");
        }
        if ( arrayItems[position-1] != null ) {
            for( int i = position-1; i < (arrayItems.length-1); i++ ) {
                arrayItems[i] = arrayItems[i+1];
            }
        }
    }
}

Tip. remove()가 필요없다면 예외를 던질 수도 있다.

public void remove() {
    throw new UnsupportedOperationException("Unsupprted Operation 'remove()'");
}


반복자를 구현했으면 이 반복자를 사용하기 위해 객체를 생성해줘야 한다.


ArrayList 로 구현한 A 클래스에서는

public Iterator createIterator() {
    return arrayListItems.iterator();
}

Array 로 구현한 B 클래스에서는

public Iterator createIterator() {
    return new IteratorB(arrayItems);
}


자, 반복자 패턴을 위한 멍석을 다 깔아놓았으니 Client 클래스 코드에서 출력 코드를 작성해보자.

public class Client {
    A arrayListItems;
    B arrayItems;

    public Client ( A arrayListItems, B arrayItems ) {
        this.arrayListItems = arrayListItems;
        this.arrayItems = arrayItems;
    }

    public void printMenuItems() {
        Iterator iteratorA = arrayListItems.createIterator();
        Iterator iteratorB = arrayItems.createIterator();
        printMenuItems(iteratorA);
        printMenuItems(iteratorB);
    }

    public void printMenuItems(Iterator iterator) {
        while ( iterator.hasNext() ) {
            MenuItem menuItem = (MenuItem) iterator.next();
            // 출력
    }
}


여기서 끝난 게 아니다. 아직 Client 가 A, B 라는 구상 클래스를 참조하고 있다.

화룡점정으로 집합체 클래스인 A, B 의 인터페이스를 통일시켜주자.


아까 위에서 A, B 클래스 각자 createIterator() 를 구현했다.

public interface Aggregate {
    public Iterator createIterator();
}

Aggregate 라는 인터페이스를 이처럼 작성하고

A, B 클래스 선언부에 implements Aggregate 만 추가해주면 통일끝.

그러면 Client 는 구상 클래스가 아닌 인터페이스를 참조할 수 있다.

public class Client {
    Aggregate arrayListItems;
    Aggregate arrayItems;

    public Client ( Aggregate arrayListItems, Aggregate arrayItems ) {
        this.arrayListItems = arrayListItems;
        this.arrayItems = arrayItems;
    }

    // 이하 생략
}



휴, 힘들다. 코드가 여기저기 튀어나와 복잡하니 정리해보겠다.

클래스 다이어그램을 보자.

이 클래스 다이어그램으로 위에서 설명했던 것이 정리가 된다.

(참고로 Factory Method 패턴의 다이어그램과 상당히 유사한 형태이다)


* Iterator Pattern : 

컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에

하나씩 접근할 수 있게 해주는 방법을 제공한다.


이것이 바로 반복을 캡슐화하는 패턴이다.


Iterator 패턴을 사용하면 모든 항목에 일일이 접근하는 작업을

컬렉션 객체(Concrete Aggregate)가 아닌 반복자 객체에서 맡게된다는 장점이 있다.


이렇게 하면 컬렉션의 인터페이스 및 구현이 간단해지고,

컬렉션은 반복작업에서 손 떼고 컬렉션 관리에만 전념하면 된다.



* 단일 역할 원칙

- 어떤 클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.


즉, 한 클래스에는 1가지 역할만 맡도록 해야 한다.

Iterator 패턴의 경우 반복작업과 컬렉션 관리를 신경쓰는 컬렉션을

관리라는 역할에만 충실할 수 있도록 도와주었으니 이 원칙을 준수한다.


+ 응집도(cohesion) :

한 클래스 또는 모듈이 특정 목적 또는 역할을 얼마나 일관되게 지원하는지를 나타내는 척도.

응집도가 높다는 것은 일련의 서로 연관된 기능이 묶여있다는 것을,

응집도가 낮다는 것은 서로 상관 없는 기능들이 묶여있다는 것을 뜻한다.



※ 마무리 Tip 

(1) HashTable 의 경우 iterator() 를 지원하는 자바 컬렉션이지만

① hashTableItems.iterator() -> (X)

② hashTableItems.values().iterator -> (O)


HashTable 이 <key, value> 로 구성되있다는 점을 떠올리면 ② 가 왜 옳은지 알 수 있다.


(2) JAVA 컬렉션 프레임워크에서는 ListIterator 라는 반복자 인터페이스도 제공하는데,

이 인터페이스에는 Iterator 의 메소드 외에도 previous() 를 비롯해 몇 개 더 추가되어 있다.

그러니 next() 때문에 한 방향으로만 접근해야 되는지에 대한 걱정은 안 해도 된다.




2. Composite Pattern


- Iterator 패턴과 Composite 패턴을 꼭 묶어서 생각할 필요는 없다 -

Iterator 패턴 덕분에 출력 코드를 for 문으로 도배하는 것을 막을 수 있었다.

하지만 Client 클래스의 printMenuItems() 를 보자.

public void printMenuItems() {
    Iterator iteratorA = arrayListItems.createIterator();
    Iterator iteratorB = arrayItems.createIterator();
    print(iteratorA);
    print(iteratorB);
}

새로운 Concrete Aggregate 가 추가될 때마다 여기에 코드를 추가해주어야 한다.

이는 'OCP(Open Closed Principle) 원칙 - 확장은 open, 코드 변경은 close -' 에 위배된다.


게다가 집합체(Aggregate) 안에 또 다른 집합체를 갖고 있을 경우 골치가 아파진다.

ex) array = { 1, 2, 3, {7, 8, 9} } 



자, 더 유연한 방법으로 아이템에 대해서 반복작업을 수행할 수 있어야 한다.

리팩토링이 필요한 시기라 할 수 있다.


우선 골치 아파지기 전에 다음과 같이 Tree 구조를 사용하는 게 좋을 듯 싶다.

* Composite Pattern :

객체들을 트리(Tree) 구조로 구성하여 부분-전체 계층구조를 구현한다.

이 패턴을 이용하면 Client 에서 개별 객체와 복합 객체(Composite)를 똑같은 방법으로 다루도록 할 수 있다.


정의 내린 김에 Composite 패턴의 클래스 다이어그램도 보자.


복합 객체(Composite) 에는 구성요소(Component) 가 들어가있다.

구성요소는 2 종류로 나뉜다.

하나는 복합 객체, 다른 하나는 잎(Leaf)이다.

복합 객체에는 일련의 자식들(Children)이 있고, 그 자식노드 역시 복합객체 or 잎일 수 있다.

그리고 잘 알다시피 잎은 자식이 하나도 없다.


즉, 재귀적인 구조를 갖는다.



이제 패턴을 이용해서 아까의 문제로 돌아가서 적용해보자.

아래 다이어그램에서 웨이트리스(Waitress) 는 Client 다.

원래 책의 예제에서는 아까 언급했던 A, B 클래스가 서로 다른 음식점의 메뉴(Menu)에 해당한다.


MenuComponent 가 

MenuItem(Leaf) 와 Menu(Composite) 에서 쓰이는 인터페이스 역할을 한다는 점을 기억해두자.


MenuComponent 의 기본 구현을 해보자면,

public abstract class MenuComponent 
{
    // Composite's Method
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException(); // body 는 이하 모두 동일
    }
    public void remove(MenuComponent menuComponent) { }
    public MenuComponent getChild(int i) { }

    // Leaf, Composite's Method
    public String getName() { }
    public String getDescription() { }
    public double getPrice() { }
    public boolean isVegitarian() { }

    public void print() { }
}

MenuItem 은 위 클래스 다이어그램 만으로도 충분하므로 Menu 코드만 보겠다.

Menu 코드에서 print() 를 자세히 보면 재귀적인 방법으로 출력하는 것을 볼 수 있다.

public class Menu extends MenuComponent 
{
    ArrayList menuComponents = new ArrayList();

    // 생성자 코드

    // 기타 메소드

    public void print() { 
        Iterator iterator = menuComponents.iterator();
        while ( iterator.hasNext() ) {
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            menuComponent.print();
    }
}

Client 코드는 간단하다.

public class Client 
{
    MenuComponent allMenus;

    public Client (MenuComponent allMenus) {
        this.allMenus = allMenus;
    }

    public void print() {
        allMenus.print();
    }
}

테스트 코드에서는 

MenuComponent allMenus = new Menu(); 를 선언하고 

여러 메뉴 항목들을 만들어 allMenus 에 추가 한 다음(add()),

Client client = new Client(allMenus); -> client.print(); 하면 말끔하다.



Q. Composite 패턴에서 계층 구조 관리와 메뉴와 관련된 작업을 처리하는데,

단일 역할 원칙에 위배되는 건 아닌가?

- Composite 패턴은 단일 역할 원칙을 깨지만 대신에 투명성(Transparency)을 확보하는 패턴이다.


Component 인터페이스에 계층 구조 관리 기능과 잎(Leaf)의 기능을 전부 집어넣음으로써

Client 에서 복합 객체와 잎 노드를 똑같은 방식으로 처리할 수 있도록 할 수 있다.

어떤 것이 복합 객체고 잎 노드인지가 Client 입장에서는 투명하게 느껴진다.


2가지 기능을 하다보니 안정성은 약간 떨어지나 

(Client 입장에서는 구분을 못 하므로 어떤 객체에 부적절한 메소드를 호출하는 일이 있을 수 있다)

만약 쪼개서 디자인한다면 투명성이 떨어져 코드에 조건문, instanceof 를 써야할 수도 있다.


모든 디자인 원칙을 준수하는 건 어렵기 때문에 상황에 따라 원칙을 적절하게 사용해야한다.




* 복합 객체(Composite) 에 대한 반복자(Iterator) 구현


끝난 게 아니다. createIterator() 가 남아있다.


인터페이스인 MenuComponent 에 이 메소드를 추가할 것이다.

그러면 Menu 와 MenuItem 클래스에서 추가로 구현하자.


Menu 는

public Iterator createIterator() {
    return new CompositeIterator(menuComponents.iterator());
}

MenuItem 는 NullIterator 객체를 리턴하는데

NullIterator는 Iterator 를 구현한 클래스이고 Command 패턴에서 봤던 Null 객체와 같은 개념이다.

public Iterator createIterator() {
    return new NullIterator();
}


마지막 코드다. CompositeIterator 클래스이다.

import java.util.*;

public class CompositeIterator implements Iterator 
{
    Stack stack = new Stack();

    public CompositeIterator(Iterator iterator) {
        stack.push(iterator);
    }

    public Object next() {
        if (hasNext()) {
            Iterator iterator = (Iterator) stack.peek();
            MenuComponent component = (MenuComponent) iterator.next();
            if (component instanceof Menu) {
                stack.push(component.createIterator());
            }
            return component;
        } else {
            return null;
        }
    }

    public boolean hasNext() {
        if (stack.empty()) {
            return false;
        } else {
            Iterator iterator = (Iterator) stack.peek();
            if (!iterator.hasNext()) {
                stack.pop();
                return hasNext();
            } else {
                return true;
            }
        }
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }
}

재귀때문에 조금 빡칠 수도 있다.

이해하고 보면 별 것도 아니니 인내를 가지고 이해하자.



- Composite 패턴의 가장 큰 장점은 클라이언트를 단순화 시킬 수 있다는 점이다.

아까 투명성 얘기에서 말했다시피 클라이언트는 복합 객체인지, 잎 객체인지 신경쓰지 않아도 된다. 

올바른 객체에 대해서 올바른 메소드를 호출하고 있는지 확인하려고 

여기저기 if 문을 사용하지 않아도 된다는 이야기이다.

그리고 메소드 하나만 호출하면 전체 구조에 대해서 반복해서 작업을 처리할 수 있는 경우도 자주 있다.


- 복합 구조가 너무 복잡하거나 복합 객체 전체를 Traveling 하는 데

너무 많은 자원이 필요한 경우에는 복합 노드를 Caching 해두는 것도 도움이 된다.

예를 들어 복합 객체에 있는 모든 자식노드에서 어떤 계산을 하고, 그 모든 자식노드들에 대해서

반복적인 작업을 수행해야 한다면 계산 결과를 임시로 저장하는 캐시를 만들어 속도를 향상시킬 수 있다.



* 객체지향의 원칙 :

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

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

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

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

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

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

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

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

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



* 이 장에서의 정리 :


- Iterator 패턴 - 

. 반복자(Iterator)를 이용하면 내부 구조를 드러내지 않으면서도 클라이언트로부터

  컬렉션 안에 들어있는 모든 원소들에 일일이 접근하도록 할 수 있다.

. 집합체에 대한 반복 작업을 별도의 객체로 캡슐화 할 수 있다.

. 컬렉션에 모든 데이터에 대해서 반복 작업을 하는 역할을 컬렉션에서 분리할 수 있다.


- Composite 패턴 -

. 개별 객체와 복합 객체를 모두 담아둘 수 있는 구조를 제공한다.

. 클라이언트에서 개별 객체와 복합 객체를 동일한 방법으로 다룰 수 있다.

. 복합 구조는 구성 요소로 이루어진다. 구성요소에는 복합 객체와 잎 노드가 있다.

. 상황에 따라 투명성(Transparency)과 안전성 사이에서 적절한 평형점을 찾아야 한다.



# Pattern 짧게 복습 #

- Observer : 어떤 상태가 변경되었을 때 일련의 객체들에게 이를 알려준다.

- Adapter : 하나 이상의 클래스의 인터페이스를 변환한다.

- Facade : 일련의 클래스들에 대한 인터페이스를 단순화시킨다.

- Iterator : 컬렉션의 구현을 드러내지 않으면서도 컬렉션에 있는 모든 객체들에 대해 반복 작업이 가능하다.

- Composite : 클라이언트에서 객체 컬렉션과 개별 객체를 같은 방식으로 처리할 수 있다.



(이미지 및 기타 출처 :

http://stevenjsmin.tistory.com/77

http://sadiles.blog.me/10110397609

http://blog.lukaszewski.it/2013/11/19/design-patterns-composite/

http://blog.lukaszewski.it/2013/10/14/design-patterns-iterator/

http://www.cnblogs.com/zhuqiang/archive/2012/04/27/2474233.html

http://www.cnblogs.com/zhuqiang/archive/2012/05/03/2481288.html)



P.S. 이번 포스팅 겁나 힘들다 흑ㅠ_ㅠ


by kelicia 2014. 5. 29. 03:40