본문 바로가기

소프트웨어/디자인패턴

[소프트웨어/디자인패턴] 스테이트 패턴(State Pattern)

State Pattern

스테이트 패턴은 상태에 행위를 위임하는 패턴이다.

Context

전등이 있다고 가정하자, 우리는 on버튼과 off버튼을 이용해서 끌 수 있고, 킬 수 있다. off상태에서 off버튼 눌러도 아무런 변화는 없다(반대의 경우도 마찮가지다) 이를 아래와 같이 상태 머신 다이어그램으로 나타낼 수 있다.


상태 머신 다이어그램은 별다른 코드레벨 설계가 아니라고 볼 수 있다. 그래서 우리는 보통 생각하는 것과 같이 if의 향현의 코드를 아래와 같이 만들 수 있다. 내부 또는 내부지만 공개한 enum을 통해 상태를 정의하고 Light클래스는 해당 STATE를 필드로 가지고 있음으로써 해당 객체의 상태를 정의했다.

각각의 on_button_pushed(), off_button_pushed()를 보면 같은 상태의 경우 아무것도 안하고 다를 경우 키거나 끈다.

class Light {
    public enum STATE { ON, OFF }

    private STATE state = STATE.OFF;

    public void onButtonPushed() {
        if(state == STATE.ON) {
            System.out.println("반응 없음");
        } else {
            System.out.println("Turn on the light");
            state = STATE.ON;
        }
    }

    public void offButtonPushed() {
        if(state == STATE.OFF) {
            System.out.println("반응 없음");
        } else {
            System.out.println("Turn off the light");
            state = STATE.OFF;
        }
    }
}

Problem

요즘 전등은 LED에 블루투스까지 달려있다. 따라서 앱으로 기능을 조작할 수 있는데, 확김에 Sleeping모드를 만들어서 취침등 기능을 한번 전구에 집어 넣어보자.

기능

  • on 상태에서 한번 더 on버튼을 눌렀을 때 Sleeping 모드로 전환한다.
  • sleeping 상태에서 한번 더 on버튼을 눌렀을 때 기존 on모드로 변환한다.
  • on, sleeping 상태에서 off버튼을 누르면 off상태로 변한다.

위 기능을 아래와 같이 상태 머신 다이어그램으로 그릴 수 있다.

과거 했던 방식과 같이 enumSLEEPING상태를 정의하고 아래와 같이 코드를 작성할 수 있다. 아래와 같이 각 행위에 대해서 수정 및 추가되었고 if문은 점점 복잡해져간다.

class Light {
    public enum STATE { ON, OFF, SLEEPING }

    private STATE state = STATE.OFF;

    public void onButtonPushed() {
        if(state == STATE.OFF || state == STATE.SLEEPING) {
            System.out.println("Turn on the light!");
            state = STATE.ON;
        } else if(state == STATE.ON){
            System.out.println("Sleeping mode!");
            state = STATE.SLEEPING;
        } else {
            System.out.println("아무것도 안해용");
        }
    }

    public void offButtonPushed() {
        if(state == STATE.ON || state == STATE.SLEEPING) {
            System.out.println("Turn off the light!");
            state = STATE.OFF;
        } else {
            System.out.println("아무것도 안함!");
        }
    }
}

Solution

위 기능이 4개, 5개, 6개 등 점점 늘어나갈 수록 각 상태(노드)에 대한 연결점과 그에 대한 구현이 어려워지고 복잡해질 것이다. 코드가 복잡하다는 건 버그를 유발시킬 가능성이 높아지므로 지양해야하는 방법이다.

잘 생각해보자, 전의 strategy pattern에서는 행위(전략)이 추상화되어 교체가 가능했다. 하지만 여기에서 만약 상태(state)를 추상화한 인터페이스를 정의하고 각 인터페이스가 행위에 대해서 정의한다면 어떻게 변할 수 있을까?

그에 관한 클래스 다이어 그램을 한번 그려보았다.


전구 클래스는 상태를 추상화한 State 인터페이스를 참조하고 있고 각각의 인터페이스 구현체(즉, 상태)들은 자신의 상태에서 어떠한 행위가 발생되었을 때의 로직을 구현해야한다. 상태가 추가되거나, 수정됬을 때 원래 코드를 수정하지 않으므로 OCP에도 만족하는 충분히 객체지향적인 설계가 되었다. 상태는 단 하나만 존재하므로 싱글턴 패턴을 사용한다.

class Light {
    private State state = new StateOff();

    public void setState(State state) {
        this.state = state;
    }

    public void onButtonPushed () {
        state.onButtonPushed(this);
    }

    public void offButtonPushed() {
        state.offButtonPushed(this);
    }
}

interface State {
    void onButtonPushed(Light light);
    void offButtonPushed(Light light);
}

class StateOff implements State {
    private static StateOff instance = new StateOff();

    public static State getInstance() {
        return instance;
    }

    @Override
    public void onButtonPushed(Light light) {
        System.out.println("Turn on.");
        light.setState(StateOn.getInstance());
    }

    @Override
    public void offButtonPushed(Light light) {
        System.out.println("Do nothing.");
    }
}

public class StateOn implements State {
    private static StateOn instance = new StateOn();

    public static State getInstance() {
        return instance;
    }

    @Override
    public void onButtonPushed(Light light) {
        System.out.println("Do sleeping");
        light.setState(StateSleeping.getInstance());
    }

    @Override
    public void offButtonPushed(Light light) {
        System.out.println("Turn off");
        light.setState(StateOff.getInstance());
    }
}

class StateSleeping implements State {
    private static StateSleeping instance = new StateSleeping();

    public static State getInstance() {
        return instance;
    }

    @Override
    public void onButtonPushed(Light light) {
        System.out.println("more brightly!");
        light.setState(StateOn.getInstance());
    }

    @Override
    public void offButtonPushed(Light light) {
        System.out.println("Turn off this light.");
        light.setState(StateOff.getInstance());
    }
}

Summary

사람은 아프거나 행복할 때, 슬플 때, 기쁠 때등 말하는 방법, 일의 효율, 걷는 방식, 활동량 모두 다르다. 이는 상태에 따라서 행위가 명확히 존재하므로 사람자체가 그것을 제어하는 것이 아닌 상태에 행위를 위임하여 실행토록 하는 것이다.

스테이트 패턴은 어떤 행위를 수행할 때 각 상태에 행위를 수행하도록 위임하는 것이다.