정상에서 IT를 외치다

[디자인패턴] 스테이트 패턴 본문

디자인패턴

[디자인패턴] 스테이트 패턴

Black-Jin 2019. 6. 20. 18:00
반응형

안녕하세요. 블랙진입니다.

책 HeadFirstDesignPattern 을 보며 코드를 Kotlin 으로 바꿔가며 공부한 내용입니다.


스테이트 패턴


객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있습니다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있습니다.



문제


4개의 상태와 행동을 가지는 뽑기 기계가 있습니다.


상태

enum class STATE {
SOLD_OUT, //알맹이 매진
NO_QUARTER, // 동전 없음
HAS_QUARTER, // 동전 있음
SOLD // 알맹이 판매
}


행동

//동전이 투입된 경우
fun insertQuater() {
...
}

//사용자가 동전을 반환 받으려고 하는 경우
fun ejectQuarter() {
...
}

//손잡이를 돌리는 경우
fun turnCrank() {
...
}

//알맹이 꺼내기
fun dispense() {
...
}


이 뽑기 기계를 구현하는 GumbalMachine을 코드로 작성해보겠습니다.



괜찮게 구현한거 같은가요? 4개의 행동에 4개의 상태를 when 문을 사용해 구분해 주었습니다. 하지만 이는 4가지 상태와 행동을 한 클래스에서 구현했으므로 SRC(단일책임원칙)을 위배 했으며 추상화와 캡슐화가 안되 있으므로 OCP(개방 폐쇄의 원칙)을 위해했습니다. 이에 관리 및 수정이 용이하게 코드를 리펙토리 해보겠습니다.




리펙토링


"바뀌는 부분을 캡슐화한다" -> 각 상태의 행동을 별도의 클래스에 집어넣고 모든 상태에서 각각 자기가 할 일을 구현하도록 수정하자


"구성을 활용하라" -> 뽑기 기계에 현재 상태를 나타내는 상태 객체한테 작업을 넘기도록 구현하자


위 2가지 사항을 사용해 상태 객체들을 별도의 코드에 집어넣고 어떤 행동이 일어나면 현재 상태 객체에서 필요한 작업을 처리하게 수정해 보겠습니다.


1. 우선 뽑기 기계와 관련된 모든 행동에 대한 메소드가 들어있는 State 인터페이스를 정의합니다.

2. 그 다음에는 기계의 모든 상테에 대해서 상태 크래스를 구현해야 합니다.

3. 마지막으로 조건문 코드를 전부 없애고 상태 클래스에 모든 작업을 위임합니다.



1. 상태에 따른 행동들을 모두 정의해 줍니다.


interface State {

//동전이 투입된 경우
fun insertQuarter()

//사용자가 동전을 반환 받으려고 하는 경우
fun ejectQuarter()

//손잡이를 돌리는 경우
fun turnCrank()

//알맹이 꺼내기
fun dispense()
}


2. 상태 클래스 구현


동전이 없는 상태

class NoQuarterState(private val gumballMachine: GumballMachine) : State {

override fun insertQuarter() {
println("동전을 넣으셨습니다.")
gumballMachine.state = gumballMachine.hasQuarterState
}

override fun ejectQuarter() {
println("동전을 넣어주세요.")
}

override fun turnCrank() {
println("동전을 넣어주세요.")
}

override fun dispense() {
println("동전을 넣어주세요.")
}
}


동전이 있는 상태

class HasQuarterState(private val gumballMachine: GumballMachine) : State {

override fun insertQuarter() {
println("동전은 한 개만 넣어주세요.")
}

override fun ejectQuarter() {
println("동전이 반환됩니다.")
gumballMachine.state = gumballMachine.noQuarterState
}

override fun turnCrank() {
println("손잡이를 돌리셨습니다.")
gumballMachine.state = gumballMachine.soldState
}

override fun dispense() {
println("알맹이가 나갈 수 없습니다.")
}
}


알맹이 판매 상태

class SoldState(private val gumballMachine: GumballMachine) : State {

override fun insertQuarter() {
println("잠깐만 기다려 주세요. 알맹이가 나가고 있습니다.")
}

override fun ejectQuarter() {
println("이미 알맹이를 뽑으셨숩니다.")
}

override fun turnCrank() {
println("손잡이는 한번만 돌려주세요.")
}

override fun dispense() {
gumballMachine.releaseBall()
if(gumballMachine.count > 0) {
gumballMachine.state = gumballMachine.noQuarterState
} else {
println("Oops, out of gumballs!")
gumballMachine.state = gumballMachine.soldOutState
}
}
}


알맹이 매진 상태

class SoldOutState(private val gumballMachine: GumballMachine) : State {

override fun insertQuarter() {
println("매진")
}

override fun ejectQuarter() {
println("매진")
}

override fun turnCrank() {
println("매진")
}

override fun dispense() {
println("알맹이가 나갈 수 없습니다.")
}
}


이제 4가지 상태를 각각 클래스로 정의 했고 각 클래스 별로 행동을 구현했습니다. 이제 GumbalMachine는 어떻게 달라졌을까요?


