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가 이벤트를 받아서 현재 상태와 이벤트를 보고 다음 상태를 결정하는 테이블 기반 방식은 전이 로직이 한 곳에 모여 있어 전체 그림을 파악하기는 쉽지만, 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에 위임합니다.

// 좋은 예: 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()

Context와 State 클래스 기본 구조

#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
}

신호등처럼 상태가 3개뿐이고 전이가 항상 한 방향으로만 흐르면 위 코드만으로 충분합니다. 문제는 상태 수가 늘어나고 전이가 여러 방향으로 갈라지기 시작할 때입니다. 예전에 사내 배치 작업 스케줄러의 Job 상태(대기 → 실행 → 재시도 → 완료/실패, 여기에 취소까지 끼어드는 구조)를 이 패턴으로 옮긴 적이 있는데, 각 ConcreteState 클래스는 깔끔했지만 “실행 중인 Job을 취소하면 어떤 상태로 가는가”처럼 특정 상태 쌍의 전이를 확인하려면 클래스 파일을 몇 개씩 열어봐야 했습니다. 각 상태 클래스만 보면 응집도가 높아 보이지만, 전체 상태 전이 그래프는 코드 어디에도 한 화면에 정리되어 있지 않았던 겁니다. 결국 별도로 전이 다이어그램(지금 이 글에 있는 것과 같은 mermaid stateDiagram-v2)을 코드와 별개로 관리하고, 코드 리뷰 때마다 “다이어그램과 실제 코드가 어긋나지 않았는지”를 체크리스트에 넣는 방식으로 해결했습니다. 상태 클래스가 6~7개를 넘어가는 시점부터는 처음부터 전이 다이어그램을 문서화하고 시작하는 편을 권합니다.


TCP 연결로 보는 상태 전이 다이어그램

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
}

게임 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);  // 정의는 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"; }
};
Enemy::Enemy(int hp, int dist) : health(hp), distanceToPlayer(dist) {
    state = std::make_unique<PatrolState>();
}
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
}

원래 이 예제는 Enemy 생성자를 클래스 안에서 정의하면서 std::make_unique<PatrolState>()를 호출했는데, 그 시점에는 PatrolState가 아직 선언되지 않아 'PatrolState' was not declared in this scope 에러가 납니다. State 패턴에서는 Context와 State가 서로를 알아야 하므로, 이처럼 선언은 먼저, 서로를 사용하는 함수 정의는 모든 클래스가 완전해진 뒤에 두는 배치가 거의 항상 필요합니다. 실제 프로젝트에서는 Enemy.h/AIStates.h로 선언을 나누고, 구현을 .cpp에 두면 이 문제가 자연스럽게 해결됩니다.

게임 AI에서 이 구조를 쓸 때 흔히 만나는 문제는 경계값에서 상태가 떨리는 현상입니다. Chase는 거리 20 초과에서 Patrol로, Patrol은 거리 10 미만에서 Chase로 가도록 두 임계값을 일부러 다르게 잡았는데, 이것이 히스테리시스(hysteresis)입니다. 두 값을 같은 15로 두면 플레이어가 거리 15 근처에서 왔다 갔다 할 때마다 매 프레임 Patrol↔Chase가 번갈아 바뀌어, 애니메이션이 튀고 상태 진입 처리(소리 재생 등)가 반복 실행됩니다. 전이 조건을 설계할 때는 “들어가는 조건”과 “나오는 조건” 사이에 간격을 두거나, 상태에 최소 유지 시간을 두는 것이 일반적입니다. 또 ChaseState의 조건 순서처럼 여러 전이 조건이 동시에 참일 때 어느 것이 먼저 평가되는지가 곧 우선순위이므로, “체력이 낮으면 거리와 상관없이 도망간다”가 의도라면 체력 검사를 맨 앞에 두어야 합니다. 위 코드에서는 거리 3 미만이면 체력이 낮아도 먼저 Attack으로 갑니다.


순환 의존성·전이 중 자기 삭제·생성 비용

순환 의존성

증상: 컴파일 에러. 원인: State가 Context를 참조, Context가 State를 참조.

// ✅ 해결: 전방 선언
class Context;
class State {
    virtual void handle(Context& ctx) = 0;
};

전이 중에 자기 자신이 삭제됨

증상: 가끔 크래시, 또는 ASan에서 heap-use-after-free. 원인: state->handle(*this) 안에서 setState()를 호출하면, unique_ptr가 새 상태로 바뀌는 순간 지금 실행 중인 상태 객체가 삭제됩니다.

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가 교체하는 지연 전이입니다. 다른 하나는 handle()이 다음 상태를 반환값으로 돌려주고 Context가 교체하게 하는 방식(std::unique_ptr<State> update(Enemy&), 전이가 없으면 nullptr)으로, 상태 객체가 자기 수명을 건드리지 않으므로 구조적으로 안전합니다. 진입·퇴장 동작(onEnter/onExit)을 넣을 계획이 있다면 어느 쪽이든 Context가 전이를 소유하는 구조가 훨씬 다루기 쉽습니다.

State 객체 생성 비용

증상: 성능 저하. 원인: 전이마다 State 객체 생성.

// ✅ 해결: Flyweight (공유)
class StateManager {
    static RedState redState;
    static GreenState greenState;
public:
    static State* getRed() { return &redState; }
    static State* getGreen() { return &greenState; }
};

