본문 바로가기

IT/헤드퍼스트 디자인패턴

[디자인패턴] 스테이트 패턴 (State)

안녕하세요 남갯입니다


오늘은 스테이트 패턴에 대해 포스팅 해보려고 합니다.




사건의 발단


뽑기회사에서 뽑기 프로그램을 만들게 되었습니다.


총 상태는 아래와같이 4가지가 있고,

동전없을때 동전을 넣으면 동전있음으로 바뀌고, 이것이 상태의 전환입니다.


즉 상태는 4개이고 

1.동전 있음

2.동전 없음.

3.알맹이 판매.

4.알맹이 매진.


행동도 4개가 되는거죠

1. 동전 투입

2. 동전 반환

3. 손잡이 돌림

4. 알맹이 내보냄



이제 상태 기계 역할을 하는 클래스를 만들어보겠습니다.

fun insertQuarter(){
if(state == HAS_QUARTER){
println("동전은 한개만 넣어주세요")
}else if(state == SOLD_OUT){
println("매진되었습니다. 다음 기회에 이용해주세요.")
}else if(state == SOLD){
println("잠깐만 기다려 주세요. 알맹이가 배출되고 있습니다.")
}else if(state == NO_QUARTER) {
state = HAS_QUARTER
println("동전이 투입 되었습니다.")
}
}


위의 상태에서 동전을 넣는 부분은 이렇게 만들 수 있겠죠


이제 뽑기기계를 구현해 봅니다.


class GumballMachine(val count : Int){
val SOLD_OUT = 0
val NO_QUARTER = 1
val HAS_QUARTER = 2
val SOLD = 3

var state = SOLD_OUT // now state

init {
if(count > 0){
state = NO_QUARTER
}
}

fun insertQuarter(){
if(state == HAS_QUARTER){
println("동전은 한개만 넣어주세요")
}else if(state == SOLD_OUT){
println("매진되었습니다. 다음 기회에 이용해주세요.")
}else if(state == SOLD){
println("잠깐만 기다려 주세요. 알맹이가 배출되고 있습니다.")
}else if(state == NO_QUARTER) {
state = HAS_QUARTER
println("동전이 투입 되었습니다.")
}
}
fun ejectQuarter(){
..... }
//손잡이 돌리기
fun turnCrank(){
..... }
//알맹이 꺼내기
fun dispense(){
..... }
}

이렇게 4가지 기능과 각각의 상태에 맞게 구현을 하면 되겠죠?

코드가 너무 길어서 생략하겠습니다.



테스트결과 잘 되는것을 확인하였는데요?


모두가 예상했던대로 추가적인 요청이 들어왔습니다...


뽑기게임에 게임기능을 더하면 매출액이 늘거라는 생각에 10분의 1확률로 공짜 알맹이를 받을 수 있도록 하는 이벤트입니다.


기존코드에서 바꾸게 된다면 어떻게 해야할까요?

WINNER상태(당첨) 상태를 추가하고 4가지의 function에 winner상태를 if문으로 상태를 추가해야하는 단점이 생겨버립니다.


우리가 만든코드는 

1.우리의 코드는 OCP를 지키고 있지 않습니다. (확장에는 닫혀있고 수정에는 열려있는 코드라서 OCP를 지키지 않음)

2. 바뀌는 부분을 캡슐화 하지 않았습니다.

3. 객체지향 디자인이라 하기 어렵습니다.

4. 추가하면서 버그가 생길 확률이 높습니다.

5. 상태 전환이 복잡한 조건문 속에 숨어있어서 분명하게 드러나지 않습니다.


이로하여금 우리는 STATE 패턴을 이용해보기로 합니다.


1. state 인터페이스를 정의하고 모든 행동을 넣습니다.

2. 기계의 모든 상태에 대해 상태 클래스를 구현합니다.

3. 조건문 코드를 없애고 상태클래스에 모든 작업을 위임합니다.



interface State{
fun insertQuarter()
fun ejectQuarter()
fun turnCrank()
fun dipense()
}


아까 동작을 구현한 state 인터페이스를 만든뒤에


아까 val로 선언해둔 상태에 State를 구성하여 클래스화 합니다.


이렇게 말이죠