class GumballMachine(
var count: Int = 0
) {

val soldOutState: State
val noQuarterState: State
val hasQuarterState: State
var soldState: State


var state: State

init {
soldOutState = SoldOutState(this)
noQuarterState = NoQuarterState(this)
hasQuarterState = HasQuarterState(this)
soldState = SoldState(this)


state = soldOutState
if(count > 0) state = noQuarterState
}

fun releaseBall() {
println("A gumball comes rolling out the slot...")
if(count != 0) count--
}
}


GumbalMachine의 상태는 State로 추상화가 되었습니다.  추상화된 State를 구현한 상태 객체들을 통해 행동을 보여줄 수 있는데요. 이는 각 상태를 변경에 대해서는 닫혀 있고 GumbalMachine 자체는 새로운 상태 클래스를 추가하는 확장에 대해 열려있습니다(OCP). 또한 상태에 따라 각각 객체를 구현 했기 때문에 클래스별로 책임을 맡게 되었습니다(SRP). 이제 좀더 유지보수하기 좋은 코드가 되었죠?




스테이트 패턴 vs 스트래티지 패턴(1)


이 둘의 다이어그램은 동일합니다. 하지만 용동에 있어서 차이가 있습니다.


- 스테이트 패턴을 사용할 때는 상태 객체의 일련의 행동이 캡슐화됩니다.

- 스테이트 패턴은 컨텍스트 객체에 수많은 조건문을 집어넣는 대신에 사용할 수 있는 패턴입니다.

- 행동을 상태 객체 내애 캡슐화시키면서 컨텍스트 내의 상태 객체를 바꾸는 것만으로도 컨텍스트 객체의 행동을 바꿀 수 있습니다.

- 클래이언트는 상태 객체에 대해서 거의 아무것도 모릅니다.


- 스트래티지 패턴을 사용할 때는 일반적으로 클라이언트에서 컨텍스트 객체한테 어떤 전략 객체를 사용할지를 지정해 줍니다.

- 스트래티지 패턴은 주로 실행시에 전략 객체를 변경할 수 있는 유연성은 제공하기 위해 사용됩니다.

- 스트래티지 패턴은 서브클래스를 만드는 방법은 대신하여 유연성을 극대화하기 위한 용도로 쓰입니다.

- 구성을 통해 행동을 정의하는 객체를 유연하게 바꿀 수 있습니다.




문제


열 번에 한 번 꼴로 알맹이를 하나 더 주는 게임을 만들어 주세요.




풀이


1. GumbalMachine에 winnerState를 추가해 줍니다.

class GumballMachine(
var count: Int = 0
) : State {

val soldOutState: State
val noQuarterState: State
val hasQuarterState: State
val soldState: State

//TODO 추가1
var winnerState: State

var state: State

init {
soldOutState = SoldOutState(this)
noQuarterState = NoQuarterState(this)
hasQuarterState = HasQuarterState(this)
soldState = SoldState(this)

//TODO 추가2
winnerState = WinnerState(this)

state = soldOutState

if (count > 0) state = noQuarterState
}
}


2. WinnerState를 구현해 줍니다.

//TODO 추가3
class WinnerState(private val gumballMachine: GumballMachine) : State {

override fun insertQuarter() {
println("동전을 넣어 주세요.")
}

override fun ejectQuarter() {
println("동전을 넣어 주세요.")
}

override fun turnCrank() {
println("동전을 넣어 주세요.")
}

override fun dispense() {
println("축하드립니다! 알맹이를 하나 더 받으실 수 있습니다.")
gumballMachine.releaseBall()
if (gumballMachine.count == 0) {
gumballMachine.state = gumballMachine.soldOutState
} else {
gumballMachine.releaseBall()
if (gumballMachine.count > 0) {
gumballMachine.state = gumballMachine.noQuarterState
} else {
println("더 이상 알맹이가 없습니다.")
gumballMachine.state = gumballMachine.soldOutState
}
}
}
}


3. 당첨 기능을 행동에 추가해 보겠습니다.

class HasQuarterState(private val gumballMachine: GumballMachine) : State {

override fun insertQuarter() {
println("동전은 한 개만 넣어주세요.")
}

override fun ejectQuarter() {
println("동전이 반환됩니다.")
gumballMachine.state = gumballMachine.noQuarterState
}

override fun turnCrank() {
println("손잡이를 돌리셨습니다.")
//TODO 추가4
val winner = Random.nextInt(10)

if (winner == 0 && gumballMachine.count > 1) {
gumballMachine.state = gumballMachine.winnerState
} else {
gumballMachine.state = gumballMachine.soldState
}
}

override fun dispense() {
println("알맹이가 나갈 수 없습니다.")
}
}


이렇게 스테이트 패턴을 활용해 10번에 1번꼴로 2개의 알맹이가 나오는 문제까지 구현해 보았습니다. 마지막으로 스테이트 패턴과 스트래티지 패턴에 대해 비교해 보겠습니다.




스테이트 패턴 vs 스트래티지 패턴(2)


스테이트 패턴은 몇 가지 상태를 가지고 작업합니다. Context 객체에서는 미리 정해진 상태 전환 규칙을 바탕으로 알아서 자기 상태를 변경합니다. 원래 상황에 따라서 상태를 바꾸는걸 염두에 둔 디자인이죠. 스트래티지 패턴은 객체가 상태를 변경하는 것을 장려하는 쪽이 아닙니다. 객체에서 어떤 전략을 사용하는지는 클라이언트에서 직접 결정합니다.



반응형
Comments