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

주로 문방구 앞에서 보이는 뽑기 기계(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