기록은 가장 쉬운 명상입니다.
Memodian

지표 버전 관리 및 결재 승인 프로세스의 일관성을 보장하기 위한 멱등적 구조 설계

10 min read · 2025년 11월 22일

요약

지표 관리 기능에서 결재중 상태가 중복 생성되는 문제를 발견하고,
“사전 체크 API + 조건부 UPDATE 기반 멱등적 결재요청 API” 구조로 개선하여
지표 버전 관리·결재 프로세스·부서/임계치 정보의 일관성을 안정적으로 보장했어요.

1. 문제 정의

지표관리 화면은 한 지표에 대해 “등록 → 수정 → 결재요청 → 승인(새 버전 생성)”이라는 전체 수명주기를 담당해요.
그러나 다음과 같은 문제로 인해 버전 일관성·결재 상태 일관성·동시성 안전성이 깨질 위험이 있었어요.

기존 문제 상황

  1. 여러 사용자가 같은 지표를 동시에 수정할 수 있음

  2. 한 사용자가 결재요청을 한 뒤에도,
    다른 사용자가 기존 화면 그대로 결재요청을 다시 시도하는 것이 가능

  3. 이때 TASK 테이블에서

    • 동일 지표(ANMLY_INDIC_ID)에 대해
      ANMLY_INDIC_STAT_CD = '02'(결재중) 상태의 작업본이 여러 건 생성되는 문제 발생
  4. 결재버전기준 테이블(VER_DTL / THLD_DTL / MNG_DEPT_DTL)은
    버전별 1개(또는 3개, N개) 행만 존재해야 하는 강한 일관성 규칙을 가짐
    그러나 결재중 TASK가 여러 건 존재하면

    • 어떤 TASK가 실제 승인되어야 하는지 불명확해지고
    • 승인 후 버전 증가(batch) 시
      중복된 버전 생성, 부서 매핑 꼬임, 임계치 정보 충돌 등 심각한 데이터 불일치 위험 발생

즉,

“지표는 버전별로 1개만 존재해야 하고, 한 지표 한 버전에 대해 두개의 결재요청이 들어올 수는 없다.”

이 비즈니스 규칙을 기술적으로 완벽히 보장할 필요가 있었어요.


2. 원인 분석

(1) 화면 상태 기반 처리의 한계

지표관리 화면은 사용자가 지표를 선택하거나 버튼을 클릭할 경우, 다음과 같이 상태를 분기 별로 처리했어요.

하지만 화면 상태(ScreenState.mode, ScreenState.isDirty)만으로
지표가 현재 결재 가능한지 여부를 판단했어요.

👉 실제 DB 상태(누가 어떤 지표를 결재중인지)는 전혀 확인하지 않았어요.

(2) 결재요청 API가 상태만 바꿈 (검증 없음)

기존 결재요청 API는:

shell
작성중(01) → 결재중(02)

으로 변경하는 것만 수행하고,
이미 다른 사람이 결재 요청했는지에 대해서는 검증하지 않았어요.

👉 중복 결재중 TASK 생성 가능

(3) 사전 체크 API 부재

프론트에서는 결재요청 시 다음과 같은 정보가 필요하지만,
어디에서도 조회하고 있지 않았어요.

결과적으로,


3. 해결 전략 (멱등 + 일관성 보장 구조 설계)

전체 해결책은 **UX 레벨 사전 방어 + 서버 레벨 최종 방어(멱등성)**의 이중 구조로 설계했어요.


1) 사전 체크 API 신설 — /retrieve-appv-task

결재요청 버튼 클릭 시 항상 서버에 실제 상태를 확인하도록 새로운 API를 만들었다.

쿼리에서 다음 조건을 만족하는 작업본 최신 1건만 조회:

이를 통해 프론트는 결재 요청 전에 사용자에게 다음과 같이 안내할 수 있게 되었어요.

“현재 이 지표는 ‘이병건’님에 의해 결재 요청된 상태입니다. 해당 결재요청을 진행할 수 없습니다.”

사용자는 현재 상태를 정확히 인지한 뒤 다음 행동을 할 수 있게 되었고,
잘못된 결재 요청 시도를 UI 단계에서 먼저 걸러낼 수 있게 되었어요.


2) 실제 결재요청 API에서 DB 조건부 UPDATE로 멱등성 보장

사전 체크는 어디까지나 UX용이고,
동시 클릭·여러 탭 등은 항상 존재하는 리스크이기 때문에,
핵심은 결재요청 API에 한 번 더 강력한 DB 제약을 넣는 것이에요.

조건부 UPDATE (핵심)

sql
UPDATE TB_EWS_ANMLY_INDIC_VER_TASK_DTL T SET ANMLY_INDIC_STAT_CD = '02' WHERE T.ANMLY_INDIC_CHG_TASK_ID = #{taskId} AND T.ANMLY_INDIC_STAT_CD = '01' AND NOT EXISTS ( SELECT 1 FROM TB_EWS_ANMLY_INDIC_VER_TASK_DTL X WHERE X.ANMLY_INDIC_ID = T.ANMLY_INDIC_ID AND X.ANMLY_INDIC_CHG_TASK_ID != T.ANMLY_INDIC_CHG_TASK_ID AND X.ANMLY_INDIC_STAT_CD IN ('02','03') )

이 패턴을 통해 지표결재 프로세스의 핵심 비즈니스 제약이 보장돼요.

“지표당 결재중 작업본은 단 하나만 존재해야 한다.”

이 구조 자체가 멱등성(Idempotent)을 만족하며,
동일 요청이 여러 번 반복되어도 DB 상태는 변하지 않아요.


3) 전체 화면 흐름도 비즈니스 규칙과 동기화

즉,
등록 ⇒ 수정 ⇒ 결재요청 ⇒ 승인 ⇒ 새 버전 반영
전 과정이 데이터 규칙 기반으로 자동 정렬되었어요.


4. 결과

1) 지표당 결재중 상태가 1건만 존재하는 구조 확립

중복 결재중 TASK 생성 문제 완전 해소.

2) 화면-DB 간 상태 불일치 해소

3) 결재 프로세스의 UX 대폭 향상

사용자에게:

명확히 안내하므로 불필요한 혼란 제거.

데이터 무결성 보장

멱등적 결재요청 API 완성

요청이 여러 번 발생해도 DB 상태는 정확히 한 번만 변경됨.