상태 다이어그램으로 이벤트 중심 시스템의 전이 설계하기

상태 다이어그램의 상태·전이·가드·액션 구조와 계층형 상태 기계의 설계, 테스트, 운영 원칙을 정리합니다.

2026-08-14 · 최초 발행 2025-10-31

이벤트가 복잡해질수록 상태와 전이를 분리해야 한다

소프트웨어와 임베디드 시스템의 이벤트 중심 동작은 상태 다이어그램으로 모델링할 수 있다. 상태, 전이, 이벤트, 가드, 액션을 분리하면 복잡한 규칙을 일관된 실행 흐름으로 다룰 수 있으며, UML State Machine 모델은 설계·구현·테스트 사이의 추적성을 연결하는 기준점이 된다.

상태 다이어그램은 시스템이 가질 수 있는 상태 집합과 그 사이의 이동을 이벤트와 조건으로 기술하는 행위 모델링 도구다. 기본 단위는 상태(State), 전이(Transition), 이벤트(Event), 가드(Guard), 액션(Action), 그리고 진입·수행·종료 동작(Entry/Do/Exit)이다.

UML 2.x State Machine 명세는 복합 상태(Composite), 서브머신(Submachine), 히스토리 상태(History), 병렬 영역(Orthogonal Regions)도 표현한다. 이 구조는 실시간 시스템, 임베디드 장치, 업무 프로세스처럼 실행 경로가 명확해야 하는 영역에서 널리 쓰인다.

상태의 수명주기와 전이 조건을 분명히 둔다

상태는 객체나 시스템이 처한 조건의 집합이다. 진입·수행·종료 동작을 나누면 진입 시점, 상태 유지 중, 이탈 시점의 로직을 분리할 수 있다. 초기 상태(Pseudostate)는 시작점을 나타내고, 종료 상태(Final)는 시나리오가 끝났음을 표시한다.

전이는 이벤트를 수신했을 때 일어나는 상태 간 이동이다. 동기·비동기 이벤트 모두 모델에 담을 수 있으며, 시간 이벤트인 타임아웃, 메시지 기반의 신호 이벤트, 메서드 호출 이벤트도 전이의 계기가 된다.

가드는 전이를 실행할지를 결정하는 부울 조건이다. 데이터 컨텍스트에 의존하되 부작용 없는 순수 함수로 두는 편이 적합하다. 같은 이벤트에 여러 전이가 연결될 때는 가드의 우선순위가 비결정성을 제거한다.

액션은 전이가 발생하는 순간 수행하는 부수 효과다. 상태의 엔트리·도·익시트 동작으로 업무 로직을 응집할 수 있다. 특히 재시도와 재처리가 있는 시스템에서는 액션의 멱등성이 안전성을 좌우한다.

복합 상태와 히스토리 상태는 상태 폭증(State explosion)을 줄이고, 재진입 시점의 상태 복원을 통해 UX와 업무 흐름의 연속성을 지원한다. 병렬 영역은 독립 실행 컨텍스트를 표현하며, Join/Fork 동기화 바로 동시성을 제어한다.

주문 처리 흐름에서 보는 상태 전이

전자상거래 주문 처리는 입력 이벤트, 가드 조건, 전이 액션을 함께 표현하기 좋은 예다.

checkout()paymentSuccess /assignOrderId()paymentFailretry [retryCount < 3]timeout [elapsed > 30m]createShipmentshipconfirmDeliverystockOutCartPendingPaymentPaidPaymentFailedCancelledFulfillmentShippedDelivered

이 흐름의 입력은 사용자 체크아웃, 결제 성공·실패, 재시도, 타임아웃, 재고 부족이다. 처리 단계에서는 retryCount, elapsed 가드를 평가하고 assignOrderId, createShipment 같은 전이 액션을 수행한다. 최종적으로 Delivered 또는 Cancelled 상태에 도달하며, 감사 로깅과 알림 발행을 연결할 수 있다.

유효하지 않은 전이 요청은 거부하고 로깅해야 하며, 타임아웃은 시간 이벤트로 모델링한다. 재시도 상한 가드는 무한 루프를 막고, 재고 부족 경로에는 보상 트랜잭션을 연결할 수 있다.

상태 변경과 결제 캡처 같은 외부 사이드이펙트는 전이 단위 트랜잭션 안에서 원자성을 확보해야 한다. 동시 업데이트 경쟁에는 상태와 버전을 함께 사용하는 낙관적 락 적용을 고려할 수 있다.

모델을 구현과 테스트로 연결하는 과정

먼저 외부 입력, 시간 이벤트, 내부 신호를 구분하고 이벤트 페이로드 스키마를 정한다. 이어 정상·대체·오류 경로를 나눠 상태 집합과 초기·종단·복합 상태를 배치한다.

동일 이벤트가 여러 전이 후보를 가질 경우에는 결정 트리를 세우고 가드의 순수성을 유지한다. 외부 I/O와 내부 계산은 분리하며, 액션은 재시도 안전성을 검증할 수 있도록 멱등하게 설계한다. 보상 트랜잭션과 롤백 정책, SLA 기반 타임아웃 값도 이 단계에서 명시한다.

테스트는 상태×이벤트 경계값 조합을 중심으로 구성하고 상태·전이·경로 커버리지 기준을 둔다. 운영 단계에서는 상태 변경 로그, 이벤트 추적 ID, 전이 실패율과 평균 체류 시간 메트릭을 수집한다.

