C++ State Pattern 완벽 가이드 | 상태 기계와 동작 캡슐화

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. 기본 구조
  2. 상태 전이 다이어그램
  3. 게임 AI 예제
  4. 자주 발생하는 문제와 해결법
  5. 프로덕션 패턴
  6. 완전한 예제: 자판기

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:

한 줄 요약: 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 |