디자인패턴

State Pattern(상태 패턴)

빈코더 2024. 12. 19. 16:00
728x90

State Pattern

소프트웨어 디자인 패턴중 하나로 State Pattern(상태 패턴) 은 객체의 상태에 따라 행동이 달라지는 경우에 사용되는 디자인 패턴입니다.
이때 상태를 조건문으로 검사하여 행위를 달리 하는 것이 아닌 상태를 객체화 하여 상태가 행동할 수 있도록 위임합니다.
객체 내부에 상태 객체를 저장하고, 상태 변경에 따라 행동 로직을 동적으로 변경할 수 있습니다.

상태를 클래스로 표현하면 클래스를 교체해서 ‘상태의 변화’를 표현할 수 있고, 객체 내부 상태 변경에 따라 객체의 행동을 상태에 특화된 행동들로 분리해 낼 수 있으며, 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않습니다.

State Pattern 탄생 배경

state 패턴이 등장한 배경은 객체의 상태에 따라 동작이 달라지는 경우를 더 명확하고 유연하게 처리하기 위해서입니다.
아래서 state 패턴이 왜 필요했는지, 어떤 문제를 해결하기 위해 등장했는지 설명하겠습니다.

1. 전통적인 상태 관리의 문제

객체가 상태별로 다른 동작을 해야 하는 상황에서 일반적으로 사용하는 방법은 다음과 같습니다.

  class Player {
      private var state: String = "Stopped" // 상태를 문자열로 관리

      fun play() {
          when (state) {
              "Stopped" -> {
                  println("Playing...")
                  state = "Playing"
              }
              "Paused" -> {
                  println("Resuming...")
                  state = "Playing"
              }
              "Playing" -> {
                  println("Already playing.")
              }
          }
      }

      fun pause() {
          when (state) {
              "Playing" -> {
                  println("Pausing...")
                  state = "Paused"
              }
              else -> println("Can't pause. Current state: $state")
          }
      }

      fun stop() {
          when (state) {
              "Playing", "Paused" -> {
                  println("Stopping...")
                  state = "Stopped"
              }
              "Stopped" -> println("Already stopped.")
          }
      }
  }

문제점

  1. 상태 전환 로직이 흩어짐
    • play, pause, stop메서드 마다 상태 전환 로직이 중복됩니다.
    • 상태가 추가되면 모든 메서드에 변경이 필요합니다.
  2. 코드의 복잡성 증가
    • 상태가 많아질수록 if-else나 when문이 복잡해지고, 가독성이 떨어집니다.
  3. 확장성 부족
    • 새로운 상태를 추가할 때 기존 코드를 수정해야 하며, 이는 OCP에 위배됩니다.
  4. 상태별 행동 분리가 어려움
    • 상태별 동작이 메서드 내부에 섞여 있어, 상태별 동작을 독립적으로 관리하기 어렵습니다.

State 패턴의 등장

state 패턴은 위 문제를 해결하기 위해 객체 지향 원칙을 활용하여 다음과 같은 목표를 달성하려고 등장했습니다.

state 패턴의 설계 원칙

  1. 상태와 동작을 분리
    • 상태와 상태별 행동을 별로의 클래스로 분리해 관리합니다.
  2. 행동의 위임
    • 객체는 자신의 상태를 알고 있으며, 상태에 따른 동작은 상태 객체에 위임합니다.
  3. 확장성과 유지보수성 향상
    • 새로운 상태를 추가해도 기존 코드를 수정할 필요가 없습니다.(OCP 준수)

state 패턴으로 문제 해결

  1. 상태를 클래스로 정의
    • 상태와 상태별 행동을 클래스로 분리하여 독립적으로 관리합니다.
  2. 동적 상태 전환
    • 상태 전환 로직을 클래스 내부에 캡슐화하여, 상태 변경이 간단해집니다.
  3. 컨텍스트 객체
    • 현재 상태를 저장하며, 상태별 행동을 상태 객체에 위임합니다.
  interface State {
      fun play(context: PlayerContext)
      fun pause(context: PlayerContext)
      fun stop(context: PlayerContext)
  }

  class StoppedState : State {
      override fun play(context: PlayerContext) {
          println("Playing...")
          context.changeState(PlayingState())
      }

      override fun pause(context: PlayerContext) {
          println("Already stopped, can't pause.")
      }

      override fun stop(context: PlayerContext) {
          println("Already stopped.")
      }
  }

  class PlayingState : State {
      override fun play(context: PlayerContext) {
          println("Already playing.")
      }

      override fun pause(context: PlayerContext) {
          println("Pausing...")
          context.changeState(PausedState())
      }

      override fun stop(context: PlayerContext) {
          println("Stopping...")
          context.changeState(StoppedState())
      }
  }

  class PausedState : State {
      override fun play(context: PlayerContext) {
          println("Resuming...")
          context.changeState(PlayingState())
      }

      override fun pause(context: PlayerContext) {
          println("Already paused.")
      }

      override fun stop(context: PlayerContext) {
          println("Stopping...")
          context.changeState(StoppedState())
      }
  }

  class PlayerContext {
      private var state: State = StoppedState() // 초기 상태

      fun changeState(newState: State) {
          state = newState
      }

      fun play() = state.play(this)
      fun pause() = state.pause(this)
      fun stop() = state.stop(this)
  }

  fun main() {
      val player = PlayerContext()

      player.play()  // Playing...
      player.pause() // Pausing...
      player.play()  // Resuming...
      player.stop()  // Stopping...
  }

