상태 전이로 복잡한 시스템 규칙을 제어하는 방법

상태 전이의 전이 함수, 가드 조건, 멱등성, 보상 트랜잭션과 관측성 설계를 통해 분산 시스템의 규칙과 일관성을 관리하는 방법

2026-08-14 · 최초 발행 2025-12-22

이벤트가 들어온 뒤 시스템이 갈 수 있는 상태를 제한한다

상태 전이는 시스템이 특정 상태에서 이벤트를 수신한 뒤, 가드 조건을 평가하고 액션을 실행해 다음 상태로 이동하는 과정이다. 유한 상태 기계(Finite State Machine)와 이벤트 기반 설계는 입력부터 처리, 출력까지의 흐름을 형식적으로 제어하는 데 쓰인다. 트랜잭션, 락, 보상 로직처럼 복잡해지기 쉬운 규칙도 이 경계 안에서 정리할 수 있다.

전이 함수는 δ(s, e) → s'로 표현할 수 있다. 결정적 또는 비결정적 함수로 동작하며, 부수효과(side effects)와 불변조건(invariants)을 함께 고려한다. 실제 설계에서는 허용된 전이 집합, 가드 조건, 멱등성 보장, 실패 시 롤백 또는 보상 트랜잭션을 전이 규칙으로 둔다.

상태·이벤트·부수효과를 분리해 모델링한다

상태 공간을 먼저 정의하고 종료 상태와 에러 상태를 구분한다. 전이가 가능한 그래프와 금지된 전이를 명시하면 시스템이 유효하지 않은 경로로 진행하는 일을 막을 수 있다.

이벤트에는 타입과 페이로드 스키마를 두고, 필요하면 버전을 관리한다. 가드 조건은 선행조건, 리소스 가용성, 권한을 검증하는 경계다. 상태 변경과 외부효과도 분리해야 한다. 알림 발송, DB 쓰기, 외부 API 호출 같은 작업은 실패할 수 있으므로 재시도, 멱등 키, 백오프 정책을 함께 설계한다.

동시성은 트랜잭션 경계와 락 또는 낙관적 락, 처리 순서 보장을 통해 제어한다. 분산 환경에서는 SAGA와 Outbox 패턴으로 최종 일관성을 맞춘다. 전이 로그, 이벤트 소싱, 상태 스냅샷을 남기고 전이 지연·실패율 메트릭 및 트레이싱을 연결하면 운영 중에도 상태 변화를 추적할 수 있다.

주문 이벤트가 전이와 실패 처리를 통과하는 흐름

유효성 검증 수행가드 통과가드 실패전이 성공부수효과 실패(외부 API 오류)보상 완료보상 실패(임계)'입력' 주문 생성 이벤트(e) 수신'처리' 가드 조건 평가(재고,한도, 권한)'처리' 전이 함수 δ(s,e) 적용'에러 처리' 전이 거부 로그알림'출력' 신규 상태 s' 저장이벤트 로그 기록'에러 처리' 재시도/멱등키/보상 트랜잭션'에러 처리' 운영 개입 필요 상태

도메인별로 달라지는 전이 규칙

결제와 주문에서는 주문이 Created → Paid → Fulfilled → Completed/Cancelled로 이동하며 재고와 결제 승인 가드를 적용한다. 결제는 Authorized → Captured → Refunded의 흐름에서 멱등 키와 보상 트랜잭션으로 중복 및 부분 실패를 제어한다.

마이크로서비스 SAGA에서는 로컬 트랜잭션과 보상 단계를 결합해 최종 일관성을 확보한다. Outbox와 메시지 브로커는 at-least-once 처리에서 순서를 제어하는 수단이 된다.

제조·IoT 워크셀은 Idle → Running → Paused → Completed/Error 상태에 온도와 전압 같은 안전 가드를 둔다. 오류가 발생한 뒤에는 Error → SafeStop → Ready로 복구하며 운전자 승인 이벤트가 필요하다.

접근제어에서는 계정이 Pending → Active → Suspended/Locked로 변하고, 정책 위반 가드와 감사 로그가 필수다. 리스크 기반 인증은 이벤트 점수화 결과에 따라 전이를 제한한다.

상태 전이 설계를 적용하면 전이 실패율은 3060% 감소하고, 멱등성을 적용한 기준에서 중복 처리는 90% 이상 차단할 수 있다. 장애 복구 시간(MTTR)은 2040% 단축되며, 승인 플로우 자동화는 처리량을 2~5배 확대한다. 전이 로그와 감사 추적은 감사 소요 시간을 50% 이상 절감한다.

일관성 요구와 운영 비용 사이의 선택

