State Design Pattern
객체의 내부 상태가 변경될 때 행동도 바꿔서, 마치 클래스가 바뀐 것처럼 동작하게 만드는 디자인 패턴.

우리는 흔히 삶의 선택지 앞에서 고민합니다. "만약 이렇다면 A를 하고, 저렇다면 B를 하겠다"는 식의 사고방식은 매우 인간적이고 직관적이죠. 하지만 프로그래밍의 세계에서 이 '만약에(if)'라는 가설이 수십, 수백 개로 늘어나는 순간, 우리의 코드는 마치 출구 없는 미로처럼 변해버립니다.
옵저버(Observer) 패턴이 변화를 외부에 알리는 '소식 전달자'였다면, 오늘 여러분과 함께 살펴볼 **스테이트 패턴(State Pattern)**은 시스템의 '인격' 자체를 통째로 바꾸는 마법과도 같습니다.
1. 행동은 상태에 의해 결정된다
상상해 보세요. 여러분이 게임 속 캐릭터를 조종하고 있습니다. 평상시에는 뚜벅뚜벅 걷던 캐릭터가, 마법사 아이템을 장착하는 순간 번개를 내뿜고, 전사 아이템을 들면 묵직한 근접 공격을 퍼붓습니다. 선택된 '상태'에 따라 캐릭터의 근간이 바뀌는 것이죠.
조금 더 일상적인 예로 들어가 볼까요? 우리 손에 들린 스마트폰의 전원 버튼을 떠올려 보세요.
- 상황 A (OFF 상태): 버튼을 누르면 화면이 켜지며 환영 메시지가 뜹니다.
- 상황 B (ON 상태): 동일한 버튼을 눌렀지만, 이번에는 화면이 꺼지며 작별 인사를 합니다.
똑같은 입력(버튼 클릭)임에도 결과가 판이한 이유, 그것은 바로 '현재 상태'가 다르기 때문입니다.
2. "상태를 조건문으로 분기하지 마세요"
초보 개발자 시절의 우리는 아마 다음과 같은 코드를 작성했을 겁니다.
void onPowerButtonPressed() {
if (state == ON) {
turnOff();
} else if (state == OFF) {
turnOn();
} // 절전 모드, 배터리 부족, 통화 중... if는 계속 늘어납니다.
}
처음에는 이 방식이 충분히 직관적으로 보일 수 있습니다. 하지만 기능이 확장될수록 "조건문의 저주"가 시작됩니다. 상태가 절전 모드, 잠금 화면, 통화 중, 배터리 없음 등으로 확장되기 시작하면 조건문은 빠르게 비대해지고 유지보수가 어려워집니다. 이 지점에서 설계의 한계가 드러나죠.