state 패턴의 핵심 개념

  1. 상태 객체
    • 객체의 상태를 캡슐화한 객체로, 상태별 행동을 정의합니다.
  2. 컨텍스트(Context)
    • 상태 객체를 포함하는 클래스입니다.
    • 내부 상태 객체를 변경하여 동적으로 행동을 바꿉니다.
  3. 상태 전환
    • 상태 패턴은 상태 변경을 명시적으로 표현하여 코드 가독성과 유지보수성을 높입니다.

state 패턴이 사용되는 시기

  • 객체의 행동이 상태에 따라 달라지는 경우
  • 상태 전환이 자주 일어나고, 각 상태마다 다른 동작을 해야 할 경우
  • 상태별 행동을 정의하는 코드가 복잡해질 때, 이를 명확히 분리하고 싶을 때

state 패턴의 구성 요소

  1. state 인터페이스
    • 모든 상태 클래스에서 구현해야 하는 공통 메서드를 정의합니다.
  2. Concrete State(구체적인 상태 클래스)
    • 각 상태별로 동작을 구현한 클래스입니다.
  3. Context(문맥 클래스)
    • 현재 상태를 저장하고 상태 전환을 관리하며, 현재 상태에 따라 동작을 위임합니다.

state 패턴의 코드 예제

위 예제를 풀어서 각 구성요소를 확인해봅시다.

문제 상황: 오디오 플레이어

오디오 플레이어가 Stopped, Playing, Paused 상태를 가지며, 각 상태에서 버튼을 누를 때 다른 동작을 해야 합니다.

1.state 인터페이스 정의

  interface State {
      fun play(context: PlayerContext)
      fun pause(context: PlayerContext)
      fun stop(context: PlayerContext)
  }

2. 구체적인 상태 클래스 정의

  class StoppedState : State {
      override fun play(context: PlayerContext) {
          println("Playing...")
          context.changeState(PlayingState())
      }

      override fun pause(context: PlayerContext) {
          println("Already stopped, can't pause.")
      }

      override fun stop(context: PlayerContext) {
          println("Already stopped.")
      }
  }

  class PlayingState : State {
      override fun play(context: PlayerContext) {
          println("Already playing.")
      }

      override fun pause(context: PlayerContext) {
          println("Pausing...")
          context.changeState(PausedState())
      }

      override fun stop(context: PlayerContext) {
          println("Stopping...")
          context.changeState(StoppedState())
      }
  }

  class PausedState : State {
      override fun play(context: PlayerContext) {
          println("Resuming play...")
          context.changeState(PlayingState())
      }

      override fun pause(context: PlayerContext) {
          println("Already paused.")
      }

      override fun stop(context: PlayerContext) {
          println("Stopping...")
          context.changeState(StoppedState())
      }
  }

3. Context 클래스 정의

  class PlayerContext {
      private var currentState: State = StoppedState() // 초기 상태는 Stopped

      fun changeState(state: State) {
          currentState = state
      }

      fun play() = currentState.play(this)
      fun pause() = currentState.pause(this)
      fun stop() = currentState.stop(this)
  }

4. 사용

  fun main() {
      val player = PlayerContext()

      player.play()  // Playing...
      player.pause() // Pausing...
      player.play()  // Resuming play...
      player.stop()  // Stopping...
      player.stop()  // Already stopped.
  }

state 패턴의 장점

  1. 코드의 응집력 증가
    • 상태별 행동을 각각의 클래스로 분리하여, 코드가 명확하고 관리하기 쉬워집니다.
  2. 상태 전환 로직의 명시적 표현
    • 상태 변경이 코드에 명확히 드러나므로 가독성이 높아지고 디버깅이 용이합니다.
  3. 유연한 확장
    • 새로운 상태를 추가하거나 기존 상태를 수정할 때, 다른 상태에 영향을 주지 않고 수정 가능합니다.
  4. 중복 코드 감소
    • 상태별 행동을 캡슐화하여 중복 코드를 줄일 수 있습니다.

state 패턴의 단점

  1. 클래스 수 증가
    • 상태마다 별도의 클래스를 생성해야 하므로 클래스 수가 많아질 수 있습니다.
  2. 간단한 상태 관리에는 과도한 설계
    • 상태가 적고 단순한 경우, 패턴을 적용하면 오히려 코드가 복잡해질 수 있습니다.

state 패턴에 대해 공부하면서 느낀점

과거의 저의 코드를 보며 상태를 if-else로 다 관리했던 나를 기억하며..
디자인 패턴을 공부하면서 많은 공부가 되고 '나의 코드 퀄리티는 한단계 증가한다.' 라는 기분이 듭니다.
디자인 패턴을 왜 좀더 빨리 공부하지 않았나라는 생각도 들고..
이제 이런것들을 잘 이해하고 적용한 프로젝트를 만들고 이제 취업만 하면.. 크흠..

728x90