WORK EXPERIENCE

전자 결재 시스템

상급자가 모바일 앱에서 승인·반려하는 전자 결재 시스템

Java · Spring · MySQL

아키텍처

Before - Lost Update 상황Approver AMySQLApprover BSELECT 상태 조회status: 대기SELECT 상태 조회status: 대기UPDATE 승인UPDATE 반려Lost Update마지막 요청이 덮어씀After - 조건부 UPDATE 적용Approver AMySQLApprover BUPDATE WHERE 상태=대기Record Lockaffected rows: 1UPDATE WHERE 상태=대기affected rows: 0충돌 예외 처리중복 처리 차단Lost Update 방지

개요

  • 기안자가 결재를 상신하고 승인권자가 모바일에서 승인·반려하는 전자 결재 시스템
  • 동일 결재에 승인·반려 요청이 동시에 들어와도 하나만 상태를 변경하도록 DB에서 충돌 제어

문제

  • 여러 승인권자가 동일 결재를 동시에 처리하면서 Lost Update 발생
  • SELECT로 상태를 조회한 뒤 UPDATE하는 사이 다른 요청이 상태를 변경할 수 있는 구조

해결 전략

  • WHERE status = '대기' 조건을 포함한 단일 UPDATE 사용
  • MySQL UPDATE가 조건에 맞는 row에 Record Lock을 걸어 동시 요청을 직렬화
  • affected rows가 0이면 이미 처리된 결재로 판단

기술 선택 이유

  • Redis 분산 락 — 제외

    • 락 획득·해제·TTL 관리가 추가되고 일관성 제어 지점이 DB와 Redis로 분산됨
  • 비관적 락 (SELECT ... FOR UPDATE) — 제외

    • 행을 미리 선점해 확실하지만, 경합 시 대기 시간이 길어지고 커넥션 점유가 늘어남
  • 낙관적 락 — 제외

    • version 컬럼 추가와 충돌 시 재시도·예외 변환 비용이 필요
  • 조건부 단일 UPDATE — 채택

    • 별도 락 저장소나 컬럼 없이 한 쿼리의 affected rows로 충돌 판정

검증

테스트 설정

  • 단일 애플리케이션 프로세스의 CountDownLatch 기반 통합 테스트로 동일 결재 건에 다중 스레드 동시 진입 재현
  • 승인·반려 요청을 별도 트랜잭션으로 같은 시점에 실행해 DB 경합 재현

측정 결과

  • 두 요청 중 먼저 상태를 변경한 1건만 성공, 후속 요청은 affected rows 0으로 예외 처리
  • 최종 상태가 승인 또는 반려 중 하나로 남아 후속 요청의 덮어쓰기 차단 확인