행위 패턴(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();
// 알맹이 나가는 중...
// 알맹이가 모두 소진되었습니다.
// 더 이상 판매할 알맹이가 없습니다. 동전을 반환합니다.
// 알맹이가 없습니다.