C++ State Pattern 완벽 가이드 | 상태 기계와 동작 캡슐화
이 글의 핵심
C++ State Pattern 완벽 가이드에 대한 실전 가이드입니다. 상태 기계와 동작 캡슐화 등을 예제와 함께 상세히 설명합니다.
State Pattern이란? 왜 필요한가
상태 전이를 객체로 나누는 내용은 행동 패턴 시리즈·Strategy 글과 대비하면 이해가 빨라집니다.
문제 시나리오: 조건문 폭발
문제: 객체의 상태에 따라 동작이 달라지면, 거대한 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에 위임합니다.
// 좋은 예: State Pattern
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: accept()
Established --> Closed: close()
Listen --> Closed: close()
목차
1. 기본 구조
#include <iostream>
#include <memory>
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
}
2. 상태 전이 다이어그램
TCP 연결 상태
#include <iostream>
#include <memory>
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& conn) override {
std::cout << "Already closed\n";
}
void acknowledge(TCPConnection& conn) override {
std::cout << "Error: not open\n";
}
std::string name() const override { return "Closed"; }
};
class ListenState : public TCPState {
public:
void open(TCPConnection& conn) 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& conn) override {
std::cout << "Already established\n";
}
void close(TCPConnection& conn) override;
void acknowledge(TCPConnection& conn) 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
}
3. 게임 AI 예제
#include <iostream>
#include <memory>
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) : health(hp), distanceToPlayer(dist) {
state = std::make_unique<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;
};
class PatrolState : public AIState {
public:
void update(Enemy& enemy) override {
std::cout << "Patrolling...\n";
if (enemy.getDistance() < 10) {
enemy.setState(std::make_unique<ChaseState>());
}
}
std::string name() const override { return "Patrol"; }
};
class ChaseState : public AIState {
public:
void update(Enemy& enemy) override {
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>());
}
}
std::string name() const override { return "Chase"; }
};
class AttackState : public AIState {
public:
void update(Enemy& enemy) override {
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>());
}
}
std::string name() const override { return "Attack"; }
};
class FleeState : public AIState {
public:
void update(Enemy& enemy) override {
std::cout << "Fleeing...\n";
if (enemy.getDistance() > 30) {
enemy.setState(std::make_unique<PatrolState>());
}
}
std::string name() const override { return "Flee"; }
};
int main() {
Enemy enemy(100, 15);
enemy.update(); // Patrol
enemy.setDistance(5);
enemy.update(); // Chase
enemy.setDistance(2);
enemy.update(); // Attack
enemy.takeDamage(85);
enemy.update(); // Flee
}
4. 자주 발생하는 문제와 해결법
문제 1: 순환 의존성
증상: 컴파일 에러.
원인: State가 Context를 참조, Context가 State를 참조.
// ✅ 해결: 전방 선언
class Context;
class State {
virtual void handle(Context& ctx) = 0;
};
문제 2: State 객체 생성 비용
증상: 성능 저하.
원인: 전이마다 State 객체 생성.
// ✅ 해결: Flyweight (공유)
class StateManager {
static RedState redState;
static GreenState greenState;
public:
static State* getRed() { return &redState; }
static State* getGreen() { return &greenState; }
};
5. 프로덕션 패턴
패턴 1: 상태 히스토리
class Context {
std::unique_ptr<State> state;
std::vector<std::string> history;
public:
void setState(std::unique_ptr<State> s) {
history.push_back(state->name());
state = std::move(s);
}
void printHistory() const {
for (const auto& s : history) {
std::cout << s << " -> ";
}
std::cout << state->name() << '\n';
}
};
6. 완전한 예제: 자판기
#include <iostream>
#include <memory>
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& vm) override {
std::cout << "Insert coin first\n";
}
void dispense(VendingMachine& vm) override {
std::cout << "Insert coin first\n";
}
std::string name() const override { return "NoCoin"; }
};
class HasCoinState : public State {
public:
void insertCoin(VendingMachine& vm) override {
std::cout << "Coin already inserted\n";
}
void selectProduct(VendingMachine& vm) override;
void dispense(VendingMachine& vm) override {
std::cout << "Select product first\n";
}
std::string name() const override { return "HasCoin"; }
};
class DispensingState : public State {
public:
void insertCoin(VendingMachine& vm) override {
std::cout << "Please wait\n";
}
void selectProduct(VendingMachine& vm) 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();
}
정리
| 개념 | 설명 |
|---|---|
| State Pattern | 상태별 동작을 클래스로 캡슐화 |
| 목적 | 조건문 제거, 상태 전이 명확화 |
| 구조 | Context, State, ConcreteState |
| 장점 | OCP 준수, 상태 독립성, 가독성 |
| 단점 | 클래스 증가, 상태 전이 복잡 |
| 사용 사례 | FSM, TCP, 게임 AI, 자판기 |
State Pattern은 상태 기계를 객체 지향적으로 구현하는 강력한 패턴입니다.
FAQ
Q1: State Pattern은 언제 쓰나요?
A: 상태에 따라 동작이 달라지고, 조건문이 복잡할 때 사용합니다.
Q2: Strategy Pattern과 차이는?
A: Strategy는 알고리즘 교체, State는 상태 전이에 집중합니다.
Q3: State 객체 생성 비용은?
A: Flyweight 패턴으로 State 객체를 공유하면 비용을 줄일 수 있습니다.
Q4: 상태 히스토리는?
A: std::vector로 이전 상태를 기록하면 됩니다.
Q5: 비동기 상태 전이는?
A: std::async나 이벤트 큐로 비동기 전이 가능합니다.
Q6: State Pattern 학습 리소스는?
A:
- “Design Patterns” by Gang of Four
- “Game Programming Patterns” by Robert Nystrom
- Refactoring Guru: State Pattern
한 줄 요약: State Pattern으로 상태 기계를 깔끔하게 구현할 수 있습니다. 다음으로 Decorator Pattern을 읽어보면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Strategy Pattern 완벽 가이드 | 알고리즘 캡슐화와 런타임 교체
- C++ Command Pattern 완벽 가이드 | 실행 취소와 매크로 시스템
- C++ 가상 함수 | “Virtual Functions” 가이드
관련 글
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ Command Pattern 완벽 가이드 | 실행 취소와 매크로 시스템
- C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
- C++ Decorator Pattern 완벽 가이드 | 기능 동적 추가와 조합
- C++ Default Initialization |