
상태 패턴은 객체가 특정 상태에 따라 행위를 달리하는 상황에서, 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌, 상태를 객체화하여 상태가 행동을 할 수 있도록 위임하는 패턴을 말한다. 객체 지향 프로그래밍에서의 클래스는 꼭 어떠한 형태가 있는 물체의 데이터만 표현할 수 있는게 아니라 동작, 행위들도 클래스로 묶어 표현할 수 있다. 그렇기 때문에! 상태를 클래스로 표현하면 클래스를 교체해서 상태의 변화를 표현할 수 있고, 객체 내부 상태 변경에 따라 객체의 행동을 상태의 특화된 행동들로 분리해 낼 수 있으며, 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다.
상태(State)? 객체가 가질 수 있는 조건이나 상황, 예를 들면 에어컨이 꺼진 상태에서는 희망 온도가 바뀌지 않는다. 하지만 켜진 상태에서는 희망 온도가 변한다. 즉, 에어컨의 전춴 상태에 따라 행동이 바뀐다. 이렇게 객체의 상태에 따라 행위를 달리하는 상황에서 사용하는 최적의 패턴이 state pattern이라고 보면 된다.
전략 패턴이 전략 알고리즘을 클래스로 표현한 패턴이라면, 상태 패턴은 객체 상태를 클래스로 표현한 패턴이다! 그래서 그런지 상태 패턴의 클래스 다이어그램을 보면 전략 패턴과 매우 유사한데, 전략 패턴은 전략을 객체화한 것이고, 상태 패턴은 상태를 객체화 한것인데 결국 둘 다 클래스 묶음인 것은 똑같기 때문이다.

State Interface : 상태를 추상화한 고수준 모듈ConcreteState : 구체적인 각각의 상태를 클래스로 표현. State 역할로 결정되는 인터페이스를 구체적으로 구현 + 다음 상태가 결정되면 Context에 상태 변경을 요청Context : State를 이용하는 시스템. 시스템 상태를 나타내는 State 객체를 합성하여 가지고 있음. 요청을 받으면 State 객체에 행위 실행을 위임상태 클래스는 싱글톤 클래스로 구현한다. 전략 패턴의 전략 객체 같은 경우 매개 값에 따라 알고리즘 수행 형태가 달라질 수 있지만, 상태는 그 객체의 현 폼을 나타내는 것이기 때문에 대부분의 상황에서 유일하게 있어야 한다.

interface AbstractState {
void requestHandle(Context cxt);
}
class ConcreteStateA implements AbstractState {
@Override
public void requestHandle(Context cxt) {}
}
class ConcreteStateB implements AbstractState {
@Override
public void requestHandle(Context cxt) {
// 상태에서 동작을 실행한 후 바로 다른 상태로 바꾸기도 함
// 예를 들어 전원 on 상태에서 끄기 동작을 실행한후 객체 상태를 전원 off로 변경 하듯이
cxt.setState(ConcreteStateC.getInstance());
}
}
class ConcreteStateC implements AbstractState {
@Override
public void requestHandle(Context cxt) {}
}
class Context {
AbstractState state; // composition
void setState(AbstractState state) {
this.state = state;
}
// 상태에 의존한 처리 메소드로서 state 객체에 처리를 위임함
void request() {
state.requestHandle(this);
}
}

