제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



자바 디자인 패턴 17 - State

1. State 패턴은..

현재 상황에 따라 같은 일에 대해 다르게 반응을 합니다. 배가 고플 때 밥을 먹으면 배가 부릅니다. 하지만 배가 부를 때 밥을 또 먹으면 배터질 것 같아 화가 납니다. 같은 행동인 "밥을 먹는 것"에 대해 현재 상태가 "배부름"인지 "배고픔"인지에 따라 행동이 달라지는 것입니다.

2. 예제

----- 행동들을 나타내는 Action , 그림에서 화살표 부분(EAT, DIGEST, GOTOBED) ------

package ch17_State;
public enum Action {
    EAT,DIGEST,GOTOBED;
}


----- 각각의 상태를 나타내는 State, 그림에서 모서리 둥근 사각형 부분(HUNGRY, FULL, ANGRY, SLEEPING) ------

package ch17_State;
public enum State {
    HUNGRY {
        public State act(Action action) {
            switch (action) {
            case EAT:
                return FULL;
            default:
                return null;
            }
        }
    },
    FULL {
        public State act(Action action) {
            switch (action) {
            case EAT:
                return ANGRY;
            case DIGEST:
                return HUNGRY;
            case GOTOBED:
                return SLEEPING;
            default:
                return null;
            }
        }
    },
    ANGRY {
        public State act(Action action) {
            switch (action) {
            case DIGEST:
                return FULL;
            default:
                return null;
            }
        }
    },
    SLEEPING {
        public void onEntry(){
            System.out.println("이부자리깜.");
        }
        public State act(Action action) {
            return null;
        }
    };
    abstract State act(Action action);

    public static State getInitState() {
        return HUNGRY;
    }

    public static boolean isFinalState(State arg) {
        return arg == SLEEPING;
    }
    public void onEntry(){}
    public void onExit(){}
}

----- State를 관리하는 StateContext ------

package ch17_State;
public class StateContext {
    private State currentState;
    public StateContext() {
        currentState = State.getInitState();
    }
    public void processEvent(Action action) {
        State next = currentState.act(action);
        if (next != null) {
            currentState.onExit();
            System.out.println(action + "에 의해 State가 " + currentState + "에서"
                    + next + "로 바뀜.");
            currentState = next;
            currentState.onEntry();
            if (State.isFinalState(currentState)) {
                System.out.println("오메~ 마지막 State에 도달했네~");
            }
        } else {
            System.out.println(action + "은  State가 " + currentState
                    + "에서는 의미 없는 짓.");
        }
    }
}

----- 테스트 클래스 -----
package ch17_State;
public class Test {
    public static void main(String[] args) {
        StateContext context = new StateContext();

        context.processEvent(Action.EAT);
        context.processEvent(Action.EAT);
        context.processEvent(Action.GOTOBED);
        context.processEvent(Action.DIGEST);
        context.processEvent(Action.GOTOBED);
    }
}

----- 테스트 결과 -----

EAT에 의해 State가 HUNGRY에서FULL로 바뀜.
EAT에 의해 State가 FULL에서ANGRY로 바뀜.
GOTOBED은  State가 ANGRY에서는 의미 없는 짓.
DIGEST에 의해 State가 ANGRY에서FULL로 바뀜.
GOTOBED에 의해 State가 FULL에서SLEEPING로 바뀜.
이부자리깜.
오메~ 마지막 State에 도달했네~

State 패턴에는 State를 정의하는 State가 있습니다. 전체를 통틀어 퍼런 모서리 둥근 사각형은 4개(HUNGYR, FULL, ANGRY, SLEEPING) 가 있습니다. 얘네들이 전부 State가 됩니다. 다음으로는 화살표로 스테이트 변화를 표현해주는 Action 들이 3개(EAT, DIGEST, GOTOBED) 있습니다.

State에는 abstract로 act(Action) 이 있습니다. 어떤 상태에서 어떤 Action이 들어왔을 때, 뭔일이 일어날 지를 결정하는 것입니다.  배가 고플 때 (State가 HUNGRY일 때) EAT하면 배가 불러지지만. 배가 부를 때 (State가 FULL일 때) EAT하면 화가 납니다. 다시 말해, HUNGRY 의 act와 FULL의 act는 다른 행동을 하게 됩니다.
상위에서는 일단 행동을 받아서 뭔가 하라는 의미에서 abstract로 정의를 합니다. 행동에 구체적으로 반응을 하는 것은 각각의 State들 입니다. 각각의 State에서 act를 정의할 때는 현재 자기 State에서 화살표가 나가는 것만 고려해서 코딩하면 됩니다. HUNGRY에서 나가는 화살표는 1개(EAT) 뿐이므로, HUNGRY.act는 간단하게 구현됩니다.

StateContext는 전체 상태를 관리하는 객체입니다. 테스트 코드를 보면, State라는 것은 등장하지 않습니다. StateContext만 상대하고 있을 뿐이죠. 상태가 HUNGRY에서 FULL로 바뀌는 것은 테스트 코드에서 하는 것이 아니라 StateContext에서 합니다. 테스트 코드의 역할은 이런저런일이 일어났다는 것만을 전달합니다. 그 행동들은 StateContext가 가지고 있는 상태가 뭐냐에 따라 뭔가를 하기도 하고, 안 하기도 합니다.

