상태 패턴(State Pattern)

uglyduck.dev·2020년 9월 29일
5

개념 모아 🗂

목록 보기
34/40

정의

  • 어떤 행위를 수행할 때 상태에 행위를 수행하도록 위임
  • 시스템의 각 상태를 클래스로 분리해 표현
  • 각 클래스에서 수행하는 행위들을 메서드로 구현
  • 외부로부터 캡슐화하기 위해 인터페이스를 생성하여 시스템의 각 상태를 나타나는 클래스로 실체화

state

  • State: 시스템의 모든 상태에 공통의 인터페이스를 제공한다. 따라서 이 인터페이스를 실체화한 어떤 상태 클래스도 기존 상태 클래스를 대신해 교체해서 사용할 수 있다.
  • State1, State2, State3: Context 객체가 요청한 작업을 자신의 방식으로 실제 실행한다. 대부분의 경우 다음 상태를 결정해 상태 변경을 Context 객체에 요청하는 역할도 수행한다.
  • Context: State를 이용하는 역할을 수행한다. 현재 시스템의 상태를 나타내는 상태 변수 (state)와 실제 시스템의 상태를 구성하는 여러 가지 변수가 있다. 또한 각 상태 클래스에서 상태 변경을 요청해 상태를 바꿀 수 있도록 하는 메서드(setState)가 제공된다. Context 요소를 구현한 클래스의 request 메서드는 실제 행위를 실행하는 대신 해당 상태 객체에 행위 실행을 위임한다.

상태 머신 다이어그램

  • UML에서의 상태와 상태 변화를 모델링하는 도구
    windmill
  • 바람 세기를 선택하거나 회전 등을 할 수 없고 정해진 바람 세기로만 동작하는 선풍기(단순한 기능)
  • 사각형: 상태(state)
  • 화살표: 전이(state transition)

상태(state)

  • 객체가 시스템에 존재하는 동안, 즉 객체의 라이프 타임 동안 객체가 가질 수 있는 어떤 조건이나 상황
  • ex) 객체가 어떤 상태에 있는 동안 어떤 액티비티 등을 수행하거나 특정 이벤트 발생을 기다리는 것
  • 시작 상태: 객체가 시작하는 처음 상태
    • 시작 상태에서의 진입은 객체를 새로 생성하는 이벤트만 명시하거나 아예 어떤 것도 명시하지 않아야 함
  • 상태 진입: 객체의 한 상태에서 다른 상태로 이동하는 것
    • 특정 이벤트가 발생한 후 명세된 조건을 만족한 경우에 이루어진다.

해석

  • 선풍기는 기본적으로 OFF 상태에서 시작한다.
  • OFF 상태에서 사용자가 선풍기 스위치를 켜면 switch_on 이벤트를 발생시킨다. 이때 전원이 들어온 상태라면(power_exists 조건) ON 상태로 진입한다. 이때 turnon 액션을 실행하게 된다.
  • OFF 상태에서 사용자가 선풍기 스위치를 켜면 switch_on 이벤트를 발생시킨다. 이때 전원이 들어오지 않은 상태라면(power_exists 조건) OFF 상태에 머무른다.
  • 사용자가 ON 상태에서 동작 버튼을 누르면 run 이벤트를 발생시키고 WORKING 상태로 진입한다. 이때 operate 액션을 실행하게 된다.
  • 선풍기가 ON 상태나 WORKING 상태에 머무를 때 사용자가 스위치를 끄면 switch_off 이벤트가 발생하고 이 이벤트로 인해 OFF 상태로 진입한다.

형광등 만들기

전제조건

  • 형광등(Light 클래스)을 만들려면 형광등의 행위를 분석할 필요가 있다.
  • 꺼져 있을 때 On 버튼을 누르면 켜지고, 켜져 있을 때 Off 버튼을 누르면 꺼진다.
  • 만약 켜져 있는 상태에서 On 버튼을 누르면 그대로 켜져 있고, 꺼져 있는 상태에서 Off 버튼을 눌러도 아무런 변화가 없다.
    light1
  • OFF, ON: 형광등의 상태
  • off_button_pushed, on_button_pushed: 형광등의 Off 버튼과 On 버튼의 상태
