본문 바로가기

ComputerScience/DesignPattern

[DesignPattern] 상태 패턴 (State Pattern)

상태 패턴 (State Pattern)

  • 상태 패턴을 사용하면 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.

상태를 별도의 클래스로 캡슐화한 다음 현재 상태를 나타내는 객체에게 행동을 위임하므로 내부 상태가 바뀔 때 행동이 달라지게 된다는 사실을 쉽게 알 수 있습니다.

클라이언트의 관점에서 지금 상태에 따라 사용하는 객체의 행동이 완전히 달라져 마치 그 객체가 다른 클래스로부터 만들어진 객체처럼 느껴집니다.

상태패턴을 사용하지 않으면, 상태마다 모든 분기를 if문을 사용하여 분기 처리하여야 합니다

즉, 확장에 닫혀있게 되고, 상태패턴을 이용하면 확장에 비교적 열려있게 됩니다.

 

상태 패턴의 구성은 다음과 같습니다.

 

Context

현재 상태를 구성으로 저장하고 있다. 현재 상태에게 행동을 위임한다.


State

모든 구상 상태 클래스의 공통 인터페이스다. 모든 상태 클래스에서 같은 인터페이스를 구현하므로 바꿔 쓸 수 있다.

handle()은 실질적으로 Context의 상태를 변경한다.


ConcreteState

Context로부터 전달된 요청을 자기 나름의 방식으로 구현해서 처리한다. 이렇게 구현하여 Context의 상태를 바꾸면 행동도 바뀌게 된다.

 

이미지 출처: https://en.wikipedia.org/wiki/State_pattern

위 클래스 다이어그램에 따르면, Context 클래스는 상태별 동작을 직접 구현하지 않습니다. 

대신에 Context는 상태별 동작을 수행하기 위해 State 인터페이스를 참조합니다(state.operation()). 

이렇게 함으로써 Context는 상태별 동작이 어떻게 구현되었는지에 대해 독립적입니다.

State1(ConcreteState) 및 State2(ConcreteState) 클래스는 State 인터페이스를 구현하며, 각 상태에 대한 상태별 동작을 구현(캡슐화)합니다.

 

UML 순서 다이어그램은 런타임 상호 작용을 보여줍니다
Context 객체는 상태별 동작을 다른 State 객체에 위임합니다. 먼저, Context는 현재 (초기) 상태 객체(State1)에서 handle(this)를 호출하며, 이는 작업을 수행하고 Context의 현재 상태를 State2로 변경하기 위해 Context의 setState(State2)를 호출합니다. 다음으로, Context는 다시 현재 상태 객체(State2)에서 handle(this)를 호출하며, 이는 작업을 수행하고 Context의 현재 상태를 State1로 변경합니다.

이러한 설계를 통해 Context는 현재 상태에 관계없이 일관된 방식으로 상태별 동작을 수행할 수 있습니다. 

 

예시를 봅시다.

 

Context: GumballMachine

public class GumballMachine {

    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;

    State state;
    int count = 0;

    public GumballMachine(int numberGumballs) {
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);

        this.count = numberGumballs;
        if (numberGumballs > 0) {
            state = noQuarterState;
        } else {
            state = soldOutState;
        }
    }

    public void insertQuarter() {
        state.insertQuarter();
    }

    public void ejectQuarter() {
        state.ejectQuarter();
    }

    public void turnCrank() {
        state.turnCrank();
        state.dispense();
    }

    void releaseBall() {
        System.out.println("A gumball comes rolling out the slot...");
        if (count > 0) {
            count = count - 1;
        }
    }

    int getCount() {
        return count;
    }

    void refill(int count) {
        this.count += count;
        System.out.println("The gumball machine was just refilled; its new count is: " + this.count);
        state.refill();
    }

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

    public State getState() {
        return state;
    }

    public State getSoldOutState() {
        return soldOutState;
    }

    public State getNoQuarterState() {
        return noQuarterState;
    }

    public State getHasQuarterState() {
        return hasQuarterState;
    }

    public State getSoldState() {
        return soldState;
    }

    public String toString() {
        StringBuffer result = new StringBuffer();
        result.append("\nMighty Gumball, Inc.");
        result.append("\nJava-enabled Standing Gumball Model #2004");
        result.append("\nInventory: " + count + " gumball");
        if (count != 1) {
            result.append("s");
        }
        result.append("\n");
        result.append("Machine is " + state + "\n");
        return result.toString();
    }
}

위 Context 클래스에서는 state 변수에 현재 상태를 저장합니다.