명세 우선 설계에서는 상태와 이벤트를 교차한 전이표를 만들고 가드, 액션, 에러 처리를 분리한다. 초기 모델링 비용이 늘어나는 대신 규칙의 경계가 명확해진다.

고유 키, 지수 백오프, 데드레터 큐를 사용하면 재시도 안전성을 높일 수 있지만 지연이 늘어날 수 있다. 이벤트 소싱은 감사와 상태 재구성에 유리한 반면 저장·재생 비용과 도메인 복잡도를 높인다. 강한 일관성은 지연과 락 경합 비용을 수반하고, 최종 일관성은 보상 로직을 복잡하게 만든다. 전이 메트릭과 추적은 필수지만 로그 볼륨 증가도 관리해야 한다.

접근 방식 성능 확장성 일관성 안정성 운영 편의
유한 상태 기계(FSM, 내장 코드) 낮은 오버헤드, 빠른 실행 서비스 인스턴스 수평 확장 용이 강한 일관성(단일 노드 기준) 단순 오류 처리 코드 변경·배포 필요
워크플로 엔진(BPMN 등) 엔진 오버헤드 존재 분산 실행·스케줄 확장 구성 기반 보장 시각화·리트라이 내장 운영 콘솔·버저닝 용이
이벤트 소싱 + SAGA 로그 기반 고성능 append 브로커 확장으로 우수 최종 일관성 보상 패턴으로 회복력 모델링·운영 복잡도 높음

파이썬으로 전이 규칙을 실행하는 예

전제조건은 Python 3.11+이며 외부 라이브러리는 필요하지 않다. 단일 프로세스에서 낙관적 락 버전 관리를 가정한다.

from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Callable, Dict, Tuple

class State(Enum):
    CREATED = auto()
    PAID = auto()
    FULFILLED = auto()
    CANCELLED = auto()

class Event(Enum):
    PAY = auto()
    FULFILL = auto()
    CANCEL = auto()

@dataclass
class Order:
    id: str
    state: State = State.CREATED
    version: int = 0
    meta: dict = field(default_factory=dict)

Guard = Callable[[Order, dict], bool]
Action = Callable[[Order, dict], None]
Transition = Tuple[Guard, Action, State]

def always_true(_: Order, __: dict) -> bool:
    return True

def ensure_not_cancelled(o: Order, _: dict) -> bool:
    return o.state != State.CANCELLED

def charge_payment(o: Order, ctx: dict) -> None:
    # 멱등 키 사용 가정: ctx['idem_key']
    if ctx.get("simulate_fail"):
        raise RuntimeError("charge failed")
    o.meta["paid_amount"] = ctx.get("amount", 0)

def ship_goods(o: Order, _: dict) -> None:
    o.meta["tracking"] = "TRACK-123"

TRANSITIONS: Dict[Tuple[State, Event], Transition] = {
    (State.CREATED, Event.PAY): (ensure_not_cancelled, charge_payment, State.PAID),
    (State.PAID, Event.FULFILL): (always_true, ship_goods, State.FULFILLED),
    (State.CREATED, Event.CANCEL): (always_true, lambda o, c: None, State.CANCELLED),
    (State.PAID, Event.CANCEL): (always_true, lambda o, c: None, State.CANCELLED),
}

class VersionConflict(Exception): ...

def apply_event(o: Order, expected_version: int, event: Event, ctx: dict) -> Order:
    if o.version != expected_version:
        raise VersionConflict(f"expected {expected_version}, got {o.version}")
    key = (o.state, event)
    if key not in TRANSITIONS:
        raise ValueError(f"transition not allowed: {o.state} -> {event}")
    guard, action, next_state = TRANSITIONS[key]
    if not guard(o, ctx):
        raise PermissionError("guard condition failed")
    # 상태 저장 이전에 부수효과 실행(멱등성 필수)
    action(o, ctx)
    # 전이 성공 시 상태/버전 갱신
    o.state = next_state
    o.version += 1
    return o

if __name__ == "__main__":
    order = Order(id="o-1")
    order = apply_event(order, expected_version=0, event=Event.PAY, ctx={"amount": 100, "idem_key": "k1"})
    order = apply_event(order, expected_version=1, event=Event.FULFILL, ctx={})
    print(order.state, order.version, order.meta)

외부 호출에는 멱등 키를 사용해 재시도 안전성을 확보한다. VersionConflict는 재시도 대상으로, 가드 실패는 비즈니스 예외로 분류한다. Outbox 패턴을 함께 쓸 때는 상태 저장과 이벤트 발행을 하나의 로컬 트랜잭션으로 처리한다.

상태 전이유한 상태 기계이벤트 기반 설계트랜잭션워크플로 자동화