전자 결재 시스템
승인권자가 모바일 앱에서 승인하거나 반려하는 전자 결재 시스템
아키텍처
개요
- 기안자가 결재를 상신하고 승인권자가 모바일에서 승인하거나 반려하는 전자 결재 시스템
문제
- 여러 승인권자가 동일 결재를 동시에 처리하면서 Lost Update 발생
- SELECT로 상태를 조회한 뒤 UPDATE하는 사이 다른 요청이 상태를 변경할 수 있는 구조
- 두 요청이 모두 '승인 대기'를 읽으면 다른 쪽 승인 요청이 반려 요청을 덮어써 최종 상태가 뒤집힘
해결 전략
WHERE status = '승인 대기'조건을 포함한 단일 UPDATE 사용- MySQL UPDATE가 조건에 맞는 인덱스 레코드에 record lock을 걸어 동시 요청을 직렬화
- affected rows가 0이면 이미 처리된 결재로 판단
기술 선택 이유
-
Redis 분산 락 - 제외
- 락 획득과 해제, TTL 관리가 추가되고 일관성 제어 지점이 DB와 Redis로 분산됨
-
비관적 락 (
SELECT ... FOR UPDATE) - 제외- 행을 미리 선점해 확실하지만, 경합 시 대기 시간이 길어지고 커넥션 점유가 늘어남
-
낙관적 락 - 제외
- version 컬럼 추가와 충돌 시 재시도와 예외 변환 비용이 필요
-
조건부 단일 UPDATE - 채택
- 별도 락 저장소나 컬럼 추가 없이 동시 요청 제어
검증
- 동일 결재 건에 승인과 반려를 다중 스레드에서 별도 트랜잭션으로 같은 시점에 요청해 재현
- 두 요청 중 먼저 상태를 변경한 1건만 성공, 후속 요청은 affected rows 0으로 예외 처리
- 최종 상태가 승인 또는 반려 중 하나로 남아 후속 요청의 덮어쓰기 차단 확인
회고
- affected rows 0을 이미 처리된 결재로 판단. 존재하지 않는 결재도 같은 결과가 나온다는 점은 고려하지 못했음
- 비관적 락 제외 근거로 든 대기 시간과 커넥션 점유는 수치 비교 없이 판단한 값임