스테이트 패턴의 정의는 다음과 같습니다.
스테이트 패턴을 이용하면 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있습니다. 마치 객체의 클래스가 바뀌는 것과 같은 결 과를 얻을 수 있습니다.
위에 있는 정의의 첫 번째 부분은 금방 이해가 되죠? 이 패턴에서는 상태를 별도의 클래스로 캡슐화한 다음 현재 상태 를 나타내는 객체에게 행동을 위임하기 때문에, 내부 상태가 바뀜에 따라서 행동이 달라지게 된다는 것을 알 수 있습니 다.
두번째 부분은 어떻게 해석할 수 있을까요? “클래스가 바뀌는 것 같은”결과를 얻는다는 것이 무엇을 뜻할까요? 클라 이언트 입장에서 생각해 보죠. 만약 클라이언트에서 사용하는 객체의 행동이 완전히 달라질 수 있다면 마치 그 객체가 다른 클래스로부터 만들어진 객체처럼 느껴지겠죠?
자, 그럼 스테이트 패턴 클래스 다이어그램을 살펴볼까요?
클래스 다이어그램을 살펴보면 어디서 본 것 같지 않나요? 맞습니다. 스트래티지 패턴과 모양이 비슷합니다. 하지만 이 두 패턴은 용도에 있어서 차이가 있습니다.
스테이트 패턴을 사용할 때는 상태 객체에 일련의 행동이 캡슐화됩니다. 상황에 따라 Context 객체에서 여러 상태 객체 중 한 객체에게 모든 행동을 맡기게 되죠. 그 객체의 내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌게 되고, 그 결과로 컨텍스트 객체의 행동도 자연스럽게 바뀌게 됩니다. 클라이언트 는 상태 객체에 대해서 거의 아무것도 몰라도 됩니다.
하지만 스트래티지 패턴을 사용할 때는 일반적으로 클라이언트에서 컨텍스트 객체한테 어떤 전략 객체를 사용할지를 지정해 줍니다. 스트래티지 패턴은 주로 실행시에 전략 객체를 변경할 수 있는 유연성을 제공 하기 위한 용도로 쓰이죠. 보통 가장 적합한 전략 객체를 선택해서 사용할게 됩니다.
일반적으로 스트래티지 패턴은 서브클래스를 만드는 방법을 대신하여 유연성을 극대화하기 위한 용도로 쓰입니다. 상속을 이용해서 클래스의 행동을 정의하다 보면 행동을 변경해야 할 때 마음대로 변경하기가 힘들죠. 하지만 스트래티지 패턴을 사용하면 구성을 통해 행동을 정의하는 객체를 유연하게 바꿀 수 있습니다.
스테이트 패턴은 컨텍스트 객체에 수많은 조건문을 집어넣는 대신에 사용할 수 있는 패턴이라고 생각하면 됩니다. 행동을 상태 객체 내에 캡슐화시키면 컨텍스트 내의 상태 객체를 바꾸는 것만으로도 컨텍스트 객체의 행동을 바꿀 수 있으니까요.
스테이트 패턴을 적용한 예로 뽑기 기계(Gumball Machine)를 살펴보겠습니다. 먼저 클래스 다이어그램을 살펴볼까요?
스테이트 패턴 클래스 다이어그램에서 Context에 해당하는 것이 GumballMachine 클래스입니다.
public class GumballMachine { State soldOutState; State noQuarterState; State hasQuarterState; State soldState; State winnerState; State state = soldOutState; int count = 0; public GumballMachine(int numberGumballs) { soldOutState = new SoldOutState(this); noQuarterState = new NoQuarterState(this); hasQuarterState = new HasQuarterState(this); soldState = new SoldState(this); winnerState = new WinnerState(this); this.count = numberGumballs; if (numberGumballs > 0) { state = noQuarterState; } } public void insertQuarter(){ state.insertQuarter(); } public void ejectQuarter() { state.ejectQuarter(); } public void turnCrank() { state.turnCrank(); state.dispense(); } void setState(State state) { this.state = state; } void releaseBall() { System.out.println("A gumball comes rolling out the slot..."); if (count != 0) { count = count -1; } } public int getCount() { return this.count; } public State getSoldOutState() { return soldOutState; } public State getNoQuarterState() { return noQuarterState; } public State getHasQuarterState() { return hasQuarterState; } public State getSoldState() { return soldState; } public State getWinnerState() { return winnerState; } }
상태를 구현해 볼 때가 되었습니다. 우선 NoQuarterState부터 시작해 봅시다.
public class NoQuarterState implements State { GumballMachine gumballMachine; public NoQuraterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuarter() { System.out.println("동전을 넣으셨습니다."); gumballMachine.setState(gumballMachine.getHasQuarterState()); } public void ejectQuarter() { System.out.println("동전을 넣어주세요."); } public void turnCrank() { System.out.println("동전을 넣어주세요."); } public void dispense() { System.out.println("동전을 넣어주세요."); } }
이제 GumballMachine 클래스하고 상태 클래스가 어떤 식으로 맞물려 돌아가는지 어느 정도 감히 잡힐 것 같군요. 그럼 HasQuarterState와 SoldSate 클래스도 구현해 볼까요?
public class HasQuarterState implements State { Random randomWinner = new Random(System.currentTimeMillis()); GumballMachine gumballMachine; public HasQuarterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuarter() { System.out.println("동전은 한 개만 넣어주세요."); } public void ejectQuarter() { System.out.println("동전이 반환됩니다."); gumballMachine.setState(gumballMachie.getNoQuarterState()); } public void turnCrank() { System.out.println("손잡이를 돌리셨습니다."); int winner = randomWinner.nextInt(10); if((winner==0) && (gumballMachine.getCount() > 1)) { gumballMachine.setState(gumballMachine.getWinnerState()); } else { gumballMachine.setState(gumballMachine.getSoldState()); } } public void dispense() { System.out.println("알맹이가 나갈 수 없습니다."); } }
SolidState 클래스도 살펴봅시다.
public class SolidState implements State { GumballMachine gumballMachine; public SolidState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuarter() { System.out.println("잠시만 기다려 주세요. 알맹이가 나가고 있습니다."); } public void ejectQuarter() { System.out.println("이미 알맹이를 뽑으셨습니다."); } public void turnCrank() { System.out.println("손잡이는 한 번만 돌려주세요."); } public void dispense() { gumballMachine.releaseBall(); if (gumballMachine.getCount() > 0) { gumballMachine.setState(gumballMachine.getNoQuarterState()); } else { System.out.println("Oopse, out of gumballs!."); gumballMachine.setState(gumballMachine.getSoldState()); } } }
public class SoldOutState implements State { GumballMachine gumballMachine; public SoldOutState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } public void insertQuarter() { System.out.println("죄송합니다. 매진되었습니다."); } public void ejectQuarter() { System.out.println("죄송합니다. 매진되었습니다."); } public void turnCrank() { System.out.println("죄송합니다. 매진되었습니다."); } public void dispense() { System.out.println("죄송합니다. 매진되었습니다."); } }
public class WinnerState implements State { GumballState gumballState; public WinnerState(GumballState gumballState) { this.gumballState = gumballState; } public void insertQuarter() { System.out.println("이미 알맹이를 뽑으셨습니다."); } public void ejectQuarter() { System.out.println("이미 알맹이를 뽑으셨습니다."); } public void turnCrank() { System.out.println("이미 알맹이를 뽑으셨습니다."); } public void dispense() { System.out.println("축하드립니다! 알맹이를 하나 더 받으실 수 있습니다."); gumballMachine.releaseBall(); if (gumballMachine.getCount() == 0) { gumballMachine.setState(gumballMachine.getSoldOutState()); } else { System.out.println("더 이상 알맹이가 없습니다."); gumballMachine.setState(gumballMachine.getSoldOutState()); } } }