class SoldState(val gumballMachine: GumballMachine) : State{
override fun insertQuarter() {}
override fun ejectQuarter() {}
override fun turnCrank() {}
override fun dipense() {}
}

class SoldOutState(val gumballMachine: GumballMachine) : State{
override fun insertQuarter() {}
override fun ejectQuarter() {}
override fun turnCrank() {}
override fun dipense() {}
}
class NoQuarterState(val gumballMachine: GumballMachine) : State{
override fun insertQuarter() {}
override fun ejectQuarter() {}
override fun turnCrank() {}
override fun dipense() {}
}
class HasQuarterState(val gumballMachine: GumballMachine) : State{
override fun insertQuarter() {}
override fun ejectQuarter() {}
override fun turnCrank() {}
override fun dipense() {}
}

위에중에 두개만 구현해 보도록 하겠습니다.



class NoQuarterState(val gumballMachine: GumballMachine) : State {
override fun insertQuarter() {
println("동전을 넣으셨습니다.")
gumballMachine.state = gumballMachine.hasQuarterState
}
override fun ejectQuarter() = println("동전을 넣어주세요")
override fun turnCrank() = println("동전을 넣어주세요")
override fun dipense() =println("동전을 넣어주세요")
}

class HasQuarterState(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 dipense() = println("알맹이가 나갈 수 없습니다.")
}

이렇게 구현을 했습니다.


또한 이제 바뀐 코드를 갖고 겜볼머신을 바꿔보도록 하겠습니다.


class GumballMachine(val count: Int) {
val soldOutState: State
val noQuarterState: State
val hasQuarterState: State
val soldState: State
lateinit var state :State
init {
soldOutState = SoldOutState(this)
noQuarterState = NoQuarterState(this)
hasQuarterState = HasQuarterState(this)
soldState = SoldState(this)
state = soldOutState
if (count > 0) {
state = noQuarterState
}
}
fun insertQuarter() {
state.insertQuarter()
}
fun ejectQuarter() {
state.ejectQuarter()
}
//손잡이 돌리기
fun turnCrank() {
state.turnCrank()
}
//알맹이 꺼내기
fun dispense() {
state.dipense()
}
}


실제 int에 구현하던것을 각자의 상태를 구현한뒤 state에게 구현하도록 되어있습니다.



위와같이 만들면서 

1. 각 상태의 행동을 별개의 클래스로 나눴습니다.

2. if문을 없앴습니다

3. OCP를 구현하였습니다

4. 훨씬 이해하기 좋은 코드로 변경하였습니다



스테이트패턴의 정의


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

* 상태를 별도의 클래스로 캡슐화하고 현재상태를 나타내는 객체한테 행동을 위임하기 때문에 내부상태가 바뀜에 따라 행동이 달라지는것을 알 수 있습니다. 




마지막으로 


공짜 알맹이 당첨기능을 추가해 보겠습니다.


class WinnerState(val gumballMachine: GumballMachine) : State {
override fun insertQuarter() = println("오류")
override fun ejectQuarter() = println("오류")
override fun turnCrank() = println("오류")
override fun dipense() = println("축하합니다 알맹이를 하나 더 받으실 수 있습니다.")
}

class GumballMachine(val count: Int) {
val soldOutState: State
val noQuarterState: State
val hasQuarterState: State
val soldState: State
val WinnerState: State
init {
soldOutState = SoldOutState(this)
noQuarterState = NoQuarterState(this)
hasQuarterState = HasQuarterState(this)
soldState = SoldState(this)
WinnerState = WinnerState(this)

이렇게 한뒤 추첨을 하는 부분에서 랜덤함수를 통해 상태를 변경해주면 됩니다.


class HasQuarterState(val gumballMachine: GumballMachine) : State {
val random = Random(System.currentTimeMillis())

override fun insertQuarter() = println("동전은 한개만 넣어주세요")
override fun ejectQuarter() {
println("동전이 반환됩니다.")
gumballMachine.state = gumballMachine.noQuarterState
}

override fun turnCrank() {
println("손잡이를 돌리셨습니다..")
val result = random.nextInt(10)
if(result == 0){
gumballMachine.state = gumballMachine.winnerState
}else {
gumballMachine.state = gumballMachine.soldState
}
}

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