- Published on
워크플로우에 "보완 요청(RVS)" 단계를 추가·철회한 의사결정 회고
- Authors

- Name
- Hyo814
워크플로우에 "보완 요청(RVS)" 단계를 넣었다가 하루 만에 뺀 이야기
심사 결정에 보완 요청(Revise, RVS) 단계를 추가하는 작업을 마치고 다음 날 전부 되돌렸습니다. 결과만 보면 "왜 했지?" 싶은 커밋 두 개가 남았지만, 그 과정에서 얻은 교훈이 있어 기록으로 남깁니다.
1. 원래 워크플로우 — 두 축의 단순한 상태 머신
기존 심사 프로세스는 단순했습니다.
신청(RECORD) → 심사(REVIEW) → 승인(APR) 또는 반려(REJ) → 종료
심사자는 승인(Approve) 과 반려(Reject) 두 가지 결정만 내릴 수 있고, 반려된 건은 신청자가 수정 후 재신청하는 구조였습니다.
# workflow/iso14817_factory.py — 기존 구조
class Action:
APR = 'approve' # 승인
REJ = 'reject' # 반려
2. 요구사항 — "반려하지 말고 보완해서 돌려달라"
현업에서 피드백이 왔습니다.
"반려되면 신청자가 처음부터 다시 작성해야 해서 불편해요. 사소한 수정만 필요한 경우, 심사자가 코멘트 달아서 돌려보내고 신청자가 수정해서 재제출하는 흐름이 있으면 좋겠어요."
설득력 있는 요청이라 세 번째 축 — 보완 요청(RVS) 을 추가하기로 했습니다.
신청(RECORD) → 심사(REVIEW) ┬→ 승인(APR) → 종료
├→ 반려(REJ) → 종료
└→ 보완 요청(RVS) → RECORD (self-loop)
3. 구현 — self-loop 전이와 backfill 명령
상태 머신에 self-loop 추가
# workflow/iso14817_factory.py
class Action:
APR = 'approve'
REJ = 'reject'
RVS = 'revise' # 신규 추가
class ISO14817Factory:
def build_transitions(self):
return [
(REVIEW, Action.APR, APPROVED),
(REVIEW, Action.REJ, REJECTED),
(REVIEW, Action.RVS, RECORD), # self-loop로 신청 단계로 복귀
]
기존 데이터 backfill 관리 명령
기존에 진행 중이던 신청 건들은 이 전이를 모르기 때문에, 상태 정의를 덮어쓰는 idempotent 커맨드를 따로 만들었습니다.
# workflow/management/commands/backfill_revise_flow.py
class Command(BaseCommand):
help = '기존 워크플로우에 RVS self-loop 전이를 주입'
def handle(self, *args, **options):
for wf in Workflow.objects.all():
if not wf.has_action(Action.RVS):
wf.add_transition(REVIEW, Action.RVS, RECORD)
신청자 화면 처리
RECORD 단계에서만 편집 가능했던 상세 뷰를, RVS로 돌아온 상태에서도 편집 가능하도록 게이트를 완화했습니다.
# 편집 게이트 (도입 시점)
if request.state == RECORD or has_active_revise(request):
allow_edit = True
그리고 보완 요청을 받은 신청자에게 알려주는 배너 매크로도 추가했습니다.
{% macro revise_received_banner(request) %}
<div class="alert alert-warning">
심사자가 보완을 요청했습니다: {{ request.latest_revise_comment }}
</div>
{% endmacro %}
4. 철회 — 하루 만에 전부 되돌린 이유
다음 날 팀 리뷰에서 세 가지 문제가 드러났습니다.
(1) 상태 머신이 급격히 복잡해졌다
RECORD ──→ REVIEW ──→ APPROVED
↑ │
│ ├──→ REJECTED
│ │
└───────────┘ (RVS self-loop)
self-loop는 그래프에서 한 줄 추가지만, "이 신청건이 지금 첫 신청이냐, 보완 후 재신청이냐" 를 구분해야 하는 분기가 도처에 생겼습니다. has_active_revise() 같은 상태 질의가 여기저기 번져나갔고, 리스트 화면·알림·권한 체크마다 "RVS 상태인 경우는 예외"가 붙기 시작했습니다.
(2) 기존 "반려 후 재신청" 흐름과 사실상 중복
반려당한 건을 신청자가 수정해서 새로 신청하는 흐름이 이미 존재했습니다. RVS는 그 흐름의 UX만 조금 개선한 것이지 새 기능이 아니었습니다. "돌려보낸다" 와 "반려한다" 의 경계가 심사자 입장에서도 애매했고, 실제 데이터로는 두 축이 거의 동일하게 쓰일 가능성이 컸습니다.
(3) 권한/취소 정책과 충돌
같은 PR에서 신청 취소 단계별 권한 분리(SELF_SERVE / APPROVAL_REQUIRED / FORBIDDEN) 작업도 같이 들어갔는데, RVS 상태에서는 이 세 단계가 어디에 속하는지 정의되지 않은 상태였습니다. "보완 요청 중인 건을 신청자가 혼자 취소할 수 있나?" 같은 질문에 팀 내 합의가 없었습니다.
5. 철회 커밋에서 지운 것들
- Action.type 에서 RVS 제거, ActionDecision 클래스 · backfill 커맨드 삭제
- ISO14817Factory · SimpleFactory 의 self-loop 전이 및 관련 파라미터 제거
- RequestActionManager.has_active_revise 및 self-loop 분기 제거
- 신청 상세 뷰의 편집 게이트를 RECORD 단계 전용으로 복원
- ActionSelectForm DEFAULT_MESSAGES · qualifyDecisionMessage.js 에서 revise 제거
- revise_received_banner 매크로 및 관련 템플릿 호출 제거
삭제 diff가 오히려 추가 diff보다 깔끔해서, 도입 전 코드로 100% 복귀했습니다.
6. 배운 것
상태 머신에 축을 추가하는 것은 "+1"이 아니라 "×2"다
승인/반려 두 축에 세 번째 축을 넣는 건 "옵션 하나 추가"처럼 느껴졌지만, 실제로는 모든 분기·리스트·권한 체크가 곱절로 복잡해졌습니다. 상태 머신은 노드 수가 아니라 전이 수로 복잡도를 재야 합니다.
기존 흐름으로 해결 가능한지 먼저 검증한다
"반려 후 재신청" 흐름을 UX 개선으로 풀 수 있었다면, 새 축을 넣을 필요가 없었습니다. 반려 사유를 더 잘 보여주고, 재신청 시 이전 폼을 자동 채워주는 식으로도 같은 목적을 달성할 수 있었습니다.
권한·취소 정책과 함께 설계되지 않은 상태는 버그 온상이다
새 상태를 추가할 때는 그 상태에서의 모든 권한·액션·알림을 표로 미리 정리하고 들어가야 합니다. 이번에는 그 표가 비어 있었습니다.
정리
보완 요청(RVS) 단계 도입은 현업 요구에는 맞았지만, 기존 상태 머신과 권한 체계가 흡수할 수 있는 복잡도를 초과했습니다. 결국 하루 만에 롤백하고 승인/반려 두 축 구조를 유지하는 대신, 반려 UX 개선으로 같은 목적을 달성하기로 방향을 바꿨습니다.
코드는 얼마든지 되돌릴 수 있지만, "왜 넣었고 왜 뺐는가"를 기록으로 남기지 않으면 다음 분기에 또 같은 논의가 반복됩니다. 롤백 커밋 메시지와 함께 이 글을 남기는 이유입니다.