또, 존재하는 각각의 상태를 인스턴스 변수로 모두 갖고 있어 각각의 ConcreteState에서 상태를 변경하고 싶을 때 Context.get~()로 상태를 가져와 Context.setState()로 현재 상태를 동작에 맞게 변경할 수 있습니다.

즉, ConcreteState도 Context를 인스턴스 변수로 가지고 있습니다.

getter 메소드로 굳이 가져오는 이유는 ConcreteState 간의 의존성을 최소화하기 위함입니다.

 

State: State

public interface State {

    public void insertQuarter();

    public void ejectQuarter();

    public void turnCrank();

    public void dispense();

    public void refill();
}

 

ConcreteState: HasQuarterState

public class HasQuarterState implements State {
    GumballMachine gumballMachine;

    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    public void insertQuarter() {
        System.out.println("You can't insert another quarter");
    }

    public void ejectQuarter() {
        System.out.println("Quarter returned");
        gumballMachine.setState(gumballMachine.getNoQuarterState());
    }

    public void turnCrank() {
        System.out.println("You turned...");
        gumballMachine.setState(gumballMachine.getSoldState());
    }

    public void dispense() {
        System.out.println("No gumball dispensed");
    }

    public void refill() {
    }

    public String toString() {
        return "waiting for turn of crank";
    }
}

 

각 상태는 그 자체의 클래스로 구현되어 있으며, 각 클래스는 해당 상태에서 수행되어야 하는 동작을 캡슐화합니다.

이로써 코드의 모듈화가 이루어져 각 상태에 대한 구현이 서로 독립적으로 관리될 수 있습니다.

각 상태 클래스는 확장에 열려 있고(modification closed), 동시에 변경에 대해서는 닫혀 있습니다.

즉, 새로운 상태 클래스를 추가하면서 기존 코드를 수정하지 않아도 되며,

이는 소프트웨어의 확장성을 높이는 객체지향 설계의 원칙인 "개방-폐쇄 원칙(Open/Closed Principle)"을 따른 것입니다.

상태의 변경: Context vs ConcreteState

상태 전환은 State 클래스로 제어할 수도 있고 Context 클래스로 제어할 수도 있습니다.

예시 코드에서는 상태의 변경을 구상 상태 클래스 ConcreteState가 맡았습니다. (setState 함수)

상태 전환이 고정되어 있다면 이를 Context가 책임져도 무방합니다.


상태 전환의 흐름을 결정하는 코드를 어느 쪽에 넣는지에 따라서 시스템이 점점 커지게 될 때, 어떤 클래스가 변경에 닫혀 있게 되는지도 결정됩니다.
즉, ConcreteState가 맡게되면 ConcreteState가 변경에 닫혀있게 되고, Context가 맡게되면 Context가 변경에 닫혀있게 됩니다.

그때그때 객체의 상태를 바꾼다고? 전략 패턴이랑 뭐가 달라?

상태 패턴

  • 주로 객체가 내부 상태에 따라 동작이 동적으로 변경되어야 하는 경우에 사용됩니다.
  • 객체의 상태에 따라 객체의 행동을 캡슐화하고, 상태 전환을 쉽게 만듭니다.
  • 객체가 여러 상태를 가지고 있고, 상태에 따라 객체의 동작이 동적으로 변경되어야 할 때 사용됩니다. 예를 들어, 주문 상태(처리 중, 완료 등)에 따라 주문 객체의 동작이 다르게 구현되어야 할 때 상태 패턴을 사용할 수 있습니다.

전략 패턴

  • 주로 알고리즘 또는 행동을 정의하고 이를 캡슐화하여 동적으로 교환 가능한 구성 요소로 만들 때 사용됩니다.
  • 알고리즘을 정의하고 이를 인터페이스를 통해 외부에 노출하여 알고리즘을 독립적으로 변경할 수 있게 합니다.
  • 알고리즘이나 전략이 동적으로 교환 가능해야 하며, 객체가 여러 다른 알고리즘을 선택적으로 사용해야 할 때 사용됩니다. 예를 들어, 정렬 알고리즘을 바꿀 수 있는 정렬 컨텍스트에서 전략 패턴을 사용할 수 있습니다.

참고 자료

https://en.wikipedia.org/wiki/State_pattern

 

State pattern - Wikipedia

From Wikipedia, the free encyclopedia Software design pattern The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machin

en.wikipedia.org

https://github.com/IT-Book-Organization/HeadFirst-DesignPattern/blob/main/Chapter_10/README.md