스테이트 패턴의 핵심 아이디어는 단순하지만 강력합니다. **"상태를 조건문으로 분기하지 말고, 상태 자체를 하나의 객체로 모델링하라."**는 것이죠. 즉, ‘켜짐’과 ‘꺼짐’을 값이 아닌 객체로 분리하고, 스마트폰은 현재 장착된 상태 객체에 동작을 위임합니다. 본체는 자신이 어떤 상태인지 판단하지 않고, 그 책임을 상태 객체에 맡기는 '책임의 전도'가 일어나는 셈입니다.
3. 상태를 교체 가능한 구성 요소로 다루기
스테이트 패턴은 다음 세 가지 구성 요소로 이루어집니다.
-
Context (문맥): 상태를 보유하는 주체입니다. 예를 들어 스마트폰이나 게임 캐릭터가 이에 해당합니다. Context는 내부에 현재 상태를 가리키는 포인터를 하나 유지하며, 외부 이벤트가 발생하면 직접 처리하지 않고 현재 상태 객체로 위임합니다.
-
State Interface (상태 인터페이스): 모든 상태 객체가 반드시 구현해야 하는 공통 동작의 계약입니다. 추상 클래스로 정의되며, 버튼 입력이나 행동 요청과 같은 공통 인터페이스를 제공합니다.
-
Concrete State (구체적 상태): State 인터페이스를 실제로 구현한 구체적인 클래스들입니다. 각 상태에서의 동작 차이는 이 클래스들에 캡슐화됩니다. 또한 상태 전환 로직 역시 이곳에 위치할 수 있어, 스스로 다음 상태로 전환하도록 Context에 요청하기도 합니다.
4. C++로 구현하는 '인격 교체'의 마법
스테이트 패턴은 상태를 교체 가능한 구성 요소로 다룹니다. C++에서 이를 구현할 때는 Context(스마트폰)와 State 간의 상호 참조를 위해 전방 선언을 활용하는 것이 핵심입니다.
#include <iostream>
#include <string>
using namespace std;
// 1. 전방 선언 (Forward Declaration)
class Smartphone;
// 2. State 인터페이스: 모든 상태 객체가 반드시 구현해야 하는 공통 동작
class State {
public:
virtual ~State() {}
virtual void onPowerButton(Smartphone* phone) = 0;
};
// 3. Context (스마트폰): 상태를 보유하는 주체
class Smartphone {
private:
State* state; // 현재 상태 객체를 가리키는 포인터
public:
Smartphone(State* initialState) : state(initialState) {}
// 상태를 교체하는 메서드
void setState(State* newState) {
if (state != nullptr) {
delete state; // 기존 상태 삭제 (메모리 누수 방지)
}
state = newState;
}
// 행동 위임: 버튼이 눌리면 현재 상태에게 처리를 맡긴다.
void pressButton() {
state->onPowerButton(this);
}
~Smartphone() { delete state; }
};
// 4. Concrete State (구체적인 상태들) 구현
class OnState : public State {
public:
void onPowerButton(Smartphone* phone) override {
cout << "전원 끄는 중... 안녕히 계세요." << endl;
// On -> Off 상태 전환
phone->setState(new class OffState());
}
};
class OffState : public State {
public:
void onPowerButton(Smartphone* phone) override {
cout << "전원 켜는 중... 환영합니다!" << endl;
// Off -> On 상태 전환
phone->setState(new OnState());
}
};
// --- 메인 함수 실행 ---
int main() {
// 처음에 '꺼진 상태'로 시작
Smartphone* myPhone = new Smartphone(new OffState());
myPhone->pressButton(); // 1. 첫 번째 클릭 (꺼짐 -> 켜짐)
myPhone->pressButton(); // 2. 두 번째 클릭 (켜짐 -> 꺼짐)
myPhone->pressButton(); // 3. 세 번째 클릭 (꺼짐 -> 켜짐)
delete myPhone;
return 0;
}
Smartphone::pressButton 메서드에는 조건문이 전혀 없다. 오직 현재 상태 객체에 처리를 위임하는 한 줄의 코드만 존재한다. 상태가 2개이든 100개이든 Context의 코드는 변하지 않는다.

5. 설계 관점에서의 이점: 개방-폐쇄 원칙(OCP)
여러분이 이 코드에서 주목해야 할 지점은 Smartphone::pressButton 메서드입니다. 조건문이 단 한 줄도 없죠? 상태가 2개든 100개든 스마트폰 본체의 코드는 수정될 필요가 없습니다.
새로운 상태를 추가해야 하는 상황을 가정해 보죠. 예를 들어 절전 모드를 도입하고 싶다면 SleepState 클래스를 하나 추가하고, 기존 상태에서의 전이 규칙만 정의하면 됩니다. 이는 객체지향 설계 원칙 중 하나인 **개방-폐쇄 원칙(Open-Closed Principle)**을 자연스럽게 만족합니다. 기능 확장은 가능하지만, 기존 코드는 변경하지 않는 것이죠.
결국 중요한 건 '질서 있는 자유'
스테이트 패턴은 상태가 늘어날수록 가치가 분명해지는 설계 방식입니다. 물론 클래스 개수가 늘어나는 오버헤드는 우리가 감내해야 할 비용입니다. 하지만 복잡한 조건 분기 대신, 명확한 책임 분리와 확장 가능한 구조를 제공한다는 점에서 대규모 시스템일수록 그 효과는 극대화됩니다.
지금 작성 중인 코드에 끝없는 else if가 도사리고 있지는 않나요? 그렇다면 지금이 바로 스테이트 패턴이라는 '질서'를 도입할 때입니다.