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.")
}
}
}
문제점
- 상태 전환 로직이 흩어짐
- play, pause, stop메서드 마다 상태 전환 로직이 중복됩니다.
- 상태가 추가되면 모든 메서드에 변경이 필요합니다.
- 코드의 복잡성 증가
- 상태가 많아질수록 if-else나 when문이 복잡해지고, 가독성이 떨어집니다.
- 확장성 부족
- 새로운 상태를 추가할 때 기존 코드를 수정해야 하며, 이는 OCP에 위배됩니다.
- 상태별 행동 분리가 어려움
- 상태별 동작이 메서드 내부에 섞여 있어, 상태별 동작을 독립적으로 관리하기 어렵습니다.
State 패턴의 등장
state 패턴은 위 문제를 해결하기 위해 객체 지향 원칙을 활용하여 다음과 같은 목표를 달성하려고 등장했습니다.
state 패턴의 설계 원칙
- 상태와 동작을 분리
- 상태와 상태별 행동을 별로의 클래스로 분리해 관리합니다.
- 행동의 위임
- 객체는 자신의 상태를 알고 있으며, 상태에 따른 동작은 상태 객체에 위임합니다.
- 확장성과 유지보수성 향상
- 새로운 상태를 추가해도 기존 코드를 수정할 필요가 없습니다.(OCP 준수)
state 패턴으로 문제 해결
- 상태를 클래스로 정의
- 상태와 상태별 행동을 클래스로 분리하여 독립적으로 관리합니다.
- 동적 상태 전환
- 상태 전환 로직을 클래스 내부에 캡슐화하여, 상태 변경이 간단해집니다.
- 컨텍스트 객체
- 현재 상태를 저장하며, 상태별 행동을 상태 객체에 위임합니다.
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 패턴의 핵심 개념
- 상태 객체
- 객체의 상태를 캡슐화한 객체로, 상태별 행동을 정의합니다.
- 컨텍스트(Context)
- 상태 객체를 포함하는 클래스입니다.
- 내부 상태 객체를 변경하여 동적으로 행동을 바꿉니다.
- 상태 전환
- 상태 패턴은 상태 변경을 명시적으로 표현하여 코드 가독성과 유지보수성을 높입니다.
state 패턴이 사용되는 시기
- 객체의 행동이 상태에 따라 달라지는 경우
- 상태 전환이 자주 일어나고, 각 상태마다 다른 동작을 해야 할 경우
- 상태별 행동을 정의하는 코드가 복잡해질 때, 이를 명확히 분리하고 싶을 때
state 패턴의 구성 요소
- state 인터페이스
- 모든 상태 클래스에서 구현해야 하는 공통 메서드를 정의합니다.
- Concrete State(구체적인 상태 클래스)
- 각 상태별로 동작을 구현한 클래스입니다.
- 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 패턴의 장점
- 코드의 응집력 증가
- 상태별 행동을 각각의 클래스로 분리하여, 코드가 명확하고 관리하기 쉬워집니다.
- 상태 전환 로직의 명시적 표현
- 상태 변경이 코드에 명확히 드러나므로 가독성이 높아지고 디버깅이 용이합니다.
- 유연한 확장
- 새로운 상태를 추가하거나 기존 상태를 수정할 때, 다른 상태에 영향을 주지 않고 수정 가능합니다.
- 중복 코드 감소
- 상태별 행동을 캡슐화하여 중복 코드를 줄일 수 있습니다.
state 패턴의 단점
- 클래스 수 증가
- 상태마다 별도의 클래스를 생성해야 하므로 클래스 수가 많아질 수 있습니다.
- 간단한 상태 관리에는 과도한 설계
- 상태가 적고 단순한 경우, 패턴을 적용하면 오히려 코드가 복잡해질 수 있습니다.
state 패턴에 대해 공부하면서 느낀점
과거의 저의 코드를 보며 상태를 if-else로 다 관리했던 나를 기억하며..
디자인 패턴을 공부하면서 많은 공부가 되고 '나의 코드 퀄리티는 한단계 증가한다.' 라는 기분이 듭니다.
디자인 패턴을 왜 좀더 빨리 공부하지 않았나라는 생각도 들고..
이제 이런것들을 잘 이해하고 적용한 프로젝트를 만들고 이제 취업만 하면.. 크흠..
'디자인패턴' 카테고리의 다른 글
Stractegy Pattern - 전략 패턴 (1) | 2024.12.28 |
---|---|
Factory Method Pattern - 팩토리 메서드 패턴 (0) | 2024.12.28 |
Abstract Factory Pattern(추상 팩토리) (2) | 2024.12.17 |
Builder Pattern(빌더 패턴) (0) | 2024.12.16 |
Adapter Pattern(어댑터 패턴) (2) | 2024.12.11 |