본문으로 건너뛰기

상태 설계

상태 설계는 업무 객체가 현재 어떤 단계에 있고, 다음에 어떤 행동을 허용할지 정하는 기준이다. 유통 시스템에서는 주문, 출고, 피킹, 검수, 배송, 반품, 정산, 외부 연동이 모두 상태를 가진다. 상태가 모호하면 화면 버튼, API 검증, 재고 반영, 취소 처리, 장애 재처리가 모두 흔들린다.

주의사항

상태값은 화면 표시용 문구가 아니라 업무 규칙이다. 화면에서 버튼을 숨겼더라도 백엔드 API에서 같은 상태 전이 검증을 반드시 수행해야 한다.

개념 정의

좋은 상태 설계는 다음 세 가지를 분리한다.

구분설명예시
업무 상태주문이나 출고가 실제 업무상 어디까지 진행됐는지ORDER_CONFIRMED, PICKING, SHIPPED
작업 상태작업자가 수행하는 단위 작업의 진행 상태ASSIGNED, IN_PROGRESS, DONE
연동 상태외부 시스템에 데이터가 전송됐는지PENDING, SENT, FAILED

하나의 status에 모든 의미를 넣으면 "출고는 확정됐는데 ERP 전송은 실패" 같은 상황을 표현하기 어렵다. 그래서 업무 상태와 연동 상태는 보통 분리한다.

실제 업무 흐름

주문부터 정산까지의 상태는 하나의 긴 줄처럼 보이지만 실제로는 여러 도메인이 맞물려 움직인다.

이 흐름에서 주문 상태, 출고 상태, 배송 상태, 정산 상태를 모두 하나로 합치면 부분 출고, 부분 배송, 반품 후 재정산을 다루기 어렵다. 도메인별 상태를 따로 두고, 상위 화면에서는 이를 조합해 보여주는 방식이 실무에서 더 안전하다.

화면/기능 관점

상태는 화면에서 가능한 기능을 결정한다.

화면상태 기준노출 기능
주문 상세주문 상태취소, 부분 취소, 출고 요청
출고 상세출고 상태피킹 지시, 검수 완료, 출고 확정, 출고 취소
배송 상세배송 상태송장 출력, 배송 추적, 배송 완료 확인
반품 상세반품 상태회수 접수, 검수 완료, 환불 요청
정산 상세정산 상태금액 확정, 세금계산서 발행, 마감

프론트엔드는 상태에 따라 버튼을 숨기거나 비활성화할 수 있지만, 최종 판단은 백엔드가 해야 한다. 여러 사용자가 동시에 같은 주문을 처리할 수 있고, 외부몰이나 택배사 콜백이 화면보다 먼저 들어올 수 있기 때문이다.

백엔드 API 관점

상태 변경 API는 단순히 PATCH /status처럼 만들기보다 업무 행위 중심으로 설계하는 편이 좋다.

API의미허용 전이
POST /orders/{id}/confirm주문 확정RECEIVED -> CONFIRMED
POST /orders/{id}/cancel주문 취소RECEIVED, CONFIRMED -> CANCELED
POST /outbounds/{id}/start-picking피킹 시작ALLOCATED -> PICKING
POST /outbounds/{id}/confirm출고 확정PACKED -> CONFIRMED
POST /returns/{id}/inspect반품 검수ARRIVED -> INSPECTED

업무 행위 API는 로그를 읽기 쉽고 권한을 나누기 쉽다. 또한 API 내부에서 수량, 재고, 마감, 중복 요청, 멱등성 키를 함께 검증할 수 있다.

const transitions = {
RECEIVED: ['CONFIRMED', 'CANCELED'],
CONFIRMED: ['OUTBOUND_REQUESTED', 'CANCELED'],
OUTBOUND_REQUESTED: ['PICKING', 'CANCELED'],
PICKING: ['PICKED'],
PICKED: ['INSPECTED'],
INSPECTED: ['PACKED'],
PACKED: ['CONFIRMED'],
CONFIRMED_OUTBOUND: ['DELIVERY_REQUESTED'],
} as const;

데이터베이스 테이블 관점

현재 상태와 상태 이력은 분리한다.

테이블역할
orders주문 현재 상태와 주문 요약
outbound_orders출고 현재 상태와 창고 작업 요약
deliveries배송 현재 상태와 송장 정보
returns반품 현재 상태와 판정 결과
status_history상태 변경 이력
integration_messages외부 연동 처리 상태

상태 이력 테이블 예시는 다음과 같다.

컬럼설명
id상태 이력 ID
entity_typeORDER, OUTBOUND, DELIVERY, RETURN
entity_id대상 업무 객체 ID
from_status변경 전 상태
to_status변경 후 상태
action상태를 바꾼 업무 행위
reason_code취소, 보류, 실패 사유
changed_by사용자 또는 시스템
changed_at변경 시각

상태값 예시

도메인상태 예시
주문RECEIVED, CONFIRMED, PARTIAL_CANCELED, CANCELED
출고REQUESTED, ALLOCATED, PICKING, PACKED, CONFIRMED, CANCELED
배송READY, INVOICED, SHIPPED, DELIVERING, DELIVERED, FAILED
반품REQUESTED, PICKUP_REQUESTED, ARRIVED, INSPECTED, REFUNDED, CLOSED
정산WAITING, CONFIRMED, CLOSED, ADJUSTED
연동PENDING, PROCESSING, SUCCESS, FAILED, RETRY_WAITING

예외 상황

예외설계 방향
같은 API가 두 번 호출됨멱등성 키나 unique key로 같은 결과를 반환
배송 완료 후 주문 취소 요청취소가 아니라 반품 프로세스로 전환
ERP 전송 실패업무 상태는 유지하고 연동 상태만 실패 처리
부분 출고주문 상태와 출고 상태를 별도로 관리
마감 후 상태 변경직접 수정 대신 보정 전표나 조정 이벤트 생성

실무에서 자주 생기는 문제

  • DONE, COMPLETE, CONFIRMED가 섞여 의미가 불분명해진다.
  • statusisCanceled, isConfirmed 같은 플래그가 서로 다른 값을 가진다.
  • 화면에서는 버튼이 막혀 있지만 API는 직접 호출하면 상태가 바뀐다.
  • 출고 확정 후 ERP 연동 실패를 출고 실패로 되돌려 재고가 꼬인다.
  • 상태 이력이 없어 누가 언제 어떤 사유로 취소했는지 추적하지 못한다.

설계 시 주의사항

상태는 업무 행위와 함께 설계해야 한다. "어떤 상태가 있는가"보다 "어떤 상태에서 어떤 행위를 허용하는가"가 더 중요하다. 상태 전이표, 권한, 재고 영향, 정산 영향, 외부 연동 영향을 함께 문서화하면 개발자와 운영자가 같은 기준으로 판단할 수 있다.

상태명을 만들 때는 화면 문구보다 업무 의미를 우선한다. STEP_1, STEP_2보다 PICKING, INSPECTED, CONFIRMED가 운영 추적에 유리하다.

간단한 예시 테이블

entity_typeentity_idfrom_statusactionto_statuschanged_by
ORDERORD-1001RECEIVEDCONFIRMCONFIRMEDsystem
OUTBOUNDOUT-2001ALLOCATEDSTART_PICKINGPICKINGworker-17
OUTBOUNDOUT-2001PACKEDCONFIRMCONFIRMEDmanager-01
DELIVERYDLV-3001SHIPPEDCOMPLETE_DELIVERYDELIVEREDcourier-api

관련 문서 링크