본문 바로가기
디자인패턴

상태 패턴(State Pattern) 소개

by 대박플머 2024. 10. 17.

- 상태 패턴(State Pattern) 소개 
- 상태 패턴의 구체적인 구현(State Pattern)
- 상태 패턴의 응용 사례(State Pattern)
- 상태 패턴의 확장과 변형(State Pattern)
- 상태 패턴의 장단점과 최적의 사용 사례(State Pattern)

상태 패턴(State Pattern)은 객체가 자신의 상태에 따라 다른 동작을 수행하도록 해주는 디자인 패턴입니다. 상태 패턴을 사용하면 상태에 따른 복잡한 조건문이나 스위치 케이스문을 제거하고, 상태 객체에 따라 행동을 정의할 수 있습니다. 이번 글에서는 상태 패턴이 무엇인지, 그 필요성과 이점을 알아보고, 간단한 예제를 통해 상태 패턴의 구조를 이해해보겠습니다.


1. 상태 패턴이란?

상태 패턴은 객체 지향 설계에서 "행동"과 "상태"를 분리하여 코드의 복잡도를 줄이고 유지 보수성을 높이는 패턴입니다. 즉, 객체가 특정 상태를 가질 때 그 상태에 맞는 행동을 실행하게 됩니다. 이를 통해 상태에 따라 객체의 행동이 변하는 로직을 쉽게 관리할 수 있습니다.

상태 패턴의 정의:

상태 패턴은 객체의 상태가 변경됨에 따라 객체의 행동이 달라지도록 설계하는 패턴입니다. 상태에 따른 행위를 개별 상태 객체로 분리하고, 상태 전환을 통해 객체의 행동을 변경합니다.

쉽게 말해, 상태 패턴은 상태마다 다른 행동을 실행하는 객체를 설계하는 방법입니다. 예를 들어, 우리가 자주 접하는 교통 신호등 시스템을 생각해볼 수 있습니다. 신호등은 '빨간불', '노란불', '초록불' 상태를 가지고 있으며, 각각의 상태에 따라 다른 행동을 요구합니다. 이때 신호등 객체는 상태에 따라 동작을 결정하게 되며, 이는 상태 패턴으로 구현할 수 있습니다.


2. 상태 패턴이 필요한 이유

상태 패턴이 필요한 이유는 주로 복잡한 조건문을 제거하고, 각 상태별 동작을 객체지향적으로 처리할 수 있기 때문입니다. 조건문이나 스위치 케이스문을 사용한 상태 처리는 시간이 지남에 따라 코드가 복잡해지고, 새로운 상태가 추가되거나 로직이 변경될 때 유지보수가 어려워질 수 있습니다.

예를 들어, 다음과 같은 스위치 케이스문이 있는 코드를 살펴봅시다.

switch (currentState) {
  case "idle":
    // idle 상태의 동작
    break;
  case "running":
    // running 상태의 동작
    break;
  case "paused":
    // paused 상태의 동작
    break;
  default:
    throw new Error("Unknown state");
}

위 코드는 처음에는 간단해 보이지만, 상태가 늘어날수록 조건문이 길어지고 복잡해집니다. 새로운 상태가 추가될 때마다 코드 전체를 수정해야 하는 번거로움이 발생하며, 코드의 가독성도 떨어집니다. 이런 상황에서 상태 패턴을 사용하면 각 상태를 독립적인 객체로 만들고, 해당 상태에서 수행할 동작을 객체 내부에 캡슐화할 수 있습니다.


3. 상태 패턴의 구조

상태 패턴의 핵심은 "상태 객체"를 사용하여 상태를 관리하고, 각 상태에 따른 행동을 상태 객체에 위임하는 것입니다. 상태 패턴의 기본 구조는 다음과 같습니다.

  • Context(문맥): 상태 객체를 포함하고, 상태에 따른 동작을 위임받는 주체입니다. 문맥 객체는 현재 상태에 따라 동작을 처리하며, 상태 전환을 담당합니다.
  • State(상태 인터페이스): 각 상태가 구현해야 하는 행동을 정의하는 인터페이스입니다. 모든 상태는 이 인터페이스를 구현하여 상태별 행동을 정의합니다.
  • ConcreteState(구체적인 상태 클래스): 각 상태를 나타내는 구체적인 클래스입니다. 이 클래스는 상태 인터페이스를 구현하고, 상태에 맞는 행동을 정의합니다.