일반적으로 State가 변하면 변하는 와중에 뭔가를 해야하는 경우가 있습니다. 위에서는 SLEEPING 상태에 들어오려면, 일단 이부자리를 까는 행동 등이 있을 수 있겠습니다. 그래서 State는 onEntry()와 onExit() 함수를 가집니다. 그런데, act와는 달리 abstract로 처리하지 않는 게 일반적입니다. 딱히 뭔가 해야할 일이 없을 수도 있으니까요. 예제에서는 SLEEPING 상태에서만 onEntry를 구현했습니다. 즉, 필요한 행동이 있을 때만 override하면 됩니다. 코드에서 보라색 부분이 oldState.onExit, state 변경, newState.onEntry를 호출하는 부분입니다.

3. State Diagram

State 패턴으로 구현을 할려면, 일단 State Diagram이라는 그림을 그려봐야 합니다. 맨 처음 등장하는 그림이지요.
일단 까만 똥그라미는 시작이라는 것을 의미합니다. 그래서 시작하자마자 바로 HUNGRY 상태가 초기상태로 잡히게 됩니다.
까만 이중똥그라미는 종료 상태를 나타냅니다. 일반적으로 SLEEPING에서 종료 상태로 가는 것도 하나의 Action에 의해 처리될 수 있지만, 간단한 프로그램을 위해서 생략했습니다. 
시작은 반드시 하나가 있어야 하고, 종료는 여러 개가 될 수도 있습니다. 위의 State Diagram에다가 ANGRY에서 EAT 하면, "배터져 죽은 상태"를 하나 더 추가하면, 그것도 종료 상태가 될 수 있을 것입니다.

4. State 패턴을 쓰면 좋은 경우

State 패턴은 위에서 설명한 것처럼, 현재 상태만 고려해서 코팅을 하면 됩니다. 앞 뒤의 복잡한 사정 같은 것은 신경쓰지 않아도 됩니다. 각각의 자기 상태에서 다른 상태로 변화되는 화살표(자기 상태에서 나가는 화살표) 와 자기한테 들어올 때 하는 일, 나갈 때 하는 일을 정의하는 것으로 프로그램이 매우 간단해 집니다.

State 패턴은 사실 if ~ else if 와 같은 구조를 State로 표현한 것에 지나지 않습니다. State의 메쏘드를 없애면, StateContext.processEvent(Action action) 는 대략 다음과 같은 코드로 구성될 것입니다.

if(currentState == State.HUNGRY){
    if(action == Action.EAT){
        //뭔가 old state에서 나갈 때 할 일이 한다.
        currentState = State.FULL;
        //뭔가 new state 들어올 때 할 일이 있으면 한다.
    }
}else if (currentState = State.FULL){
......

아무래도 같은 기능을 하지만, StatePattern을 정확히 쓰는 것에 비해 산만할 것 같은 느낌이 팍팍 듭니다. State가 늘어나거나 했을 때, 코드 수정은 어느 쪽이 더 쉬울까는 너무나 자명해 보입니다. 프로그램에서 복잡한 분기문이 난무해야 될 상황이라고 생각하시면 State 패턴을 쓰면 좋습니다.

State 패턴의 구성요소는 3가지입니다.
첫째, 상태를 나타내는 state
둘째, 상태 간의 전이를 표현하는 action
셋째, 상태 전이에 나타나는 onEntry, onExit 함수.


이 세 가지에 대한 개념만 명확하면 구현이 어렵지 않습니다.

5. state 패턴을 구현하는 여러가지 방법

위에서는 전부 action과 state를 전부 enum으로 표현했지만, 직접 클래스로 표현해도 됩니다. action을 경우는 정의하는 방법이 다양할 것이고, state의 경우 class로 표현하게 되면, 상위 AbstractState 클래스와 AbstractState를 상속 받는 여러 개의 ConcreteClass들(hungry, full, angry, sleeping) 을 정의할 수 있을 것입니다.

예제에서는 특정 action에 대해서는 구체적인 상태에서 분기를 했습니다. 그러나, 분기 시점은 여러가지가 경우가 가능합니다.

아래와 같은 코드를 
        StateContext context = new StateContext();

        context.processEvent(Action.EAT);
        context.processEvent(Action.GOTOBED);

아래와 같이 쓰고
        StateContext context = new StateContext();

        context.eat();
        context.goToBed();

StateContext에서 분기를 해도 됩니다. 아니면 State.act에서(ConcreteState 말고, 그냥 State) 분기를 해도 됩니다. 이렇게 되면, State의 method가 많아지게 되지만(Action 당 1개의 메쏘드), ConcreteState (hungry, full, angry, sleeping) 에서 필요한 method만 override하면 됩니다.

by 삼실청년 | 2010/02/19 22:48 | 컴터질~ | 트랙백 | 덧글(3)

트랙백 주소 : http://iilii.egloos.com/tb/5203506
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by 태양 at 2011/12/07 15:00
졸어렵네요 ㅋ
Commented by at 2014/05/23 15:20
좋은 글 아주 잘 보고 갑니다~
Commented by 삼실청년 at 2014/06/14 02:12
도움이 되셨길 바래요.^^

:         :

:

비공개 덧글

◀ 이전 페이지          다음 페이지 ▶