Priv's Blog

7. 스테이트 패턴 본문

Dev. Study Note/Design Pattern

7. 스테이트 패턴

Priv 2022. 7. 3. 23:10


 

 

1. 상태 머신 다이어그램

많은 시스템들은 다양한 상태가 있고, 그 상태에 따라 다양한 행동을 한다.

UML에서도 이러한 상태와 그 상태의 변화를 모델링하는 도구로써 상태 머신 다이어그램을 제공한다.

아래 그림 7-1은 선풍기의 상태를 나타낸 것이다.

이 다이어그램에서 모서리가 둥근 사각형은 상태를 나타내고, 상태 사이에 화살표를 사용해 상태 전이를 나타냈다.

여기서 상태란, 객체가 시스템에 존재하는 동안(객체의 라이프 타임 동안) 객체가 가질 수 있는 어떠한 조건이나 상황을 의미한다.

예를 들면 객체가 어떤 상태에 있는 동안 어떤 행동을 수행하거나 특정 이벤트가 발생하기를 기다릴 수 있다.

기본 상태 머신 다이어그램에서 흔히 볼 수 있는 것은 시작과 종료 상태이다.

위 그림 7-1에서 왼쪽 하단에 있는 검은색 동그라미가 시작 상태를 나타낸다.

보통 시작 상태에서의 진입은 객체를 새로 생성하는 이벤트만 명시하거나, 아예 어떠한 것도 명시하지 않아야 한다.

시작 상태에서 다른 상태로 진입하는 것은 1개만 존재할 수 있으며, 액션을 수행한다.

상태 진입이란, 객체의 한 상태에서 다른 상태로 이동하는 것을 말한다.

보통 상태 진입은 특정 이벤트 발생 후, 명세된 조건 만족 시에 이루어진다.

 


 

2. 형광등 만들기

형광등(Light 클래스)을 만들려면 우선 형광등의 행위를 분석해야 한다.

  • 형광등이 꺼져 있을 경우, 외부에서 ON 버튼을 누르면 형광등이 켜진다.
  • 형광등이 켜져 있을 경우, OFF 버튼을 누르면 형광등이 꺼진다.
  • 형광등이 켜져 있는 상태에서 ON 버튼을 누르거나, 형광등이 꺼져 있는 상태에서 OFF 버튼을 누르면 아무런 변화가 없다.

아래 그림 7-2는 형광등을 UML 상태 머신 다이어그램으로 표현한 것이다.

package P7;

public class Light {
    private static int ON = 0;
    private static int OFF = 1;
    private int state;


    public Light() {
        this.state = OFF;
    }

    public void onButtonPushed() {
        if (this.state == ON) {
            System.out.println("반응 없음");
        }
        else {
            System.out.println("형광등 켜짐");
            this.state = ON;
        }
    }

    public void offButtonPushed() {
        if (this.state == OFF) {
            System.out.println("반응 없음");
        }
        else {
            System.out.println("형광등 꺼짐");
        }
    }
}

 


 

3. 문제점

여기서 만약 형광등에 새로운 상태를 추가하고자 한다면 어떻게 될까?

형광등이 켜져 있을 때 ON 버튼을 누르면 아무런 반응이 없었지만, 이제는 '취침등' 상태로 변경되도록 만들어야 한다고 가정해 보자.

취침등 상태에 있을 때 ON 버튼을 누르면 형광등이 다시 켜지고, OFF 버튼을 누르면 형광등이 꺼져야 한다.

이를 상태 머신 다이어그램으로 나타내면 다음과 같다.

package P7;

public class Light {
    private static int ON = 0;
    private static int OFF = 1;
    private static int SLEEPING = 2;
    private int state;


    public Light() {
        this.state = OFF;
    }

    public void onButtonPushed() {
        if (this.state == ON) {
            System.out.println("취침등 켜짐");
        }
        else if (this.state == SLEEPING) {
            System.out.println("형광등 켜짐");
            this.state = ON;
        }
    }