이 문제는 이론이 아니라 실제로 프로파일러에 찍힙니다. 게임 AI FSM을 위 예제와 거의 동일한 구조(std::make_unique<ChaseState>()를 전이마다 새로 호출하는 방식)로 만들었다가, 적 유닛 수백 개가 매 프레임 상태를 들락날락하는 구간에서 perf로 봤을 때 operator new/operator delete 호출 비중이 예상보다 높게 나온 적이 있습니다. 상태 객체 자체는 멤버 변수가 거의 없는 가벼운 클래스인데도, 프레임마다 수백 번씩 힙 할당·해제가 반복되니 할당자 오버헤드가 누적된 것입니다. 위 StateManager처럼 상태 객체를 정적 인스턴스로 미리 만들어두고 포인터만 넘겨주는 방식으로 바꾸면 이 할당 비용은 사라집니다.

다만 이 최적화는 공짜가 아닙니다. 상태 객체가 정적으로 공유되면 그 상태 객체 자체는 어떤 가변 상태도 가지면 안 됩니다 — 상태 객체 안에 멤버 변수를 두고 값을 바꾸는 순간, 같은 RedState 인스턴스를 참조하는 모든 Context가 그 값을 공유하게 되어 멀티스레드 환경에서는 물론 단일 스레드에서도 서로 다른 Context끼리 상태가 오염될 수 있습니다. 즉 Flyweight로 전환하려면 상태별 데이터는 반드시 Context 쪽에 두고, State 클래스는 순수하게 무상태(stateless) 함수 집합으로 유지해야 합니다. 이 제약을 지킬 수 없는 상태(예: 진입 시각을 기록해야 하는 상태)라면 무리하게 공유하지 말고 unique_ptr 방식을 유지하는 편이 안전합니다.

공유 방식으로 바꿀 때는 Context의 멤버 타입도 함께 바꿔야 합니다. StateManager::getRed()가 돌려주는 포인터를 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 정의에 나열되어야 하므로, “기존 코드를 건드리지 않고 새 상태 클래스만 추가한다”는 State 패턴의 OCP 이점은 포기해야 합니다. 상태 목록이 고정되어 있고 성능이 중요한 게임 루프나 파서라면 variant, 플러그인처럼 상태가 외부에서 추가될 수 있다면 가상 함수 기반 State가 맞습니다.

State vs Strategy, 왜 항상 헷갈릴까: 두 패턴은 구조적으로 거의 동일합니다 — 둘 다 인터페이스 뒤에 여러 구현체를 두고 Context가 그중 하나로 위임합니다. 차이는 코드 구조가 아니라 의도에 있습니다. Strategy는 “이 알고리즘을 저 알고리즘으로 바꿔 끼운다”는 의도(예: 정렬 알고리즘 교체)이고, State는 “이 객체가 시간이 지나며 다른 국면(상태)으로 전이한다”는 의도입니다. 실무에서 헷갈리는 이유는 State 구현체가 스스로 다음 State로 전이시키는 코드(setState(...)를 호출하는 부분)가 있으면 명백히 State이고, 전이 없이 한 번 선택되면 끝까지 그대로 쓰이면 Strategy에 가깝다는 식으로 코드를 보고 사후에 판단해야 하기 때문입니다. 처음 설계할 때 “이 객체가 상태를 스스로 바꿀 수 있는가”를 먼저 정해두면 나중에 코드 리뷰에서 이름을 두고 논쟁하는 일을 줄일 수 있습니다.


상태 히스토리로 이전 상태 되돌리기

상태 히스토리

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 << " -> ";
        }
        std::cout << state->name() << '\n';
    }
};

히스토리는 단순해 보이지만 디버깅에서 가장 효과가 큰 장치입니다. 상태 머신 버그의 대부분은 “지금 상태가 이상하다”가 아니라 “어떤 경로로 이 상태에 도달했는가”를 알아야 풀리는데, 크래시 덤프나 에러 로그에 최근 전이 몇 개(Patrol -> Chase -> Attack -> Flee)가 함께 찍혀 있으면 재현 절차를 거의 그대로 얻을 수 있습니다. 다만 오래 사는 객체에서 vector에 계속 쌓으면 메모리가 끝없이 늘어나므로, 실무에서는 최근 N개만 보관하는 고정 크기 링 버퍼로 두는 편이 일반적입니다.


자판기 상태 머신 전체 예제

#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 요약

개념설명
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: 여러 스레드가 Context의 메서드를 직접 부르게 하기보다, 이벤트를 큐에 넣고 한 스레드(이벤트 루프)가 순서대로 꺼내 처리하게 만드는 것이 가장 안전합니다. 전이가 항상 한 스레드에서만 일어나므로 뮤텍스 없이도 경쟁 조건이 생기지 않고, 이벤트 순서가 로그로 남아 재현도 쉽습니다. 네트워크 응답처럼 나중에 도착하는 결과는 그 결과 자체를 이벤트로 큐에 넣고, 도착했을 때 상태가 이미 바뀌었다면(예: 이미 Closed) 무시하도록 처리합니다.

Q6: State Pattern 학습 리소스는?

A:

  • “Design Patterns” by Gang of Four
  • “Game Programming Patterns” by Robert Nystrom
  • Refactoring Guru: State Pattern State Pattern으로 상태 기계를 깔끔하게 구현할 수 있습니다. 다음으로 Decorator Pattern을 읽어보면 좋습니다.

관련 글