class Client {
public static void main(String[] args) {
Context context = new Context();
// 1. StateA 상태 설정
context.setState(new ConcreteStateA());
// 2. 현재 StateA 상태에 맞는 메소드 실행
context.request();
// 3. StateB 상태 설정
context.setState(new ConcreteStateB());
// 4. StateB 상태에서 또다른 StateC 상태로 변경
context.request();
// 5. StateC 상태에 맞는 메소드 실행
context.request();
}
}
이러한 상태 패턴을 사용하게 되면..
전략 패턴과 유사한 단점을 지니고 있다.
문을 여는 버튼이 있다고 생각해보자.
문을 여는 버튼을 누르면 나타나는 상태 변화는 아래와 같이 3단계와 같다.
보통이라면! 상태에 따른 동작 분기는 if 문이나 switch 문으로 처리하기 마련이다.
using UnityEngine;
public class OpenDoor
{
public static readonly int OPEN = 0;
public static readonly int CLOSE = 1;
public static readonly int LOCK = 2;
private int DoorState;
public OpenDoor() {
this.DoorState = OpenDoor.CLOSE; //닫은 상태로 시작
}
public void changeState(int state) { //상태 전환
this.DoorState = state;
}
public void DoorOpenButtonClick() {
if (DoorState == OPEN) {
Debug.Log("문 CLOSE");
changeState(OpenDoor.CLOSE);
}
else if (DoorState == CLOSE) {
Debug.Log("문 OPEN");
changeState(OpenDoor.OPEN);
}
else if (DoorState == LOCK) {
Debug.Log("문 OPEN");
changeState(OpenDoor.OPEN);
}
}
public void TryUnlockDoor() { //자물쇠로 문을 따기 (LOCK에서만 동작)
if (DoorState == OPEN) {
throw new System.Exception("문이 UNLOCK 된 상태입니다.");
}
else if (DoorState == CLOSE) {
throw new System.Exception("문이 UNLOCK 된 상태입니다.");
}
else if (DoorState == LOCK) {
Debug.Log("잠금을 해제할 수 있습니다.");
}
}
public void setLockState() {
Debug.Log("문을 잠궜습니다.");
changeState(OpenDoor.LOCK);
}
public void PrintCurrentState() {
if (DoorState == OPEN) {
Debug.Log("문은 현재 OPEN 상태입니다.");
}
else if (DoorState == CLOSE) {
Debug.Log("문은 현재 CLOSE 상태입니다.");
}
else if (DoorState == LOCK) {
Debug.Log("문은 현재 LOCK 상태입니다.");
}
}
}
using UnityEngine;
public class Player : MonoBehaviour
{
void Start() {
OpenDoor door = new OpenDoor();
door.PrintCurrentState();
// 문 열기 : CLOSE → OPEN
door.DoorOpenButtonClick();
door.PrintCurrentState();
// 문 잠구기 : OPEN → LOCK
door.setLockState();
door.PrintCurrentState();
door.TryUnlockDoor(); // 문 따기 시도
door.PrintCurrentState();
// 문 다시 열기 : LOCK → OPEN
door.DoorOpenButtonClick();
door.PrintCurrentState();
// 문 닫기 : OPEN → CLOSE
door.DoorOpenButtonClick();
door.PrintCurrentState();
}
}

그러나 상태 변수를 써서 굉장히 쉽게 해결한 것 처럼 보이지만 협업이나 실무에서 전혀 좋지 않은 방법이다. 좋지 않은 방법인 이유?
상태 변수는 변수와 행위와의 결합을 만들어 내고, 이 과정에서 조건문들을 부수적으로 생산해 내기 때문이다. 언적으로 허용되는 한 상태 변수는 최대한 없애주는 것이 좋다.
enum을 써도 마찬가지이다. 핵심은
상태 상수화를 자제하라는 것이다
상태 패턴을 적용해보자! 상태를 개체화해야한다. 문의 상태 3가지를 모두 클래스로 구성한다. 이때 인터페이스나 추아 클래스로 묶어 추상화/캡슐화를 한다. 상태를 클래스로 분리하였으니, 상태에 따른 행동 메소드도 각 상태 클래스마다 구현을 해준다. 코드의 전체 라인수가 길어지고 괜히 클래스도 많아진 것 같지만, 오히려 이러한 벙법이 지속적인 유지보수를 용이하게 해준다.