    public void offButtonPushed() {
        if (this.state == OFF) {
            System.out.println("반응 없음");
        }
        else if (this.state == SLEEPING) {
            System.out.println("형광등 꺼짐");
        }
    }
}

상태 진입이 복잡한 조건문이 내포되어 있기 때문에 현재 상태로는 상태 변화를 파악하기에 용이하지 않다.

또한 새로운 상태가 추가되는 경우, 상태 변화를 초래하는 모든 메서드에 이를 반영하고자 코드를 수정해야 한다는 문제점도 있다.

이와 같이 복잡한 조건문에 상태 변화가 숨어 있는 경우, 상태 변화가 어떻게 이루어지는지 이해하기가 어렵고, 새로운 상태 추가에 맞춰 모든 메서드를 수정해야 한다.

 


 

4. 해결책

이번에도 역시 무엇이 변화하는지를 찾아서 이를 캡슐화하는 것이 중요하다.

현재 목표는 시스템의 상태와 상관없이 구성하고 상태 변화에서 독립적이게 코드를 수정하는 것이다.

이를 달성하기 위해서는 상태를 클래스로 분리해 캡슐화하도록 한다.

또한 상태에 의존적인 행위들도 상태 클래스에 같이 두어서 특정 상태에 따른 행위를 구현하도록 바꾼다.

그림 7-4를 보면 스트래티지 패턴과 구조가 동일하다는 것을 알 수 있다.

Light 클래스에서 구체적인 상태 클래스가 아닌 추상화된 State 인터페이스만 참조하기 때문에 현재 어떤 상태에 있는지와 무관하게 코드를 작성할 수 있다.

즉, 어떤 전략을 사용 하든지와 무관하게 코드를 작성했던 것과 동일한 것이다.

이를 코드로 구현하면 다음과 같다.

package P7;

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

public class On implements State {
    private static On on = new On();
    

    private On() {
    }

    public static On getInstance() {
        return on;
    }

    public void onButtonPushed(Light light) {
        System.out.println("반응 없음");
    }

    public void offButtonPushed(Light light) {
        light.setState(Off.getInstance());
        System.out.println("형광등 꺼짐");
    }
}
package P7;

public class Off implements State {
    private static Off off = new Off();
    

    private Off() {
    }

    public static Off getInstance() {
        return off;
    }

    public void onButtonPushed(Light light) {
        light.setState(On.getInstance());
        System.out.println("형광등 꺼짐");
    }

    public void offButtonPushed(Light light) {
        System.out.println("반응 없음");
    }
}
package P7;

public class Light {
    private State state;


    public Light() {
        //this.state = new Off();
    }

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

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

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

상태 변화가 생길 때마다 새로운 상태 객체가 생성되지 않도록 싱글턴 패턴까지 적용해 주었다.

 


 

5. 스테이트 패턴

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

즉, 스테이트 패턴은 상태에 따라 동일한 작업이 다른 방식으로 실행될 때, 해당 상태가 작업을 수행하도록 위임하는 디자인 패턴이다.

이를 위해 시스템의 각 상태를 클래스로 분리해 표현하고, 각 클래스에서 수행하는 행위들을 메서드로 구현한다.

이러한 상태들은 외부로부터 캡슐화하기 위해 인터페이스를 만들어 시스템의 각 상태를 나타내는 클래스로 실체화한다.

여기서 인터페이스의 메서드는 각 상태에서 수행해야 하는 행위들이다.

스테이트 패턴을 여태까지 살펴본 형광등 예제에 접목시키면 아래와 같다.

 


 


수고하셨습니다!


'Dev. Study Note > Design Pattern' 카테고리의 다른 글

9. 옵서버 패턴  (0) 2022.07.03
8. 커맨드 패턴  (0) 2022.07.03
6. 싱글턴 패턴  (0) 2022.07.03
5. 스트래티지 패턴  (0) 2022.07.03
4. 디자인 패턴  (0) 2022.07.03
Comments