상태 다이어그램은 임베디드·펌웨어의 모드 전환, 오류 복구, 전원 관리 자동화에 적용할 수 있다. 백엔드와 마이크로서비스에서는 주문·결제·송장 워크플로와 사가 보상 트랜잭션을 모델링하는 방식으로 쓰인다. 모바일·프론트엔드의 화면 흐름과 오프라인 동기화 상태, 네트워크 프로토콜의 연결·해제·재전송·타임아웃 제어, 다단계 검토와 승인 경로에도 적합하다.

상태 기계 유형에 따른 선택 기준

FSM 유형 성능(지연/오버헤드) 확장성(상태 폭증 관리) 일관성(결정성) 안정성(오류 격리) 운영 편의(가시성)
Moore FSM 전이 시 계산 단순, 지연 낮음 상태 수 증가 경향 높음, 출력이 상태에만 의존 높음, 부작용 예측 용이 높음, 디버깅 용이
Mealy FSM 전이 수 감소 가능, 지연 변동 비교적 유연 중간, 입력에 민감 중간, 글리치 주의 중간, 관측 포인트 추가 필요
Harel/HFSM(계층) 계층 해석 오버헤드 소폭 매우 높음, 상태 재사용 높음, 지역화된 결정 높음, 상위 상태로 오류 흡수 높음, 추적·히스토리 활용

전이 테이블로 구현한 Python 예제

전제조건은 Python 3.10+이며 외부 패키지는 필요 없다. 다음 코드는 테이블 기반 전이, 가드, 액션을 구현한 예시다.

from enum import Enum, auto
from dataclasses import dataclass

class State(Enum):
    CART = auto()
    PENDING = auto()
    PAID = auto()
    FAILED = auto()
    FULFILL = auto()
    SHIPPED = auto()
    DELIVERED = auto()
    CANCELLED = auto()

@dataclass
class Context:
    retry_count: int = 0
    elapsed_min: int = 0
    order_id: str | None = None

class FSM:
    def __init__(self, initial: State, ctx: Context):
        self.state = initial
        self.ctx = ctx

    def _guard_retry(self) -> bool:
        return self.ctx.retry_count < 3

    def _guard_timeout(self) -> bool:
        return self.ctx.elapsed_min > 30

    def _assign_order_id(self):
        self.ctx.order_id = "ORD-001"

    def _inc_retry(self):
        self.ctx.retry_count += 1

    def _noop(self): ...

    # 전이 테이블: (현재상태, 이벤트) -> (다음상태, 가드, 액션)
    transitions = {
        (State.CART, "checkout"):      (State.PENDING, None, _noop),
        (State.PENDING, "payment_ok"): (State.PAID, None, _assign_order_id),
        (State.PENDING, "payment_ng"): (State.FAILED, None, _noop),
        (State.FAILED, "retry"):       (State.PENDING, _guard_retry, _inc_retry),
        (State.PENDING, "timeout"):    (State.CANCELLED, _guard_timeout, _noop),
        (State.PAID, "create_ship"):   (State.FULFILL, None, _noop),
        (State.FULFILL, "ship"):       (State.SHIPPED, None, _noop),
        (State.SHIPPED, "deliver"):    (State.DELIVERED, None, _noop),
        (State.FULFILL, "stock_out"):  (State.CANCELLED, None, _noop),
    }

    def send(self, event: str):
        key = (self.state, event)
        spec = self.transitions.get(key)
        if not spec:
            raise ValueError(f"invalid transition: {self.state=} {event=}")
        next_state, guard, action = spec
        if guard and not guard(self):
            raise ValueError(f"guard blocked: {self.state=} {event=}")
        # 전이
        action(self)
        self.state = next_state
        return self.state

if __name__ == "__main__":
    fsm = FSM(State.CART, Context())
    fsm.send("checkout")
    fsm.send("payment_ok")
    fsm.send("create_ship")
    fsm.send("ship")
    fsm.send("deliver")
    print(f"final={fsm.state}, order_id={fsm.ctx.order_id}")

실행 결과 예시는 final=State.DELIVERED, order_id=ORD-001이다. 유효하지 않은 전이나 가드 실패는 예외로 처리하며, 실제 운영 환경에서는 로그와 메트릭 연계가 필요하다.

설계 품질과 운영 효과를 함께 관리한다

상태·전이 관련 버그는 20~40% 감소하고 재현성은 높아질 수 있다. 전이 커버리지 기반 테스트는 케이스 중복을 25% 이상 줄이며, 정책 변경 시 전이 테이블만 교체해 영향 범위를 국지화할 수 있다. 상태 체류 시간과 전이 실패율을 기반으로 SLA 준수 모니터링도 정밀해진다. 신규 인력의 설계 이해 시간은 30% 단축될 수 있다.

전이 정의는 단일 소스로 관리해 변경이 여러 위치로 퍼지는 것을 막는다. 코드 생성기나 모델 동기화 자동화도 이 원칙과 맞닿아 있다.

가드와 액션은 역할을 분리한다. 가드는 부작용 없이 판단하고, 액션은 멱등성을 갖춰 재시도와 롤백에 안전해야 한다. 계층과 히스토리는 상태 폭증을 줄일 수 있지만, 단계를 과도하게 중첩하면 추적성이 떨어진다.

병렬 영역은 독립 관심사를 분리하는 수단이지만 조인 지점의 경쟁 상태와 교착 위험을 함께 관리해야 한다. 상태 변경 이벤트, 감사 로그, 트레이스 ID를 설계에 내장하면 사후 분석과 리플레이도 쉬워진다.

상태 다이어그램UML이벤트 중심 설계상태 기계소프트웨어 설계