272. 스테이트 (State) - 상태에 따라 객체 행위 변경

핵심 인사이트 (3줄 요약)

  1. 본질: 스테이트(State) 패턴은 객체 내부의 상태(State)가 바뀜에 따라 객체가 마치 다른 클래스로 변신한 것처럼 행동(Behavior)을 완벽하게 동적으로 바꿀 수 있게 해주는 행동 패턴이다.
  2. 가치: 복잡한 상태 전이를 제어하기 위해 얽히고설킨 거대한 if-else 또는 switch 문(스파게티 코드)을 각 상태별 독립된 클래스로 캡슐화하여, OCP(개방-폐쇄 원칙)를 준수하며 새로운 상태를 쉽게 추가할 수 있게 한다.
  3. 융합: 유한 상태 기계(FSM, Finite State Machine)를 객체 지향적으로 가장 우아하게 구현하는 표준 메커니즘이며, 게임 캐릭터의 행동 상태, 네트워크 소켓(TCP)의 연결 상태, 결제 및 승인 프로세스의 상태 다이어그램과 직결된다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 스테이트 패턴은 객체의 특정 상태를 별도의 클래스(State 인터페이스 구현체)로 정의하고, 문맥 객체(Context)가 현재 상태 객체에게 행동을 위임(Delegate)하여 내부 상태에 따라 다른 결과를 내도록 설계하는 기법이다.

  • 필요성: 커피 자판기를 만든다고 치자. 자판기에는 "동전 없음", "동전 있음", "알맹이 매진"이라는 3가지 상태가 있다. 손님이 '동전 투입'이나 '버튼 누름'을 할 때마다 현재 상태를 확인하는 if (state == HAS_COIN) ... else if (state == NO_COIN) ... 구문이 메서드마다 반복된다. 나중에 "1+1 이벤트 상태"를 추가하려면 모든 메서드를 찾아 if문을 추가하다가 버그가 폭발한다. 상태(State)와 행위(Behavior)가 분리되지 않아 벌어지는 참사다.

  • 💡 비유: 한 명의 직원이 평소(상태 A)에는 아주 친절하지만, 극심한 스트레스(상태 B)를 받으면 헐크처럼 화를 내고, 휴가 중(상태 C)이면 아무 대답도 하지 않는 것과 같습니다. 사람은 한 명(Context)인데 마음 상태(State)에 따라 완전히 다른 사람처럼 행동합니다.

  • 등장 배경 및 발전 과정:

    1. 절차적 상태 분기 (Spaghetti if-else): 초창기 FSM(Finite State Machine)은 거대한 switch-case 문 하나에 의존하여 코드가 길어지고 유지보수가 불가능에 가까웠다.
    2. 상태 객체화 (State Object): 디자인 패턴이 도입되면서, 각 분기문의 로직을 독립적인 State 클래스로 찢어내어 다형성(Polymorphism)으로 해결했다.
    3. 비즈니스 워크플로우와의 융합: 결제, 배송(상품 준비 중 → 배송 중 → 배송 완료), 주문 승인 프로세스 등 엔터프라이즈의 라이프사이클을 매니징하는 기본 설계로 정착했다.
  • 📢 섹션 요약 비유: 로봇 장난감 안에 '순한 모드 칩'과 '전투 모드 칩'을 번갈아 끼우는 것과 같습니다. 로봇(Context)은 그저 버튼을 누를 뿐이지만, 어떤 칩(State)이 꽂혀 있느냐에 따라 춤을 추거나 미사일을 쏩니다.


Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

구성 요소 (클래스 다이어그램)

요소명역할비유
Context (컨텍스트/문맥)클라이언트와 상호작용하는 주인공. 내부에 현재 상태를 나타내는 State 인터페이스의 참조 변수를 가지고 있으며, 요청을 이 변수에 위임한다.커피 자판기 껍데기
State (상태 인터페이스)자판기의 모든 상태들이 공통으로 가져야 할 행위(동전 넣기, 버튼 누르기 등)를 정의하는 인터페이스.자판기의 행동 매뉴얼
ConcreteStateState 인터페이스를 구현하여, 특정 상태일 때 각 행위가 어떻게 구체적으로 동작해야 하는지, 그리고 언제 다른 상태로 전이(Transition)해야 하는지를 정의한다."동전 있음 상태", "매진 상태"

동작 메커니즘 (코드 뼈대 구조)