private static int ON = 0;
private static int OFF = 1;
  • 형광등의 상태를 저장하는 변수
private int state;

구현

Light 클래스

public class Light{
    private static int ON = 0; // 형광등이 켜진 상태
    private static int OFF = 1; // 형광등이 꺼진 상태
    pricate int state; // 형광등의 현재 상태
    
    public Light(){
        state = OFF; // 형광등 초기 상태는 꺼져 있는 상태임
    }
    
    public void on_button_pushed(){
        if( state == ON ){
            System.out.println("반응 없음");
        }
        else { // 형광등이 꺼져 있을 때 On 버튼을 누르면 켜진 상태로 전환
            System.out.println("Light On!");
            state = ON;
        }
    }
    
    public void off_button_pushed(){
        if( state == OFF) {
            System.out.println("반응 없음");
        }
        else { // 형광등이 켜져 있을 때 OFF 버튼을 누르면 꺼진 상태로 전환됨
            System.out.println("Light Off!");
            state = OFF;
        }
    }
}

Client 클래스

public class Client{
    public static void main(String[] args){
        Light light = new Light();
        light.off(); // 반응 없음
        light.on();
        light.off();
    }
}

문제점

  • 형광등에 새로운 상태를 추가할 떄, 가령 형광등에 '취침등' 상태를 추가하려면?

설계

light2

  • 취침등 상태를 나타내는 상수인 SLEEPING를 추가
private static int SLEEPING = 2;
  • on_button_pushed 상태와 off_button_pushed 메서드 안에서 현재 상태 값이 SLEEPING 변수 값과 같은지를 검사하고 값이 같다면 요구사항에 맞게 수정
public void on_button_pushed(){
    if(state == ON) { // 형광등이 켜져 있는 경우에 On 버튼을 누르면 취침등 상태로 전환됨
        System.out.println("취침등 상태");
        state = SLEEPING;
    }
    else if(state == SLEEPING) { // 형광등이 취침등 상태에 있는 경우
        System.out.println("Light On!"); // On 버튼을 누르면 켜진 상태로 전환됨
        state = ON;
    }
    else { // 상태가 꺼져 있는 경우에 On 버튼을 누르면 켜진 상태로 전환됨
        System.out.println("Light On!");
        state = ON;
    }
}

요구사항 반영

public class Light{
    private static int ON = 0;
    private static int OFF = 1;
    private static int SLEEPING = 2;
    private int state;
    
    public Light(){
        state = OFF; // 초기 상태는 형광등이 꺼져 있는 상태
    }
    
    public void off_button_pushed(){
        if(state == OFF) { // 형광등이 꺼져 있는 경우에 OFF 버튼을 누르면 상태 전환 없음
            System.out.println("반응 없음");
        }
        else if(state == SLEEPING) { // 형광등이 취침등 상태에 있는 경우
            System.out.println("Light OFF!"); // Off 버튼을 누르면 OFF로 전환됨
            state = OFF;
        }
        else {
            System.out.println("Light Off!");
            state = OFF;
        }
    }
}

public void on_button_pushed(){
    if(state == ON){ // 형광등이 켜져 있는 경우에 On 버튼을 누르면 취침등 상태로 전환됨
        System.out.println("취침등 상태");
        state = SLEEPING;
    } 
    else if(state == SLEEPING){ // 형광등이 취침등 상태에 있는 경우
        System.out.println("Light On!");
        state = ON;
    }
    else { // 꺼져 있는 상태라면 On 버튼을 눌렀을 때 켜진 상태로 전환됨
        System.out.println("Light On!");
        state = ON;
    }
}
  • 새로운 상태가 추가되는 경우에 상태 변화를 초래하는 모든 메서드에 이를 반영하기 위해 코드 수정은 불가피함

