| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
- 디자인패턴
- designPattern
- PrototypePattern
- 옵저버 패턴
- android designsystem
- 디자인패턴 #
- 추상팩토리패턴
- Design Pattern
- ㅋㅁ
- 코틀린
- Abstract Factory
- builderPattern
- define
- 안드로이드 디자인시스템
- 코루틴
- 코틀린멀티플랫폼
- 추상 팩토리
- 팩토리 메소드
- material3
- factory method
- kotlin multiplatform
- Kotlin
- kmp
- Observer Pattern
- 프로토타입 패턴
- Functional Programming
- 빌터패턴
- compose
- Coroutines
- 함수형프로그래밍
- Today
- Total
오늘도 더 나은 코드를 작성하였습니까?
State Pattern(feat. kotlin) 본문
정의
State Pattern은 객체의 내부 상태에 따라 행동이 바뀌어야 할 때 사용하는 행동 디자인 패턴입니다. 객체가 마치 자신의 클래스를 바꾼 것처럼 보이게 만드는 게 핵심이다.
언제 사용하는지?
when이나 if-else로 상태를 분기하는 코드가 여러 메서드에 흩어져 있을 때가 가장 명확한 신호입니다. 예를 들어 결제 화면에서 PENDING, PROCESSING, SUCCESS, FAILED 상태마다 버튼 클릭, 화면 이탈, 재시도 동작이 다르다면 — 각 메서드마다 when (state) 분기가 반복됩니다. 상태가 하나 추가될 때마다 모든 메서드를 수정해야 하니 OCP(개방-폐쇄 원칙)를 위반하게 되고, 수정범위가 많아진다. "그냥 enum class XXXStatus로 충분하지 않나?" 싶을 수 있는데, 명확한 차이가 존재한다. enum + when 분기는 상태 값만 표현하지만, State 패턴은 상태 + 그 상태의 행동을 함께 캡슐화합니다. 각 상태마다 허용/금지되는 행동이 많고, 전이 규칙이 복잡할수록 State 패턴의 가치가 커집니다. 반대로 상태가 단순한 라벨 역할만 한다면 enum이 더 적절합니다.
트레이드오프
- 장점: 상태별 행동이 응집됨, 새 상태 추가가 OCP를 따름, 거대한 when 블록 제거
- 단점: 클래스가 늘어남, 상태 전이의 흐름을 따라가려면 여러 파일을 봐야 함 (특히 상태가 4~5개 이상일 때)
클래스 다이어그램

- Context: 현재 상태를 들고 있는 객체. 외부에서 보는 인터페이스.
- State (인터페이스): 상태별로 달라지는 행동을 정의 및 캡슐화.
- ConcreteState: 각 상태의 구체적인 행동 구현. 필요하면 다음 상태로의 전이도 담당.
// ============================================================
// State 인터페이스
// ============================================================
sealed interface PaymentState {
fun pay(context: PaymentContext)
fun approve(context: PaymentContext)
fun fail(context: PaymentContext, reason: String)
fun retry(context: PaymentContext)
}
package org.example
// ============================================================
// ConcreteStates
// ============================================================
// 결제 대기
object IdleState : PaymentState {
override fun pay(context: PaymentContext) {
println("[${context.orderId}] 결제 요청 시작")
context.transitionTo(ProcessingState)
}
override fun approve(context: PaymentContext) = reject("결제 시작 전입니다")
override fun fail(context: PaymentContext, reason: String) = reject("결제 시작 전입니다")
override fun retry(context: PaymentContext) = reject("재시도할 결제가 없습니다")
}
// 결제 처리 중 (PG사 응답 대기)
object ProcessingState : PaymentState {
override fun pay(context: PaymentContext) = reject("이미 처리 중입니다 (중복 요청 차단)")
override fun approve(context: PaymentContext) {
println("[${context.orderId}] PG 승인 완료")
context.transitionTo(SuccessState)
}
override fun fail(context: PaymentContext, reason: String) {
println("[${context.orderId}] 결제 실패: $reason")
context.transitionTo(FailedState)
}
override fun retry(context: PaymentContext) = reject("처리 중에는 재시도할 수 없습니다")
}
// 결제 실패
object FailedState : PaymentState {
override fun pay(context: PaymentContext) = reject("실패 상태에서는 retry()로 재시도하세요")
override fun approve(context: PaymentContext) = reject("실패한 결제는 승인할 수 없습니다")
override fun fail(context: PaymentContext, reason: String) = reject("이미 실패 상태입니다")
override fun retry(context: PaymentContext) {
println("[${context.orderId}] 재시도 시작")
context.transitionTo(ProcessingState)
}
}
// 결제 성공 (종료 상태)
object SuccessState : PaymentState {
override fun pay(context: PaymentContext) = reject("이미 완료된 결제입니다")
override fun approve(context: PaymentContext) = reject("이미 승인된 결제입니다")
override fun fail(context: PaymentContext, reason: String) = reject("이미 완료된 결제입니다")
override fun retry(context: PaymentContext) = reject("완료된 결제는 재시도할 수 없습니다")
}
private fun reject(msg: String) {
println(" ↳ 거부: $msg")
}
// ============================================================
// Context
// ============================================================
class PaymentContext(val orderId: String) {
var state: PaymentState = IdleState
private set
fun transitionTo(next: PaymentState) {
println("[$orderId] ${state::class.simpleName} → ${next::class.simpleName}")
state = next
}
// 외부에서 호출하는 행동 — 모두 현재 state에 위임
fun pay() = state.pay(this)
fun approve() = state.approve(this)
fun fail(reason: String) = state.fail(this, reason)
fun retry() = state.retry(this)
}
fun main() {
// 시나리오 1: 정상 흐름
val payment1 = PaymentContext("ORDER-001")
payment1.pay() // Idle → Processing
payment1.pay() // 거부: 이미 처리 중 (중복 차단)
payment1.approve() // Processing → Success
payment1.retry() // 거부: 완료된 결제
println()
// 시나리오 2: 실패 후 재시도
val payment2 = PaymentContext("ORDER-002")
payment2.pay() // Idle → Processing
payment2.fail("카드 한도 초과") // Processing → Failed
payment2.retry() // Failed → Processing
payment2.approve() // Processing → Success
}

중복 결제 방지가 됨: ProcessingState.pay()가 reject로 처리하기 때문에, UI에서 사용자가 결제 버튼을 두 번 눌러도 두 번째 요청은 자연스럽게 차단됩니다. Context 쪽에 if (isProcessing) return 같은 플래그 체크를 흩어둘 필요가 없어요.
전이 규칙이 한 곳에 모임: "어떤 상태에서 어떤 액션이 허용되는가"가 각 State 객체의 메서드 구현으로 표현됩니다. 새 상태(예: CancelledState, RefundedState)를 추가할 때 기존 코드를 거의 건드리지 않습니다.
sealed interface + object 조합: 상태가 내부 데이터를 갖지 않으므로 object(싱글톤)로 충분합니다. 만약 실패 사유나 트랜잭션 ID 같은 상태별 데이터가 필요하다면 object 대신 data class로 바꾸고, sealed interface가 컴파일러의 when exhaustive 체크까지 받쳐줍니다.
'디자인패턴' 카테고리의 다른 글
| 디자인 패턴(Design pattern)이란??? (0) | 2024.05.28 |
|---|---|
| Command Pattern (0) | 2022.08.17 |
| Abstract Factory Pattern (0) | 2022.08.01 |
| Decorator Pattern (데코레이터 패턴) (0) | 2022.07.25 |
| 느슨한 결합과 강한 결합 (0) | 2022.07.18 |