interface State {
  handle(context: Context): void;
}

class Context {
  private state: State;

  constructor(state: State) {
    this.state = state;
  }

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

  request(): void {
    this.state.handle(this);
  }
}

class ConcreteStateA implements State {
  handle(context: Context): void {
    console.log("ConcreteStateA의 동작을 수행합니다.");
    context.setState(new ConcreteStateB());
  }
}

class ConcreteStateB implements State {
  handle(context: Context): void {
    console.log("ConcreteStateB의 동작을 수행합니다.");
    context.setState(new ConcreteStateA());
  }
}

위 예제에서 Context 클래스는 현재 상태를 관리하며, request() 메서드를 호출할 때 현재 상태의 handle() 메서드를 실행합니다. 상태 전환은 각 상태 객체에서 이루어지며, 예제에서는 ConcreteStateA가 자신의 동작을 마친 후 ConcreteStateB로 상태를 전환합니다.


4. 상태 패턴의 간단한 예제

앞서 설명한 개념을 바탕으로, 간단한 교통 신호등 시스템을 예제로 살펴보겠습니다. 이 예제에서는 세 가지 상태(빨간불, 노란불, 초록불)를 가진 신호등을 상태 패턴으로 구현합니다.

// 상태 인터페이스
interface TrafficLightState {
  changeLight(light: TrafficLight): void;
}

// 구체적인 상태 클래스
class RedLight implements TrafficLightState {
  changeLight(light: TrafficLight): void {
    console.log("빨간불입니다. 멈추세요.");
    light.setState(new GreenLight());
  }
}

class GreenLight implements TrafficLightState {
  changeLight(light: TrafficLight): void {
    console.log("초록불입니다. 진행하세요.");
    light.setState(new YellowLight());
  }
}

class YellowLight implements TrafficLightState {
  changeLight(light: TrafficLight): void {
    console.log("노란불입니다. 주의하세요.");
    light.setState(new RedLight());
  }
}

// 문맥 클래스
class TrafficLight {
  private state: TrafficLightState;

  constructor() {
    this.state = new RedLight(); // 초기 상태는 빨간불
  }

  setState(state: TrafficLightState): void {
    this.state = state;
  }

  change(): void {
    this.state.changeLight(this);
  }
}

// 상태 전환 예제
const trafficLight = new TrafficLight();
trafficLight.change(); // 빨간불 -> 초록불
trafficLight.change(); // 초록불 -> 노란불
trafficLight.change(); // 노란불 -> 빨간불

이 예제에서 TrafficLight는 문맥(Context) 역할을 하며, 상태 객체(RedLight, GreenLight, YellowLight)는 각각의 상태에 따른 동작을 정의합니다. 신호등이 상태에 따라 행동을 다르게 하며, 상태가 변화할 때마다 다음 상태로 전환됩니다. 이를 통해 조건문 없이도 각 상태별로 행동을 정의할 수 있습니다.


5. 상태 패턴의 이점

상태 패턴은 다음과 같은 이점을 제공합니다.

  1. 복잡한 조건문 제거: 상태에 따라 행동을 다르게 하는 복잡한 조건문을 제거하고, 상태 객체를 통해 행동을 정의할 수 있습니다.
  2. 유지보수 용이: 새로운 상태가 추가되거나 상태 전환 로직이 변경되더라도 상태 객체만 수정하면 되므로, 유지보수가 간편해집니다.
  3. 캡슐화된 상태 관리: 상태 전환 로직을 각 상태 객체에 캡슐화함으로써, 상태 변화에 따른 부수 효과를 줄일 수 있습니다.
  4. 확장성: 상태 패턴은 새로운 상태를 쉽게 추가할 수 있으며, 상태별로 독립적인 동작을 정의할 수 있어 확장성이 뛰어납니다.

6. 요약 및 정리

상태 패턴은 객체의 상태에 따라 동작을 변경하는 디자인 패턴입니다. 이 패턴을 사용하면 복잡한 조건문을 제거하고, 상태 객체를 통해 동작을 정의할 수 있어 코드의 가독성과 유지보수성이 높아집니다. 이번 글에서는 상태 패턴의 기본 개념과 구조, 그리고 간단한 예제를 통해 상태 패턴의 유용성을 살펴보았습니다.