행위 패턴(Behavioral Pattern) 중 하나로, 객체가 내부 상태(state) 변경에 따라 행동(behavior)과 상태 전이를 각각의 상태 객체에 위임하는 패턴입니다.
예를 들어 뽑기 기계를 개발할때, “동전 있음”, “동전 없음”, “상품 판매”, “상품 매진” 등의 상태는“동전 투입”, “동전 반환”, “손잡이 돌림”, “알맹이 내보냄” 등의 행동 에 따라 변경됩니다.
각각의 행동에 대한 함수를 구현해봅시다. 아래와 같은 고려사항이 있을 수 있습니다.
위와 같은 상황을 고려하여 구현하려면 행동 함수 내부의 많은 if-else가 필요합니다. 또한, 변경 사항이 발생(재고 보충, 특별 보너스 상품 지급 등등)하면 내부 코드를 전체적으로 고려하여 변경해야 합니다. (버그 발생 위험 높아짐)
이러한 문제를 해결하고자 각각의 상태에 대한 객체를 생성하고(NoCoinState, HasCoinState, SoldState, SoldOutState) 내부에서는 현재 상태에서 실행할 행동(insertCoin(), ejectCoin() ...)과 상태전이를 처리하도록 캡슐화 하여 각각의 객체에게 책임을 위임하는 패턴이 상태 패턴이 등장하였습니다.
이렇게 각각의 객체에게 위임하게 되면 OCP를 준수하게 되어 확장이 용이하고, SRP를 준수하게 되어 테스트가 용이해집니다.

State Context의 뼈대를 만듭니다. State Object를 보유하고 상태 전의를 관리하는 상태의 주인State Interface를 생성합니다.State Interface로 각각의 State Object 생성합니다.State Context의 내부를 다시 구현합니다.💡 구성 관계 vs 참조 관계
(1) 구성 관계
구성 관계는 소스 코드 내에서 new를 써서 내부에서 직접 인스턴스를 생성하고 그 객체의 생명주기를 자신이 책임집니다.
⇒ 부분(part)이 전체(whole)에 강하게 종속되어, 전체가 생성·파괴될 때 부분도 함께 생성·파괴되어야 할 때 사용
(2) 참조 관계
참조관계는 외부에서 만들어진 인스턴스를 단순히 constructor 파라미터로 받아와서 참조만 할당하여 사용만 할뿐, 생성/파괴 책임은 없습니다.
⇒ 한 객체가 다른 객체를 사용만 해야 할때 사용

State interface
export interface State {
insertCoin(): void;
ejectCoin(): void;
turnCrank(): void;
dispense(): void;
}
Context class
import { State } from './State';
import { NoCoinState } from './NoCoinState';
import { HasCoinState } from './HasCoinState';
import { SoldState } from './SoldState';
import { SoldOutState } from './SoldOutState';
export class GumballMachine {
private noCoinState: State;
private hasCoinState: State;
private soldState: State;
private soldOutState: State;
private state: State;
private count: number;
constructor(count: number) {
this.count = count;
this.noCoinState = new NoCoinState(this);
this.hasCoinState = new HasCoinState(this);
this.soldState = new SoldState(this);
this.soldOutState = new SoldOutState(this);
this.state = count > 0 ? this.noCoinState : this.soldOutState;
}
// 상태 전이를 위한 세터/게터
setState(state: State) {
this.state = state;
}
getNoCoinState() { return this.noCoinState; }
getHasCoinState() { return this.hasCoinState; }
getSoldState() { return this.soldState; }
getSoldOutState() { return this.soldOutState; }
// 알맹이 재고 차감
releaseBall(): void {
if (this.count > 0) {
console.log('알맹이 나가는 중...');
this.count--;
}
}
// 외부에서 호출할 동작
insertCoin() { this.state.insertCoin(); }
ejectCoin() { this.state.ejectCoin(); }
turnCrank() {
this.state.turnCrank();
this.state.dispense();
}
getCount() { return this.count; }
}
각각의 State Object
import { State } from './State';
import { GumballMachine } from './GumballMachine';
export class NoCoinState implements State {
constructor(private machine: GumballMachine) {}
insertCoin(): void {
console.log('동전이 투입되었습니다.');
this.machine.setState(this.machine.getHasCoinState());
}
ejectCoin(): void {
console.log('동전이 없습니다. 반환할 수 없습니다.');
}
turnCrank(): void {
console.log('먼저 동전을 넣어주세요.');
}
dispense(): void {
console.log('알맹이를 받을 수 없습니다.');
}
}
// HasCoinState.ts
import { State } from './State';
import { GumballMachine } from './GumballMachine';
export class HasCoinState implements State {
constructor(private machine: GumballMachine) {}
insertCoin(): void {
console.log('이미 동전이 있습니다. 추가로 넣을 수 없습니다.');
}
ejectCoin(): void {
console.log('동전을 반환합니다.');
this.machine.setState(this.machine.getNoCoinState());
}
turnCrank(): void {
console.log('손잡이를 돌리셨습니다...');
this.machine.setState(this.machine.getSoldState());
}
dispense(): void {
console.log('알맹이를 받을 수 없습니다.');
}
}
// SoldState.ts
import { State } from './State';
import { GumballMachine } from './GumballMachine';
export class SoldState implements State {
constructor(private machine: GumballMachine) {}
insertCoin(): void {
console.log('잠시만 기다려주세요. 알맹이가 나가는 중입니다.');
}
ejectCoin(): void {
console.log('이미 손잡이를 돌리셨습니다. 반환 불가합니다.');
}
turnCrank(): void {
console.log('손잡이는 한 번만 돌려주세요.');
}
dispense(): void {
this.machine.releaseBall();
if (this.machine.getCount() > 0) {
this.machine.setState(this.machine.getNoCoinState());
} else {
console.log('알맹이가 모두 소진되었습니다.');
this.machine.setState(this.machine.getSoldOutState());
}
}
}
// SoldOutState.ts
import { State } from './State';
import { GumballMachine } from './GumballMachine';
export class SoldOutState implements State {
constructor(private machine: GumballMachine) {}
insertCoin(): void {
console.log('더 이상 판매할 알맹이가 없습니다. 동전을 반환합니다.');
}
ejectCoin(): void {
console.log('동전이 없습니다.');
}
turnCrank(): void {
console.log('알맹이가 없어서 손잡이를 돌릴 수 없습니다.');
}
dispense(): void {
console.log('알맹이가 없습니다.');
}
}
실제 사용하는 부분
// main.ts (테스트)
import { GumballMachine } from './GumballMachine';
const machine = new GumballMachine(2);
machine.insertCoin();
machine.turnCrank();
// 동전이 투입되었습니다.
// 손잡이를 돌리셨습니다...
// 알맹이 나가는 중...
// 상태가 NoCoinState로 전이
machine.insertCoin();
machine.ejectCoin();
// 동전이 투입되었습니다.
// 동전을 반환합니다.
machine.insertCoin();
machine.turnCrank();
machine.insertCoin();
machine.turnCrank();
// 알맹이 나가는 중...
// 알맹이가 모두 소진되었습니다.
// 더 이상 판매할 알맹이가 없습니다. 동전을 반환합니다.
// 알맹이가 없습니다.