스테이트 패턴의 묘미는 Context가 상태 전환의 책임을 지는 것이 아니라, ConcreteState가 스스로 다음 상태로 전이(context.setState())시킬 책임을 진다는 점이다.

  ┌─────────────────────────────────────────────────────────────┐
  │                 스테이트 패턴의 상태 전이 메커니즘                │
  ├─────────────────────────────────────────────────────────────┤
  │                                                             │
  │  [Context (VendingMachine)]                                 │
  │  - state : State                                            │
  │  + insertCoin() { state.insertCoin(); }  ◀── 위임(Delegate) │
  │  + pushButton() { state.pushBtn(); }                        │
  │  + setState(State s) { this.state = s; }                    │
  │                                                             │
  │          △ (위임)                                            │
  │          │                                                  │
  │  [State (인터페이스)]                                         │
  │  + insertCoin()                                             │
  │  + pushBtn()                                                │
  │                                                             │
  │          △ (구현)                                            │
  │     ┌────┴────┐───────────────────────┐                     │
  │     │         │                       │                     │
  │ [NoCoinState] │                 [HasCoinState]              │
  │ insertCoin(){ │ ──(전이)──▶       insertCoin(){             │
  │   // 동전받음   │ context.setState  // 거절 (이미 있음)        │
  │   // 전이호출   │ (HasCoinState)  }                         │
  │ }             │                 pushBtn(){                │
  │ pushBtn(){    │ ◀──(전이)──       // 커피배출               │
  │   // 경고음    │ context.setState  // 전이호출               │
  │ }             │ (NoCoinState)   }                         │
  └─────────────────────────────────────────────────────────────┘

[다이어그램 해설] 자판기(Context)는 "동전이 투입되었다"는 사실을 현재 꽂혀있는 State 객체에게 통째로 넘긴다(delegate). 만약 지금 NoCoinState(동전 없음)가 꽂혀있다면, "동전을 받고, 상태를 '동전 있음'으로 바꿔!"라고 스스로 context.setState()를 호출하여 다음 상태로 진화한다. 자판기 코드 내부의 거대한 if-else 지옥이 완전히 소멸하고, 각 상태 클래스가 자신의 앞가림과 전이를 스스로 챙기는 완벽한 객체 지향 구조가 완성된다.


전략(Strategy) 패턴과의 결정적 차이 (Intent)

구조적(클래스 다이어그램)으로는 스테이트와 전략 패턴은 쌍둥이다. 둘 다 Context가 인터페이스를 포함(위임)한다. 하지만 그 **'의도(Intent)'**가 180도 다르다.

  • 전략 (Strategy): 클라이언트가 "A 전략으로 실행해라"라고 주입한다. 전략 객체들은 서로의 존재를 전혀 모른다.

  • 스테이트 (State): 클라이언트는 상태 변화에 관여하지 않는다. 상태 객체들이 내부적인 조건에 따라 스스로 다른 상태로 전이(Transition)한다. 서로의 존재를 잘 안다.

  • 📢 섹션 요약 비유: 전략 패턴은 운전자가 직접 에코 모드에서 스포츠 모드로 '스위치를 누르는 것'이고, 스테이트 패턴은 언덕을 오를 때 자동차 엔진이 '스스로' 저단 기어로 '상태를 바꾸는 것'입니다.


Ⅲ. 융합 비교 및 다각도 분석

1. 상태 패턴 설계 시 '전이(Transition)' 주체의 딜레마

상태를 다음 단계로 넘어가는 전이(context.setState()) 로직을 누가 가져야 하는가는 실무 아키텍처의 큰 논쟁거리다.

주체장점단점실무 권장 상황
Context가 주도상태 객체들이 서로의 존재를 모르게 되어 결합도가 완전히 떨어짐.Context 안에 또다시 거대한 분기문이나 매핑 테이블이 생겨 복잡해짐.상태가 정적이거나 전이 룰이 고정된 경우
State 객체가 스스로 주도 (기본)Context가 가벼워지고 응집도가 올라감. (객체 지향적)상태 클래스들끼리 강하게 결합(Coupling)되어 하나를 추가할 때 다른 상태 코드도 건드려야 함.상태 간의 동적인(조건부) 전이가 잦고 복잡한 유한 상태 기계(FSM)

과목 융합 관점

  • 네트워크 (NW): TCP 프로토콜의 연결 상태 다이어그램(CLOSED → SYN_SENT → ESTABLISHED → TIME_WAIT 등)을 구현할 때 가장 정석으로 쓰이는 패턴이다. 커널 개발 시 거대한 if문 대신 상태 패턴을 적용하여, 패킷 도착 이벤트를 각 State 구조체(객체)로 위임한다.

  • 운영체제 (OS): 프로세스 생명주기(New, Ready, Running, Blocked, Terminated)를 관리할 때, OS 커널은 각 프로세스 블록(PCB)의 상태 변경을 이 패턴의 철학을 차용하여 분리 통제한다.

  • 📢 섹션 요약 비유: 애벌레가 번데기를 거쳐 나비가 되는 과정에서, 자연의 법칙이(Context 주도) 강제로 바꾸는 것이 아니라, 각 껍질(State) 안의 유전자가 때가 되면 스스로 다음 형태로 깨고 나오는(State 스스로 주도) 생명 진화의 과정과 같습니다.


Ⅳ. 실무 적용 및 기술사적 판단

실무 시나리오

  1. 시나리오 — 전자상거래(E-Commerce) 주문 상태의 지옥 탈출: 쇼핑몰의 주문 테이블에 status 칼럼이 있고, 그 값이 PAYMENT_WAIT, PREPARING, SHIPPING, DELIVERED, CANCELED 5가지다. 사용자가 "주문 취소" 버튼을 눌렀을 때, 각 상태에 따라 취소 가능 여부와 로직(환불, 재고 원복, 택배사 취소 등)이 완전히 다르다. OrderService.cancel() 메서드에 300줄짜리 switch-case문이 생겼고 유지보수가 불가능해졌다.

    • 아키텍트의 해결책: OrderState 인터페이스를 만들고 각 5개의 상태를 클래스로 분리한다. OrderContext는 데이터만 들고 있고, 취소 로직은 state.cancel(this)로 위임한다. 배송 중(SHIPPING) 상태 클래스에서는 cancel() 호출 시 예외(Exception)를 던지고, 결제 대기(PAYMENT_WAIT) 상태 클래스에서는 바로 취소 처리 후 state = new CanceledState()로 변경하도록 캡슐화한다.
  2. 시나리오 — 상태 객체의 잦은 생성으로 인한 메모리(GC) 부하: 게임에서 1만 마리의 NPC가 각각 자신의 '순찰', '추적', '공격' 상태 패턴을 가지며 매 초마다 상태를 전이시킨다. 전이할 때마다 this.state = new AttackState() 처럼 new 키워드를 남발하면 초당 수만 개의 가비지가 생성되어 GC(Garbage Collection) 프리즈가 발생한다.

    • 아키텍트의 해결책: 상태 패턴과 싱글톤(Singleton) 패턴 혹은 플라이웨이트(Flyweight) 패턴을 결합해야 한다. 상태 객체 내부에 Context의 고유 변수(HP, 마나)를 저장하지 않고(Stateless 유지), 전이 시 this.state = AttackState.getInstance()처럼 미리 생성된 전역 상태 인스턴스의 참조만 넘겨주는 방식으로 설계하면 메모리 부하를 0으로 줄일 수 있다.

도입 체크리스트

  • 기술적: 만약 상태의 종류가 딱 2~3개뿐이고 절대 늘어날 일 없는 간단한 로직(예: ON/OFF 스위치)이라면? 상태 패턴 도입은 오버엔지니어링(Over Engineering)이다. 클래스 개수만 불어나므로 그냥 boolean이나 단순 분기문을 써라.
  • 설계적: 상태 전이가 너무 복잡하여 머릿속에 안 그려질 때, 코딩 전 반드시 **상태 전이 다이어그램(State Transition Diagram)**을 그려 문서화했는가? 상태 패턴은 상태 개수(N)와 전이 조건(M)에 비례해 클래스 파악이 어려워지는 치명적 단점이 있다.

안티패턴

  • 상태 객체 안의 또 다른 분기문 (State Sneaking): 상태 클래스로 분리해 놓고도 그 안에서 if (context.getAmount() < 10) ... else ... 처럼 또 다시 컨텍스트의 데이터에 의존한 복잡한 분기문을 짜는 행위. 이는 상태를 덜 쪼갰다는 증거다. '잔액 부족 동전 있음 상태' 등 새로운 상태 클래스로 더 잘게 찢어야 다형성이 살아난다.

  • 📢 섹션 요약 비유: 교통신호등(상태)을 초록, 노랑, 빨강 클래스로 나눠 놨는데, 초록 불 클래스 안에서 다시 "차가 10대 이상이면 빨리 지나가게 해라" 하고 복잡하게 분기한다면 분리의 의미가 퇴색되는 것과 같습니다.


Ⅴ. 기대효과 및 결론

정량/정성 기대효과

구분거대한 분기문(AS-IS)상태 패턴 도입(TO-BE)개선 효과
정량상태 1개 추가 시 기존 수십 개 메서드 내부 if문 일일이 수정신규 상태 클래스 1개만 생성하여 플러그인처럼 삽입OCP 준수로 인한 사이드 이펙트 발생률 0% 수렴
정량Cyclomatic Complexity (순환 복잡도) 지표 폭발적 증가각 상태 클래스별 복잡도 1~2 수준으로 저하유지보수성 및 가독성 획기적 증가
정성복잡한 상태 전이 룰이 코드 곳곳에 산재해 파악 불가상태 다이어그램이 그대로 1:1 클래스 맵핑됨비즈니스 로직과 소스 코드의 완벽한 동기화

미래 전망

  • 엔터프라이즈 상태 머신 프레임워크 (Spring StateMachine): 실무에서는 거대한 비즈니스 플로우를 제어할 때 직접 상태 패턴 클래스를 수십 개씩 짜지 않고, Spring StateMachine이나 XState(프론트엔드) 같은 FSM 전용 라이브러리를 통해 상태와 이벤트, 전이 조건(Guard)을 설정(Configuration)만으로 분리 선언하여 더욱 선언적(Declarative)으로 발전시키고 있다.
  • 데이터베이스 주도 상태 패턴: MSA 트랜잭션 관리에서는 인메모리 상태 패턴만으론 부족하다. 상태의 현재 값을 DB에 이벤트로 기록하고(Event Sourcing), 그 저장된 값에 다형성을 씌워 복원해 내는 방식으로 클라우드 안정성을 확보한다.

참고 표준

  • GoF (Gang of Four): Behavioral Patterns - State
  • UML: State Machine Diagram (상태 기계 다이어그램)
  • Spring Framework: Spring StateMachine 프로젝트

상태 패턴은 복잡함을 마법처럼 단순함으로 치환하는 연금술이다. 기술사는 아무리 거대하고 더러운 500줄짜리 분기문 로직을 만나더라도, "이 도메인에는 어떤 뚜렷한 '상태(State)'들이 존재하는가?"라는 본질적 질문을 던지고, 각 상태를 독립된 객체로 분리해내어 코드의 멱살을 잡고 있는 분기 지옥(Spaghetti code)을 부수고 객체 지향의 우아함(Polymorphism)을 복원해 내는 결단력을 발휘해야 한다.

  • 📢 섹션 요약 비유: 더러운 방 안의 물건들(조건문)을 치우기 위해, 계절별 수납상자(상태 클래스)를 만들어 봄옷, 여름옷을 각각 넣어두면 필요할 때 그 상자만 열면 되는 마법 같은 정리의 기술입니다.

📌 관련 개념 맵 (Knowledge Graph)

개념 명칭관계 및 시너지 설명
FSM (유한 상태 기계, Finite State Machine)한 번에 하나의 상태만 가질 수 있고 특정 조건에 의해 상태를 바꾸는 수학적/논리적 모델. 이를 객체 지향으로 구현한 것이 State 패턴이다.
전략 패턴 (Strategy Pattern)전략이 '어떻게 할까'를 캡슐화한다면, 상태는 '어떤 모습일까'를 캡슐화하는 구조적 형제 패턴. (전략은 고정, 상태는 스스로 변화)
싱글톤 / 플라이웨이트 패턴상태 전이 시 빈번한 new State() 호출로 인한 메모리 누수와 가비지를 막기 위해 각 상태를 단일 인스턴스 공유 체계로 최적화할 때 결합한다.
OCP (Open-Closed Principle)기존 Context 코드 수정 없이(Closed), 새로운 상태 클래스를 추가(Open)하여 무한히 확장할 수 있게 해주는 패턴의 이론적 근거.
Spring StateMachine스프링에서 제공하는 프레임워크로, 개발자가 상태 패턴 클래스들을 수작업으로 짤 필요 없이 상태와 이벤트 전이 룰만 선언하게 해준다.

👶 어린이를 위한 3줄 비유 설명

  1. 얼음(상태 1), 물(상태 2), 수증기(상태 3)는 모두 'H2O'라는 본질(Context)은 같지만, 생김새와 행동이 완전 다르죠.
  2. 얼음을 만지면 차갑고 컵에 담을 수 있지만, 열(이벤트)을 가하면 스스로 물로 변신(전이)해서 컵에서 찰랑거리는 행동으로 바뀝니다.
  3. 이렇게 하나의 사물이 내부의 '상태'가 바뀔 때마다 마치 다른 사물인 것처럼 완전히 다르게 행동하도록 프로그래밍하는 마법을 **'스테이트(상태) 패턴'**이라고 부른답니다!