상태 기계! 상태 머신. 유한 상태 기계(FSM, finite-state machine)
위키 백과에 따르면 상태 머신은 유한한 갯수의 상태를 가질 수 있는 오토마타, 즉 추상 기계라고 한다. 이러한 기계는 한 번에 오로지 하나의 상태만을 가지고, 현재 상태(current state)란 임의의 주어진 시간의 상태를 칭한다.
한 상태는 어떤 사건(event)에 의해 다른 상태로 변화할 수 있으며, 이를 전이(transition)라고 한다.
특정한 유한 오토마톤은 현재 상태로부터 가능한 전이 상태 + 전이를 유발하는 조건들의 집합으로 정의된다.
상태 패턴(State Pattern)은 행동과 상태를 나눈 패턴이다.
행동을 인터페이스로 정의하여, 상태에 따라 행동들을 분류시킨다. 유한 상태 기계(FSM)는 상태와 행동들을 노드로 연결시켜 도식화한 것을 말한다.
상태 패턴이란, 객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보인다.
게임을 예로 들어본다.
슈퍼마리오를 움직이는 행동은 세 가지다.
땅에 있을 때 아래 버튼을 누르면 엎드리고, 버튼을 떼면 다시 일어선다.
B버튼을 누르면 점프를 한다.
여기서 슈퍼마리오는 네 가지의 상태를 가진다.
이 내용을 FSM을 사용하여 노드로 표현한다면 이런 플로우차트가 나온다.
스위치 문을 사용하여 구현하는 것은 가장 쉽고 흔한 방법이다.
switch (currentState) {
case ST_IDLE:
// do something in the idle state
break;
case ST_STOP:
// do something in the stop state
break;
// etc...
}
이 방법은 이벤트 기반의 멀티 스레드 프로젝트에는 적합하지 않다. 상태 머신은 싱글 스레드에 적합하다.
상태 머신을 사용하면 복잡한 문제를 단순화하기에 좋다. 프로그램을 상태로 나누고, 각 상태는 제한된 동작을 수행하도록 한다. 이벤트들이 상태를 변화시키는 트리거로 작용한다.
이벤트가 발생했을 때 상태 변이는 현재 머신의 상태에 따라 달라진다.
신호등을 예시로 들어보자.
신호등은 세 가지 상태를 가진다.
이 상태들에는 규칙이 있다. 빨간불이었을때는 초록불로, 초록불은 노란불로, 다시 노란불은 빨간불로 바뀐다. 이 패턴이 반복되는 것이다.
여기서 다음 상태로의 전이를 결정하는 것은 상태 객체라는 점에 유의해야 한다. 신호등의 현재 상태를 바꾸는 것은 신호등 자체가 아니라 상태 객체이다.
const TrafficLight = () => {
let count = 0;
let currentState = new Red(this);
this.change = (state) => {
// limits number of changes
if (count++ >= 10) return;
currentState = state;
currentState.go();
};
this.start = () => {
currentState.go();
};
}
const Red = (light) => {
this.light = light;
this.go = () => {
console.log("Red --> for 1 minute");
light.change(new Green(light));
}
};
const Yellow = (light) => {
this.light = light;
this.go = () => {
console.log("Yellow --> for 10 seconds");
light.change(new Red(light));
}
};
const Green = (light) => {
this.light = light;
this.go = () => {
console.log("Green --> for 1 minute");
light.change(new Yellow(light));
}
};
const run = () => {
let light = new TrafficLight();
light.start();
}
나는 var를 못보는 병에 걸려서 그것만 바꿔줬다;
아무튼 이건 정말 간단한 예시이고,
상태 클래스들을 인터페이스로 캡슐화해주면, 클라이언트에서 인터페이스를 호출하여 사용하는 것이다.
class TrafficLight {
constructor() {
this.states = [new GreenLight(), new RedLight(), new YellowLight()];
this.current = this.states[0];
}
change() {
const totalStates = this.states.length;
let currentIndex = this.states.findIndex(light => light === this.current);
if (currentIndex + 1 < totalStates) this.current = this.states[currentIndex + 1];
else this.current = this.states[0];
}
sign() {
return this.current.sign();
}
}
class Light {
constructor(light) {
this.light = light;
}
}
class RedLight extends Light {
constructor() {
super('red');
}
sign() {
return 'STOP';
}
}
class YellowLight extends Light {
constructor() {
super('yellow');
}
sign() {
return 'STEADY';
}
}
class GreenLight extends Light {
constructor() {
super('green');
}
sign() {
return 'GO';
}
}
// usage
const trafficLight = new TrafficLight();
console.log(trafficLight.sign()); // 'GO'
trafficLight.change();
console.log(trafficLight.sign()); // 'STOP'
trafficLight.change();
console.log(trafficLight.sign()); // 'STEADY'
trafficLight.change();
console.log(trafficLight.sign()); // 'GO'
trafficLight.change();
console.log(trafficLight.sign()); // 'STOP'
자바스크립트 예시는 찾기가 힘들어서, 타입스크립트 예시를 가져왔다.
/**
* The Context defines the interface of interest to clients. It also maintains a
* reference to an instance of a State subclass, which represents the current
* state of the Context.
*/
class Context {
/**
* @type {State} A reference to the current state of the Context.
*/
private state: State;
constructor(state: State) {
this.transitionTo(state);
}
/**
* The Context allows changing the State object at runtime.
*/
public transitionTo(state: State): void {
console.log(`Context: Transition to ${(<any>state).constructor.name}.`);
this.state = state;
this.state.setContext(this);
}
/**
* The Context delegates part of its behavior to the current State object.
*/
public request1(): void {
this.state.handle1();
}
public request2(): void {
this.state.handle2();
}
}
/**
* The base State class declares methods that all Concrete State should
* implement and also provides a backreference to the Context object, associated
* with the State. This backreference can be used by States to transition the
* Context to another State.
*/
abstract class State {
protected context: Context;
public setContext(context: Context) {
this.context = context;
}
public abstract handle1(): void;
public abstract handle2(): void;
}
/**
* Concrete States implement various behaviors, associated with a state of the
* Context.
*/
class ConcreteStateA extends State {
public handle1(): void {
console.log('ConcreteStateA handles request1.');
console.log('ConcreteStateA wants to change the state of the context.');
this.context.transitionTo(new ConcreteStateB());
}
public handle2(): void {
console.log('ConcreteStateA handles request2.');
}
}
class ConcreteStateB extends State {
public handle1(): void {
console.log('ConcreteStateB handles request1.');
}
public handle2(): void {
console.log('ConcreteStateB handles request2.');
console.log('ConcreteStateB wants to change the state of the context.');
this.context.transitionTo(new ConcreteStateA());
}
}
/**
* The client code.
*/
const context = new Context(new ConcreteStateA());
context.request1();
context.request2();
<GoF의 디자인 패턴> 이라는 책이 디자인 패턴에 대한 교과서같은 느낌인데 GoF가 디자인 패턴 분야의 F4인거 같다 개웃기다 Gang of Four ㅋ ㅋㅋ ㅋㅋㅋㅋ 암튼 이 F4의 책이 좋은 모양이니 한 번 읽어보면 좋을듯 하다.