C++ State 패턴: 조건문 폭발을 상태 객체로 바꾸기 (TCP·게임 AI·자판기 예제)
이 글의 핵심
State 패턴으로 상태별 동작을 객체에 캡슐화하는 방법, 상태 전이 다이어그램 설계, 게임 AI와 자판기 예제, Strategy 패턴과의 차이와 상태 객체 재사용 트레이드오프를 정리합니다.
State Pattern이란? 왜 필요한가
상태 전이를 객체로 나누는 내용은 행동 패턴 시리즈와 Strategy 패턴 글과 대비하면 이해가 빠릅니다.
State Pattern을 도입하는 실질적인 이유는 개방-폐쇄 원칙(OCP)입니다. enum과 switch로 상태를 관리하면 새 상태 하나를 추가할 때마다 기존의 모든 switch 문을 찾아다니며 case를 추가해야 합니다. 상태를 다루는 함수가 5개고 상태가 10개라면, 새 상태 하나를 넣을 때 최소 5곳을 손대야 하고, 그중 하나라도 빠뜨리면 컴파일은 성공하지만 특정 상태에서만 재현되는 버그가 남습니다. State Pattern은 이 문제를 뒤집습니다. 새 상태를 추가할 때 기존 ConcreteState 클래스는 건드리지 않고 새 클래스 하나만 추가하면 됩니다.
다만 이 이점은 공짜가 아닙니다. 상태가 3~4개뿐인 작은 FSM이라면 클래스 계층과 가상 함수 호출, 파일 분리 비용이 오히려 switch 한 블록보다 유지보수 부담을 키울 수 있습니다. 상태 개수가 늘어나고 상태별 로직이 복잡해질수록, 그리고 여러 사람이 서로 다른 상태를 나눠 맡아야 할수록 State Pattern의 이점이 커집니다.
실무에서 자주 고민하게 되는 설계 지점은 전이 로직을 어디에 둘 것인가입니다. 아래 예제들처럼 ConcreteState::open() 안에서 직접 conn.setState(std::make_unique<ListenState>())를 호출하는 방식(상태 자신이 다음 상태를 결정)은 “이 상태에서 무슨 일이 일어나는가”를 한 클래스 안에서 다 볼 수 있다는 장점이 있습니다. 반면 Context가 이벤트를 받아 현재 상태와 이벤트를 보고 다음 상태를 정하는 테이블 기반 방식은 전이 규칙이 한 곳에 모여 전체 그림을 파악하기 쉽지만, 전이 테이블이 상태 수와 이벤트 수에 비례해 커집니다. 상태별로 독립적으로 테스트해야 하는지, 전이 규칙 자체가 자주 바뀌는지를 기준으로 고르면 됩니다.
조건문이 폭발하는 상태 관리 코드
객체의 상태에 따라 동작이 달라지면, 모든 메서드에 상태를 검사하는 거대한 if-else가 생깁니다.
// 나쁜 예: 조건문 폭발
class TCPConnection {
enum State { CLOSED, LISTEN, ESTABLISHED };
State state = CLOSED;
void open() {
if (state == CLOSED) {
// LISTEN으로 전이
} else if (state == LISTEN) {
// 이미 열림
} else if (state == ESTABLISHED) {
// 에러
}
}
void close() {
if (state == CLOSED) {
// 에러
} else if (state == LISTEN) {
// CLOSED로 전이
} else if (state == ESTABLISHED) {
// CLOSED로 전이
}
}
};
State Pattern은 각 상태를 클래스로 캡슐화합니다. Context는 현재 State 객체를 가지고, 요청을 그 State에 위임합니다. 아래는 구조만 보여 주는 개략적인 코드이고, 컴파일 가능한 전체 코드는 TCP 절에 있습니다.
// 구조 스케치
class TCPState {
public:
virtual void open(TCPConnection& conn) = 0;
virtual void close(TCPConnection& conn) = 0;
virtual ~TCPState() = default;
};
class ClosedState : public TCPState {
void open(TCPConnection& conn) override {
conn.setState(std::make_unique<ListenState>());
}
void close(TCPConnection& conn) override {
// 이미 닫힘: 에러 처리
}
};
class TCPConnection {
std::unique_ptr<TCPState> state;
public:
void open() { state->open(*this); }
void close() { state->close(*this); }
void setState(std::unique_ptr<TCPState> s) { state = std::move(s); }
};
stateDiagram-v2
[*] --> Closed
Closed --> Listen: open()
Listen --> Established: acknowledge()
Established --> Closed: close()
Listen --> Closed: close()
실제 TCP 상태 머신(RFC 793)에는 SYN-SENT, SYN-RECEIVED, FIN-WAIT, TIME-WAIT 등 훨씬 많은 상태가 있습니다. 이 글의 TCP 예제는 패턴을 설명하기 위해 세 상태만 남긴 단순화된 모델입니다.
Context와 State 클래스 기본 구조
#include <iostream>
#include <memory>
#include <string>
class TrafficLight;
class State {
public:
virtual void handle(TrafficLight& light) = 0;
virtual std::string name() const = 0;
virtual ~State() = default;
};
class TrafficLight {
public:
TrafficLight();
void setState(std::unique_ptr<State> s) {
state = std::move(s);
std::cout << "State: " << state->name() << '\n';
}
void change() {
state->handle(*this);
}
private:
std::unique_ptr<State> state;
};
class RedState : public State {
public:
void handle(TrafficLight& light) override;
std::string name() const override { return "Red"; }
};
class GreenState : public State {
public:
void handle(TrafficLight& light) override;
std::string name() const override { return "Green"; }
};
class YellowState : public State {
public:
void handle(TrafficLight& light) override;
std::string name() const override { return "Yellow"; }
};
void RedState::handle(TrafficLight& light) {
light.setState(std::make_unique<GreenState>());
}
void GreenState::handle(TrafficLight& light) {
light.setState(std::make_unique<YellowState>());
}
void YellowState::handle(TrafficLight& light) {
light.setState(std::make_unique<RedState>());
}
TrafficLight::TrafficLight() {
state = std::make_unique<RedState>();
std::cout << "Initial state: " << state->name() << '\n';
}
int main() {
TrafficLight light;
light.change(); // Red -> Green
light.change(); // Green -> Yellow
light.change(); // Yellow -> Red
}
Context와 State가 서로를 참조하므로, 클래스 선언을 먼저 모두 두고 서로를 사용하는 함수 정의(handle, 생성자)는 모든 클래스가 완전한 타입이 된 뒤에 둡니다. 실제 프로젝트에서는 헤더에 선언을, .cpp에 구현을 두면 자연스럽게 이 순서가 됩니다.
신호등처럼 상태가 3개뿐이고 전이가 항상 한 방향으로만 흐르면 이 코드만으로 충분합니다. 문제는 상태 수가 늘어나고 전이가 여러 방향으로 갈라지기 시작할 때입니다. 예를 들어 배치 작업의 Job 상태(대기, 실행, 재시도, 완료, 실패에 취소까지 끼어드는 구조)를 이 패턴으로 옮기면, 각 ConcreteState 클래스는 깔끔하지만 “실행 중인 Job을 취소하면 어떤 상태로 가는가”처럼 특정 상태 쌍의 전이를 확인하려면 여러 클래스 파일을 열어 봐야 합니다. 상태 클래스 하나하나는 응집도가 높아도, 전체 전이 그래프는 코드 어디에도 한 화면에 정리되어 있지 않은 것입니다. 상태가 많아질수록 이 글의 stateDiagram-v2 같은 전이 다이어그램을 코드와 함께 관리하고, 리뷰 때 다이어그램과 코드가 일치하는지 확인하는 것이 좋습니다.
TCP 연결로 보는 상태 전이
그림: 글 예제 코드의 상태 전이 — open은 Listen으로, acknowledge는 Established로, close는 Closed로 보낸다.
#include <iostream>
#include <memory>
#include <string>
class TCPConnection;
class TCPState {
public:
virtual void open(TCPConnection& conn) = 0;
virtual void close(TCPConnection& conn) = 0;
virtual void acknowledge(TCPConnection& conn) = 0;
virtual std::string name() const = 0;
virtual ~TCPState() = default;
};
class TCPConnection {
public:
TCPConnection();
void setState(std::unique_ptr<TCPState> s) {
state = std::move(s);
std::cout << "State: " << state->name() << '\n';
}
void open() { state->open(*this); }
void close() { state->close(*this); }
void acknowledge() { state->acknowledge(*this); }
private:
std::unique_ptr<TCPState> state;
};
class ClosedState : public TCPState {
public:
void open(TCPConnection& conn) override;
void close(TCPConnection&) override {
std::cout << "Already closed\n";
}
void acknowledge(TCPConnection&) override {
std::cout << "Error: not open\n";
}
std::string name() const override { return "Closed"; }
};
class ListenState : public TCPState {
public:
void open(TCPConnection&) override {
std::cout << "Already listening\n";
}
void close(TCPConnection& conn) override;
void acknowledge(TCPConnection& conn) override;
std::string name() const override { return "Listen"; }
};
class EstablishedState : public TCPState {
public:
void open(TCPConnection&) override {
std::cout << "Already established\n";
}
void close(TCPConnection& conn) override;
void acknowledge(TCPConnection&) override {
std::cout << "Data transfer...\n";
}
std::string name() const override { return "Established"; }
};
void ClosedState::open(TCPConnection& conn) {
conn.setState(std::make_unique<ListenState>());
}
void ListenState::close(TCPConnection& conn) {
conn.setState(std::make_unique<ClosedState>());
}
void ListenState::acknowledge(TCPConnection& conn) {
conn.setState(std::make_unique<EstablishedState>());
}
void EstablishedState::close(TCPConnection& conn) {
conn.setState(std::make_unique<ClosedState>());
}
TCPConnection::TCPConnection() {
state = std::make_unique<ClosedState>();
std::cout << "Initial state: " << state->name() << '\n';
}
int main() {
TCPConnection conn;
conn.open(); // Closed -> Listen
conn.acknowledge(); // Listen -> Established
conn.close(); // Established -> Closed
}
각 상태는 자기와 무관한 이벤트에 대해 무엇을 할지(무시, 에러 메시지, 예외)를 스스로 정합니다. if 문이 사라진 대신, 잘못된 이벤트를 어떻게 처리할지가 상태마다 명시적으로 드러납니다.
게임 AI 상태 머신
#include <iostream>
#include <memory>
#include <string>
class Enemy;
class AIState {
public:
virtual void update(Enemy& enemy) = 0;
virtual std::string name() const = 0;
virtual ~AIState() = default;
};
class Enemy {
public:
Enemy(int hp, int dist); // 정의는 PatrolState가 완전한 타입이 된 뒤에
void setState(std::unique_ptr<AIState> s) {
state = std::move(s);
std::cout << "AI State: " << state->name() << '\n';
}
void update() {
state->update(*this);
}
int getHealth() const { return health; }
int getDistance() const { return distanceToPlayer; }
void takeDamage(int dmg) { health -= dmg; }
void setDistance(int dist) { distanceToPlayer = dist; }
private:
std::unique_ptr<AIState> state;
int health;
int distanceToPlayer;
};
// 상태끼리 서로를 생성하므로 선언을 먼저 모두 두고, update는 아래에서 정의
class PatrolState : public AIState {
public:
void update(Enemy& enemy) override;
std::string name() const override { return "Patrol"; }
};
class ChaseState : public AIState {
public:
void update(Enemy& enemy) override;
std::string name() const override { return "Chase"; }
};
class AttackState : public AIState {
public:
void update(Enemy& enemy) override;
std::string name() const override { return "Attack"; }
};
class FleeState : public AIState {
public:
void update(Enemy& enemy) override;
std::string name() const override { return "Flee"; }
};
void PatrolState::update(Enemy& enemy) {
std::cout << "Patrolling...\n";
if (enemy.getDistance() < 10) {
enemy.setState(std::make_unique<ChaseState>());
}
}
void ChaseState::update(Enemy& enemy) {
std::cout << "Chasing player...\n";
if (enemy.getDistance() > 20) {
enemy.setState(std::make_unique<PatrolState>());
} else if (enemy.getDistance() < 3) {
enemy.setState(std::make_unique<AttackState>());
} else if (enemy.getHealth() < 20) {
enemy.setState(std::make_unique<FleeState>());
}
}
void AttackState::update(Enemy& enemy) {
std::cout << "Attacking!\n";
if (enemy.getDistance() > 5) {
enemy.setState(std::make_unique<ChaseState>());
} else if (enemy.getHealth() < 20) {
enemy.setState(std::make_unique<FleeState>());
}
}
void FleeState::update(Enemy& enemy) {
std::cout << "Fleeing...\n";
if (enemy.getDistance() > 30) {
enemy.setState(std::make_unique<PatrolState>());
}
}
Enemy::Enemy(int hp, int dist) : health(hp), distanceToPlayer(dist) {
state = std::make_unique<PatrolState>();
}
int main() {
Enemy enemy(100, 15);
enemy.update(); // Patrol 유지 (거리 15)
enemy.setDistance(5);
enemy.update(); // Patrol -> Chase
enemy.setDistance(2);
enemy.update(); // Chase -> Attack
enemy.takeDamage(85);
enemy.update(); // Attack -> Flee (체력 15)
}
PatrolState::update 안에서 std::make_unique<ChaseState>()를 쓰려면 그 시점에 ChaseState가 선언되어 있어야 합니다. 상태 클래스 안에서 update를 바로 정의하면 뒤에 나오는 상태를 참조할 수 없어 컴파일 에러가 나므로, 위처럼 클래스 선언을 모두 먼저 두고 update 정의를 뒤로 미룹니다.
게임 AI에서 이 구조를 쓸 때 흔히 만나는 문제는 경계값에서 상태가 떨리는 현상입니다. Chase는 거리 20 초과에서 Patrol로, Patrol은 거리 10 미만에서 Chase로 가도록 두 임계값을 일부러 다르게 잡았는데, 이것이 히스테리시스(hysteresis)입니다. 두 값을 같은 15로 두면 플레이어가 거리 15 근처에서 왔다 갔다 할 때마다 Patrol과 Chase가 번갈아 바뀌어, 애니메이션이 튀고 상태 진입 처리(소리 재생 등)가 반복 실행됩니다. 전이 조건을 설계할 때는 들어가는 조건과 나오는 조건 사이에 간격을 두거나, 상태에 최소 유지 시간을 두는 것이 일반적입니다.
또 ChaseState처럼 여러 전이 조건이 동시에 참일 수 있으면 평가 순서가 곧 우선순위입니다. “체력이 낮으면 거리와 상관없이 도망간다”가 의도라면 체력 검사를 맨 앞에 두어야 합니다. 위 코드에서는 거리가 3 미만이면 체력이 낮아도 먼저 Attack으로 갑니다.
순환 의존성, 전이 중 자기 삭제, 생성 비용
순환 의존성
State는 Context를 참조하고 Context는 State를 참조하므로, 서로의 헤더를 include하면 순환 include로 컴파일 에러가 납니다. 한쪽에서 전방 선언을 쓰고, 상대 타입의 멤버를 사용하는 코드는 .cpp로 옮깁니다.
class Context; // 전방 선언
class State {
public:
virtual void handle(Context& ctx) = 0; // 참조만 쓰므로 전방 선언으로 충분
virtual ~State() = default;
};
전이 중에 자기 자신이 삭제됨
state->handle(*this) 안에서 setState()를 호출하면, unique_ptr가 새 상태로 바뀌는 순간 지금 실행 중인 상태 객체가 삭제됩니다. 증상은 가끔 나는 크래시나 ASan의 heap-use-after-free 보고입니다.
void ChaseState::update(Enemy& enemy) {
if (enemy.getDistance() < 3) {
enemy.setState(std::make_unique<AttackState>()); // 여기서 *this(ChaseState)가 delete됨
log("chase -> attack, frames=" + std::to_string(frames_)); // 삭제된 객체의 멤버 접근
}
}
이 글의 예제들은 setState() 호출을 함수의 마지막 동작으로 두고 그 뒤에 멤버를 건드리지 않기 때문에 정상 동작합니다(delete this 이후 멤버에 접근하지 않으면 합법입니다). 하지만 나중에 누군가 전이 뒤에 로그 한 줄, 멤버 변수 하나를 추가하는 순간 미정의 동작이 됩니다. 삭제된 메모리가 아직 재사용되지 않았다면 멀쩡히 동작하다가 다른 할당이 끼어드는 시점에만 쓰레기 값이 나오므로 재현이 어렵습니다.
방어하는 방법은 두 가지입니다. 하나는 setState()가 새 상태를 바로 교체하지 않고 pending_ 멤버에 넣어 두었다가, update()가 끝난 뒤 Context가 교체하는 지연 전이입니다. 다른 하나는 update()가 다음 상태를 반환값으로 돌려주고 Context가 교체하게 하는 방식(std::unique_ptr<AIState> update(Enemy&), 전이가 없으면 nullptr)으로, 상태 객체가 자기 수명을 건드리지 않으므로 구조적으로 안전합니다. 진입·퇴장 동작(onEnter/onExit)을 넣을 계획이 있다면 어느 쪽이든 Context가 전이를 소유하는 구조가 다루기 쉽습니다.
State 객체 생성 비용
전이마다 std::make_unique로 상태 객체를 새로 만들면 매 전이가 힙 할당과 해제를 동반합니다. 적 유닛 수백 개가 매 프레임 상태를 오가는 게임 루프처럼 전이가 아주 잦다면 이 할당이 프로파일러에 드러날 수 있습니다. 상태 객체에 데이터가 없다면 Flyweight처럼 인스턴스를 하나씩만 만들어 공유하면 됩니다.
// 함수 지역 static: 첫 호출 때 한 번 생성되고, C++11부터 초기화가 스레드 안전함
class StateManager {
public:
static State* red() { static RedState s; return &s; }
static State* green() { static GreenState s; return &s; }
static State* yellow() { static YellowState s; return &s; }
};
다만 이 최적화는 공짜가 아닙니다. 상태 객체가 공유되면 그 상태 객체는 어떤 가변 데이터도 가지면 안 됩니다. 상태 객체 안에 멤버 변수를 두고 값을 바꾸는 순간, 같은 인스턴스를 참조하는 모든 Context가 그 값을 공유하게 되어 멀티스레드 환경은 물론 단일 스레드에서도 서로 다른 Context끼리 상태가 오염됩니다. 공유하려면 상태별 데이터는 Context 쪽에 두고 State 클래스는 무상태(stateless) 함수 집합으로 유지해야 합니다. 진입 시각을 기록해야 하는 상태처럼 이 제약을 지킬 수 없다면 unique_ptr 방식을 유지하는 편이 안전합니다.
공유 방식으로 바꿀 때는 Context의 멤버 타입도 함께 바꿔야 합니다. StateManager::red()가 돌려주는 포인터를 std::unique_ptr<State>에 넣으면, 전이할 때 unique_ptr가 정적 객체를 delete하려 해서 free(): invalid pointer 같은 크래시가 납니다. 공유 상태는 소유권이 Context에 없으므로 State* state;처럼 비소유 포인터로 들고 있어야 하고(참조는 다른 상태로 다시 바인딩할 수 없으므로 맞지 않습니다), 그러면 앞의 자기 삭제 문제도 함께 사라집니다.
C++17 이후라면 가상 함수 대신 std::variant<Patrol, Chase, Attack, Flee>로 상태를 표현하는 방법도 있습니다. 상태가 Context 안에 값으로 들어가므로 힙 할당이 없고, 상태마다 오버로드를 둔 방문자로 std::visit을 호출하면 새 상태를 추가하고 처리 함수를 빠뜨렸을 때 컴파일 에러로 알 수 있습니다(포괄적인 제네릭 람다를 함께 두면 이 검사가 사라집니다). 서두의 “switch에 case를 빠뜨려도 컴파일된다” 문제를 컴파일러가 잡아 주는 셈입니다. 대신 모든 상태 타입이 한 곳의 variant 정의에 나열되어야 하므로, 기존 코드를 건드리지 않고 새 상태 클래스만 추가한다는 OCP 이점은 포기해야 합니다. 상태 목록이 고정되어 있고 성능이 중요한 게임 루프나 파서라면 variant, 플러그인처럼 상태가 외부에서 추가될 수 있다면 가상 함수 기반 State가 맞습니다.
State와 Strategy는 왜 헷갈릴까
두 패턴은 구조가 거의 같습니다. 둘 다 인터페이스 뒤에 여러 구현체를 두고 Context가 그중 하나에 위임합니다. 차이는 코드 구조가 아니라 의도에 있습니다. Strategy는 정렬 알고리즘 교체처럼 “이 알고리즘을 저 알고리즘으로 바꿔 끼운다”는 의도이고, State는 “이 객체가 시간이 지나며 다른 국면으로 전이한다”는 의도입니다. 구현체가 스스로 다음 구현체로 전이시키는 코드(setState(...) 호출)가 있으면 State이고, 외부에서 한 번 선택되면 바뀌지 않는다면 Strategy에 가깝습니다. 처음 설계할 때 “이 객체가 상태를 스스로 바꿀 수 있는가”를 정해 두면 이름을 두고 혼란스러워질 일이 줄어듭니다.
멀티스레드와 비동기 전이
여러 스레드가 Context의 메서드를 직접 부르게 하기보다, 이벤트를 큐에 넣고 한 스레드(이벤트 루프)가 순서대로 꺼내 처리하게 만드는 것이 가장 안전합니다. 전이가 항상 한 스레드에서만 일어나므로 뮤텍스 없이도 경쟁 조건이 생기지 않고, 이벤트 순서가 로그로 남아 재현도 쉽습니다. 네트워크 응답처럼 나중에 도착하는 결과는 그 결과 자체를 이벤트로 큐에 넣고, 도착했을 때 상태가 이미 바뀌었다면(예: 이미 Closed) 무시하도록 처리합니다.
상태 히스토리로 전이 경로 남기기
class Context {
std::unique_ptr<State> state;
std::vector<std::string> history;
public:
void setState(std::unique_ptr<State> s) {
if (state) history.push_back(state->name()); // 첫 설정 시 state는 nullptr
state = std::move(s);
}
void printHistory() const {
for (const auto& s : history) {
std::cout << s << " -> ";
}
if (state) std::cout << state->name();
std::cout << '\n';
}
};
히스토리는 단순해 보이지만 디버깅에 효과가 큰 장치입니다. 상태 머신 버그는 “지금 상태가 이상하다”보다 “어떤 경로로 이 상태에 도달했는가”를 알아야 풀리는 경우가 많은데, 크래시 덤프나 에러 로그에 최근 전이 몇 개(Patrol -> Chase -> Attack -> Flee)가 함께 찍혀 있으면 재현 절차를 거의 그대로 얻을 수 있습니다. 오래 사는 객체에서 vector에 계속 쌓으면 메모리가 끝없이 늘어나므로, 실무에서는 최근 N개만 보관하는 고정 크기 링 버퍼를 쓰는 편이 일반적입니다.
자판기 상태 머신 전체 예제
#include <iostream>
#include <memory>
#include <string>
class VendingMachine;
class State {
public:
virtual void insertCoin(VendingMachine& vm) = 0;
virtual void selectProduct(VendingMachine& vm) = 0;
virtual void dispense(VendingMachine& vm) = 0;
virtual std::string name() const = 0;
virtual ~State() = default;
};
class VendingMachine {
public:
VendingMachine();
void setState(std::unique_ptr<State> s) {
state = std::move(s);
std::cout << "[State: " << state->name() << "]\n";
}
void insertCoin() { state->insertCoin(*this); }
void selectProduct() { state->selectProduct(*this); }
void dispense() { state->dispense(*this); }
private:
std::unique_ptr<State> state;
};
class NoCoinState : public State {
public:
void insertCoin(VendingMachine& vm) override;
void selectProduct(VendingMachine&) override {
std::cout << "Insert coin first\n";
}
void dispense(VendingMachine&) override {
std::cout << "Insert coin first\n";
}
std::string name() const override { return "NoCoin"; }
};
class HasCoinState : public State {
public:
void insertCoin(VendingMachine&) override {
std::cout << "Coin already inserted\n";
}
void selectProduct(VendingMachine& vm) override;
void dispense(VendingMachine&) override {
std::cout << "Select product first\n";
}
std::string name() const override { return "HasCoin"; }
};
class DispensingState : public State {
public:
void insertCoin(VendingMachine&) override {
std::cout << "Please wait\n";
}
void selectProduct(VendingMachine&) override {
std::cout << "Please wait\n";
}
void dispense(VendingMachine& vm) override;
std::string name() const override { return "Dispensing"; }
};
void NoCoinState::insertCoin(VendingMachine& vm) {
std::cout << "Coin inserted\n";
vm.setState(std::make_unique<HasCoinState>());
}
void HasCoinState::selectProduct(VendingMachine& vm) {
std::cout << "Product selected\n";
vm.setState(std::make_unique<DispensingState>());
}
void DispensingState::dispense(VendingMachine& vm) {
std::cout << "Dispensing product...\n";
vm.setState(std::make_unique<NoCoinState>());
}
VendingMachine::VendingMachine() {
state = std::make_unique<NoCoinState>();
std::cout << "[Initial state: " << state->name() << "]\n";
}
int main() {
VendingMachine vm;
vm.insertCoin();
vm.selectProduct();
vm.dispense();
}
동전이 없을 때 상품을 고르거나, 배출 중에 동전을 넣는 것처럼 잘못된 순서의 조작은 각 상태 클래스가 안내 메시지로 처리합니다. 조건문 하나 없이 “어떤 상태에서 어떤 조작이 허용되는가”가 클래스 구조로 표현됩니다.
더 읽을거리로는 GoF의 “Design Patterns”, Robert Nystrom의 “Game Programming Patterns”의 State 장, Refactoring Guru: State Pattern이 있습니다.