using System;
using UnityEngine;
public interface DoorState
{
void DoorOpenButtonClick(DoorContext cxt);
void TryUnlockDoor(DoorContext cxt);
string currentState();
}
//OPEN State
class OpenState : DoorState {
public void DoorOpenButtonClick(DoorContext cxt) {
Debug.Log("문 CLOSE");
cxt.ChangeState(new CloseState());
}
public void TryUnlockDoor(DoorContext cxt) {
throw new System.Exception("문이 UNLOCK 된 상태입니다.");
}
public String currentState() {
return "문은 현재 OPEN 상태입니다.";
}
}
//CLOSE State
class CloseState : DoorState {
public void DoorOpenButtonClick(DoorContext cxt) {
Debug.Log("문 OPEN");
cxt.ChangeState(new OpenState());
}
public void TryUnlockDoor(DoorContext cxt) {
throw new System.Exception("문이 UNLOCK 된 상태입니다.");
}
public String currentState() {
return "문은 현재 CLOSE 상태입니다.";
}
}
//LOCK State
class LockState : DoorState {
public void DoorOpenButtonClick(DoorContext cxt) {
Debug.Log("문을 열 수 없습니다.");
}
public void TryUnlockDoor(DoorContext cxt) {
Debug.Log("문을 땄습니다!");
cxt.ChangeState(new OpenState());
}
public String currentState() {
return "문은 현재 LOCK 상태입니다.";
}
}
using UnityEngine;
public class DoorContext
{
DoorState doorState;
public DoorContext() {
this.doorState = new CloseState();
}
public void ChangeState(DoorState state) {
this.doorState = state;
}
public void setLockState() {
Debug.Log("문을 잠궜습니다.");
ChangeState(new LockState());
}
public void DoorOpenButtonClick() {
doorState.DoorOpenButtonClick(this);
}
public void TryUnlockDoor() {
doorState.TryUnlockDoor(this);
}
public void PrintCurrentState() {
Debug.Log(doorState.currentState());
}
}
using UnityEngine;
public class Player : MonoBehaviour
{
void Start() {
DoorContext door = new DoorContext();
door.PrintCurrentState();
// 문 열기 : CLOSE → OPEN
door.DoorOpenButtonClick();
door.PrintCurrentState();
// 문 잠구기 : OPEN → LOCK
door.setLockState();
door.PrintCurrentState();
door.TryUnlockDoor(); // 문 따기 시도
door.PrintCurrentState();
// 문 다시 열기 : LOCK → OPEN
door.DoorOpenButtonClick();
door.PrintCurrentState();
// 문 닫기 : OPEN → CLOSE
door.DoorOpenButtonClick();
door.PrintCurrentState();
}
}

동일한 결과가 나온다.
상태를 변경할 때 마다 새로 객체를 생성하는 문제가 있다.
cxt.changeState(new OpenState());
물론 가비지 컬렉션에 의해 자동으로 지워지겠지만, 이런 가비지 값이 늘어나게 되면 결국 개체 제거 과정에서 Stop-the-world가 일어나게 되고 렉의 원인이 된다. 웬만한 상황에선 상태는 새로 인스턴스화 할 필요가 전혀 없다. 따라서 각 상태 클래스들을 싱글톤화 한다.
//OPEN State
class OpenState : DoorState {
private OpenState() {}
private static class SingleInstanceHolder {
public static readonly OpenState INSTANCE = new OpenState();
}
public static OpenState getInstance() {
return SingleInstanceHolder.INSTANCE;
}
public void DoorOpenButtonClick(DoorContext cxt) {
Debug.Log("문 CLOSE");
cxt.ChangeState(CloseState.getInstance());
}
public void TryUnlockDoor(DoorContext cxt) {
throw new System.Exception("문이 UNLOCK 된 상태입니다.");
}
public String currentState() {
return "문은 현재 OPEN 상태입니다.";
}
}
이 외에도 나머지 상태 클래스와 DoorContext에서 인스턴스 생성 부분을 모두 getInstance()로 바꿔준다. 실행해보면 아래처럼 동일한 결과가 나온다.

구조는 거의 비슷해도 어떤 목적을 위해 사용되는가에 따라 차이가 있다.
전략 패턴은 알고리즘을 객체화하여 클라이언트에게 유연적으로 전략을 제공 + 교체한다.
전략 패턴의 전략 객체는 그 전략만의 알고리즘 동작을 정의 및 수행한다.
전략 패턴의 전략 객체는 입력값에 따라 전략 형태가 다양하게 될 수 있으니 인스턴스로 구성한다.
상태 패턴은 각체 상태를 객체화하여 클라이언트와 상태 클래스 내부에서 다른 상태로 교체한다.
상태 패턴의 상태 객체는 상태가 적용되는 대상 객체가 할 수 있는 일련의 모든 행동들을 정의 및 수행한다.
상태 패턴의 상태 객체는 정의된 상태를 서로 스위칭하기에 메모리 절약을 위해 싱글톤으로 구성한다.