해결책

  • 무엇이 변하는가?
  • 변하는 것에 따른 캡슐화
  • 상태 변화에 독립적으로 코드를 수정하는게 목표
  • 상태를 클래스로 분리
  • 의존적인 행위도 상태 클래스에 두어 특정 상태에 따른 행위 구현

개선된 설계, 구현

stateMachineDiagram

  • 스트래티지 패턴과 구조가 매우 흡사함
  • Light 클래스에서 구체적인 상태 클래스가 아닌 추상화된 State 인터페이스만 참조하므로 현재 어떤 상태에 있는지와 무관하게 코드를 작성할 수 있음
  • Light 클래스에서는 상태 클래스 작업만 위임하면 됨

State 인터페이스

interface State{
    public void on_button_pushed(Light light);
    public void off_button_pushed(Light light);
}

ON 클래스

public class ON implements State{
    public void on_button_pushed(Light light){
        System.out.println("반응 없음");
    }
    
    public void off_button_pushed(Light light){
        System.out.println("Light Off!");
        light.setState(new OFF(light));
    }
}

OFF 클래스

public class OFF implements State{
    public void on_button_pushed(Light light){
        System.out.println("Light On!");
        light.setState(new ON(light));
    }
    
    public void off_button_pushed(Light light){
        System.out.println("반응 없음");
    }
}

Light 클래스

public class Light{
    private State state;
    
    public Light(){
        state = new OFF();
    }
    
    public void setState(State state){
        this.state = state;
    }
    
    public void on_button_pushed(){
        state.on_button_pushed(this);
    }
    
    public void off_button_pushed(){
        state.off_button_pushed(this);
    }
}
  • state 변수를 통해 현재 시스템의 상태 객체를 참조
  • Light 클래스는 시스템이 어떤 상태에 있는지 무관

현 코드의 문제점

  • 상태 변화가 생길 때마다 새로운 상태 객체 생성
    • 메모리 낭비, 성능 저하 야기

개선된 싱글턴 패턴 ON, OFF 클래스로의 변경

ON 클래스

public class ON implements State{
    private static ON on = new ON(); // ON 클래스의 인스턴스로 초기화됨
    private ON() { }
    
    public static ON getInstance() { // 초기화된 ON 클래스의 인스턴스를 반환함
        return on;
    }
    
    public void on_button_pushed(Light light){ // ON 상태일 때 On 버튼을 눌러도 변화 없음
        System.out.println("반응 없음");
    }
    
    public void off_button_pushed(Light light){
        light.setState(OFF.getInstance());
        System.out.println("Light Off!");
    }
}

OFF 클래스

public class OFF implements State{
    private static OFF off = new OFF(); // OFF 클래스의 인스턴스로 초기화됨
    private OFF() { }
    
    public static OFF getInstance() { // 초기화된 OFF 클래스의 인스턴스를 반환함
        return off;
    }
    
    public void on_button_pushed(Light light){ // Off 상태일 때 On 버튼을 눌러도 On 상태임
        light.setState(ON.getInstance());
        System.out.println("Light On!");
    }
    
    public void off_button_pushed(Light light){ // Off 상태일 때 Off 버튼을 눌러도 변화 없음
        System.out.println("반응 없음");
    }
}

Reference

  • 정인상,채흥석, 『JAVA 객체 지향 디자인 패턴』, 한빛미디어(2014.4.2), 218p~232p
profile
시행착오, 문제해결 그 어디 즈음에.

2개의 댓글

comment-user-thumbnail
2021년 11월 19일

글 정말 잘쓰셨네요. 정말 잘보았습니다. 감사합니다.

답글 달기
comment-user-thumbnail
2022년 3월 18일

내용이 기똥차네요! 감사합니다👍

답글 달기