10장. 헤드퍼스트 디자인 패턴 - 상태 패턴
안녕하세요 😀
유로띠 입니다 😉
헤드퍼스트 디자인패턴
TIL (Today I Learned)
3줄 요약
✏️ 상태와 행동을 분리!!
✏️ 다음 현재 상태를 나타내는 객체에게 행동을 위임하므로 내부 상태가 바뀔 때 행동이 달라지게 된다는 사실을 쉽게 알 수 있다.
✏️ 전략 패턴과 쌍둥이 이지만 상태 패턴은 초기 상태 이후 Context 객체가 알아서 자기 상태를 변경한다. (전략 패턴은 1장이고 상태 패턴은 10장이다?! (전략 패턴 왈))
DAY 4
오늘 읽은 범위: 10장. 객체의 상태 바꾸기 - 상태 패턴
😉 책에서 기억하고 싶은 내용을 써보세요.
개요.
전략 패턴과 상태 패턴은 쌍둥이 입니다.
전략 패턴은 바꿔 쓸 수 있는 알고리즘을 내세워 큰 성공을 거둔 반면에, 상태 패턴은 내부 상태를 바꿈으로써 객체가 행동을 바꿀 수 있도록 도와주는 고상한 길을 택했습니다.
최첨단 뽑기 기계
뽑기 기계의 상태 다이어그램
상태 - 동전 없음 / 동전 있음 / 알맹이 매진 / 알맹이 판매
행동 - 동전 투입 / 동전 반환 / 손잡이 돌림 / 알맹이 내보냄
행동들은 뽑기 기계의 인터페이스라고 할 수 있습니다.
행동을 실행 할 때 상태가 바뀐다는 것을 알 수 있습니다.
상태 기계 역할을 하는 클래스를 만들어야 합니다. 각 행동을 구현할 때는 조건문 을 써서 상태별로 어떤 작업을 처리해야 할지 결정합니다.
뽑기 기계 코드 만들기
상태값으로 모든 행동 및 상태 전환을 처리하겠습니다.
🟢 4가지 상태와 초기 값
생성자에서는 초기 알맹이 개수를 인자로 받아들입니다.
final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
int state = SOLD_OUT;
int count = 0;
public GumballMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_QUARTER;
}
}
🟢 insertQuarter - 동전 투입
동전을 받으면 HAS_QUERTER 상태로 변경합니다.
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("You can't insert another quarter");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("You inserted a quarter");
} else if (state == SOLD_OUT) {
System.out.println("You can't insert a quarter, the machine is sold out");
} else if (state == SOLD) {
System.out.println("Please wait, we're already giving you a gumball");
}
}
🟢 ejectQuarter - 동전 반환
동전을 반환하면 NO_QUARTER 상태로 변경합니다.
public void ejectQuarter() {
if (state == HAS_QUARTER) {
System.out.println("Quarter returned");
state = NO_QUARTER;
} else if (state == NO_QUARTER) {
System.out.println("You haven't inserted a quarter");
} else if (state == SOLD) {
System.out.println("Sorry, you already turned the crank");
} else if (state == SOLD_OUT) {
System.out.println("You can't eject, you haven't inserted a quarter yet");
}
}
🟢 turnCrank - 손잡이 돌림
상태가 HAS_QUARTER인 경우 사용자가 알맹이를 받을 수 있고 상태를 SOLD로 변경 후 dispense()를 호출합니다.
public void turnCrank() {
if (state == SOLD) {
System.out.println("Turning twice doesn't get you another gumball!");
} else if (state == NO_QUARTER) {
System.out.println("You turned but there's no quarter");
} else if (state == SOLD_OUT) {
System.out.println("You turned, but there are no gumballs");
} else if (state == HAS_QUARTER) {
System.out.println("You turned...");
state = SOLD;
dispense();
}
}
🟢 dispense - 알맹이 내보내기
남아있는 알맹이가 없으면 SOLD_OUT 상태가 되고 그렇지 않다면 NO_QUARTER 상태로 변경합니다.
public void dispense() {
if (state == SOLD) {
System.out.println("A gumball comes rolling out the slot");
count = count - 1;
if (count == 0) {
System.out.println("Oops, out of gumballs!");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
} else if (state == NO_QUARTER) {
System.out.println("You need to pay first");
} else if (state == SOLD_OUT) {
System.out.println("No gumball dispensed");
} else if (state == HAS_QUARTER) {
System.out.println("No gumball dispensed");
}
}
뽑기 기계 코드 테스트 😄
public class GumballMachineTestDrive {
public static void main(String[] args) {
GumballMachine gumballMachine = new GumballMachine(5);
System.out.println(gumballMachine);
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
System.out.println(gumballMachine);
gumballMachine.insertQuarter();
gumballMachine.ejectQuarter();
gumballMachine.turnCrank();
}
}
/* 결과
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 5 gumballs
Machine is waiting for quarter
You inserted a quarter
You turned...
A gumball comes rolling out the slot
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
Machine is waiting for quarter
You inserted a quarter
Quarter returned
You turned but there's no quarter
*/
뽑기 기계 코드 수정 요청
수정사항 - 1/10 확률로 보너스 알맹이를 추가로 받을 수 있도록 합니다. 😇
수정된 상태 다이어그램
요청 사항 살펴보기
✅ WINNER 상태(당첨됐다는 사실을 나타내는 상태)를 추가해야 합니다.
✅ insertQuarter, ejectQuarter, turnCrank, dispense 메소드에 WINNER 상태를 확인하는 조건문을 추가해야 합니다.
✅ turnCrank() 메서드 에서는 당첨되었는지 확인하는 코드를 추가한 다음 상태를 WINNER, SOLD 상태로 변경해야 합니다.
위처점 진행 할 경우 다음과 같은 문제가 발생되겠죠? 😇
🔴 OCP 원칙에 위배됩니다.
🔴 상태 전환이 복잡한 조건문 속에 숨어 있어서 분명하게 드러나지 않습니다.
🔴 바뀌는 부분을 전혀 캡슐화하지 않았습니다.
🔴 새로운 기능을 추가하는 과정에서 기존 코드에 없던 새로운 버그가 생길 가능성이 높습니다.
새로운 디자인 구상하기
상태 객체들을 별도의 코드에 넣고, 어떤 행동이 일어나면 현재 상태 객체에서 필요한 작업을 처리하게 합니다.
State 인터페이스를 정의해야 합니다.
모든 상태를 대상으로 상태 클래스를 구현해야 합니다. 그 상태에 해당하는 상태 클래스가 모든 작업을 책임져야 합니다.
조건문 코드를 전부 없애고 상태 클래스에 모든 작업을 위임합니다.
각 상태의 모든 행동을 한 클래스에 넣을 예정입니다. 그러면 행동을 국지화할 수 있으므로 코드를 이해하고 수정하기가 훨씬 쉬워집니다.
State 인터페이스 및 클래스 정의하기
각 상태 클래스에서 각각의 메소드가 호출되었을 때 무슨 일을 해야 하는지 봅시다. 👀
State 클래스 구현하기
정의된 상태의 메소드에 따라 클래스를 구현합니다.
GumballMachine.java
public class GumballMachine {
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
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);
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();
}
// 이 메소드를 사용하면 이 안에 뜰어있는 State 객체를 비롯한 다른 객체에서
// 뽁기 기계의 상태를 다른 상태로 전환할 수 있습니다.
void setState(State state) {
this.state = state;
}
void releaseBall() {
System.out.println("알맹이를 내보내고 있습니다.");
if (count != 0) {
count = count - 1;
}
}
int getCount() {
return count;
}
void refill(int count) {
this.count = count;
state = noQuarterState;
}
public State getState() {
return state;
}
public State getSoldOutState() {
return soldOutState;
}
public State getNoQuarterState() {
return noQuarterState;
}
public State getHasQuarterState() {
return hasQuarterState;
}
public State getSoldState() {
return soldState;
}
public String toString() {
StringBuffer result = new StringBuffer();
result.append("\nMighty Gumball, Inc.");
result.append("\nJava-enabled Standing Gumball Model #2004");
result.append("\nInventory: " + count + " gumball");
if (count != 1) {
result.append("s");
}
result.append("\n");
result.append("Machine is " + state + "\n");
return result.toString();
}
}
정적 정수 변수를 사용하던 기존의 코드를 새로 만든 클래스 사용하는 방식으로 수정합니다.
정수가 아닌 상태 객체가 저장됩니다.
다른 상태 클래스도 구현해 보겠습니다.
NoQuarterState.java
public class NoQuarterState implements State {
GumballMachine gumballMachine;
public NoQuarterState(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("동전을 넣어 주세요.");
}
}
HasQuarterState.java
public class HasQuarterState implements State {
GumballMachine gumballMachine;
public HasQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
public void insertQuarter() {
System.out.println("동전은 한 개만 넣어 주세요.");
}
public void ejectQuarter() {
System.out.println("동전이 반환됩니다.");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}
public void turnCrank() {
System.out.println("손잡이를 돌리셨습니다.");
gumballMachine.setState(gumballMachine.getSoldState());
}
public void dispense() {
System.out.println("알맹이를 내보낼 수 없습니다.");
}
}
SoldState.java
public class SoldState implements State {
GumballMachine gumballMachine;
public SoldState(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("알맹이가 매진되었습니다.");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
당첨 기능 추가하기
수정 기능인 1/10 확률로 알맹이를 추가로 받을 수 있는 당첨 기능을 추가합니다.
WinnerState.java
public class WinnerState implements State {
GumballMachine gumballMachine;
public WinnerState(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("축하드립니다. 알맹이를 하나 더 받으실 수 있습니다.");
gumballMachine.releaseBall();
if (gumballMachine.getCount() == 0) {
gumballMachine.setState(gumballMachine.getSoldOutState());
} else {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
System.out.println("더 이상 알맹이가 없습니다.");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
public String toString() {
return "despensing two gumballs for your quarter, because YOU'RE A WINNER!";
}
}
HasQuarterState.java
turnCrank() 메소드에서 당첨되었는지 확인합니다.
당첨되었고 남아있는 알맹이도 2개 이상이면 WinnerState 상태로 전환합니다.
import java.util.Random;
public class HasQuarterState implements State {
Random randomWinner = new Random(System.currentTimeMillis());
GumballMachine gumballMachine;
public HasQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
public void turnCrank() {
System.out.println("You turned...");
int winner = randomWinner.nextInt(10);
if ((winner == 0) && (gumballMachine.getCount() > 1)) {
gumballMachine.setState(gumballMachine.getWinnerState());
} else {
gumballMachine.setState(gumballMachine.getSoldState());
}
}
}
뽑기 기계 구조 다시 살펴보기
각 상태의 행동을 별개의 클래스로 국지화했습니다.
관리하기 힘든 골칫덩어리 if 선언문들을 없앴습니다.
각 상태를 변경에는 닫혀 있게 했고, GumballMachine 클래스는 새로운 상태 클래스를 추가하는 확장에는 열려 있도록 고쳤습니다 (OCP).
기존 다이어그램보다 훨씬 더 이해하기 좋은 코드 베이스와 클래스 구조를 만들었습니다.
상태 패턴 정의 (P. 440)
상태 패턴
상태 패턴(State Pattern)을 사용하면 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있습니다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있습니다.
✅ 상태를 별도의 클래스로 캡슐화한 다음 현재 상태를 나타내는 객체에게 행동을 위임하므로 내부 상태가 바뀔 때 행동이 달라지게 된다는 사실을 쉽게 알 수 있습니다.
✅ '클래스가 바뀌는 것 같은' 결과를 얻는다는 것은 지금 사용하는 객체의 행동이 완전히 달라진다면 마치 그 객체가 다른 클래스로부터 만들어진 객체처럼 느껴진다는 의미입니다.
상태 패턴 vs 전략 패턴
🧐 오늘 익은 소감은? 떠오르는 생각을 가볍게 적어보세요
✅ 상태 객체를 캡슐화하여 행동을 맡기게 되므로서 if 조건문을 줄일 수 있어서 코드 관리도 쉬워지고 이해도 쉽게 될 수 있다.
✅ 상태와 행동을 잘 구분하여 나눈 후 정의를 해야 할 것이다.
✅ 잘못된 행동에 대해서 상태를 매번 체크할 필요가 없어지고 정해진 규칙에 따라 다음 상태로 변경 되기 때문에 side effect를 줄일 수 있다.
🙋♂️ 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.
⭐️ 상태 패턴을 사용하면 디자인에 필요한 클래스의 개수가 늘어나나요? 클래스가 정말 많아진 것 같아서요.
상태의 행동을 별도의 상태 클래스에 캡슐화하다보면 클래스 개수가 늘어나기 마련입니다. 유연성을 향상시키려고 지불해야 할 비용이라고 생각하면 됩니다. 그리고 사실 대부분의 경우에 실제 클래스 개수보다는 클라이언트에게 노출되는 클래스 개수가 중요하며, 나머지 클래스를 숨길 수 있는 방법도 있습니다.
⭐️ 클라이언트에서 상태 객체와 직접 연락 하는 경우도 있나요?
그런 일은 없습니다. 상태는 Context 쪽에서 내부 상태 및 행동을 표현하는 용도로 사용되므로 상태 요청은 전부 Context로부터 오게됩니다. 클라이언트는 Context의 상태를 직접 바꿀 수 없죠. 상태를 관리하는 일은 전적으로 Context가 책임져야 하며, Context 몰래 클라이언트가 직접 Context의 상태를 변경할 일은 없습니다.