전자 결재 시스템

Lost Update 문제 해결

기안자가 결재를 상신하고, 상급자가 모바일 앱에서 승인·반려하는 전자 결재 시스템

JavaSpringMySQL

아키텍처

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 방지

개요

  • 기안자가 결재를 상신하고 승인권자가 모바일에서 승인·반려 처리하는 전자 결재 시스템
  • 동시에 들어온 결재 처리 요청의 정합성 보장

문제

  • 현장 테스트 중 여러 승인권자가 동일 결재를 동시에 처리하자, 먼저 반영된 승인·반려 결과가 나중 요청에 덮어써지는 Lost Update가 실제로 발생
  • 원인은 결재 상태 확인(SELECT)과 상태 변경(UPDATE)이 분리되어 있어, 그 사이 다른 요청이 끼어들 수 있는 구조

해결 전략

  • 현재 결재 상태 조건을 포함한 단일 UPDATE 사용
  • MySQL UPDATE가 조건에 매칭된 row에 Record Lock을 걸어 동시 요청을 직렬화
  • UPDATE affected rows가 0이면 이미 처리된 결재로 판단
  • 충돌 상황을 비즈니스 예외로 처리해 중복 승인·반려 차단

기술 선택 이유

  • 조건부 UPDATE

    • 비관적 락보다 Lock 점유 시간과 대기 비용 감소
    • WHERE status = 'WAITING' 조건으로 DB의 원자적 갱신 결과 활용
  • affected rows 검증

    • 별도 조회 없이 UPDATE 성공 여부로 동시성 충돌 판단 가능
    • DB가 보장하는 원자성을 애플리케이션 로직에 직접 활용
  • Version 기반 낙관적 락 제외

    • 별도 version 컬럼 추가와 예외 변환 비용 대비 상태 전이 조건만으로 충분
    • 결재 상태 변경 대상이 단일 row라 조건부 UPDATE가 더 단순

검증

테스트 설정

  • CountDownLatch 기반 테스트 코드로 동일 결재 건에 다중 스레드 동시 진입 재현
  • 승인 요청과 반려 요청을 같은 시점에 출발시켜 동시 처리 상황 재현
  • 먼저 도착한 요청만 성공 처리되는지 확인
  • 후속 요청의 affected rows 0 반환과 예외 처리 확인

측정 결과

  • Lost Update 재현 시나리오 차단
  • 처리 완료 결재에 대한 중복 상태 변경 방지
  • 낮은 빈도의 충돌도 승인 결과 오염으로 이어지지 않는지 확인

배운 점

  • 상태 확인(SELECT)과 변경(UPDATE)을 분리하면 그 사이 다른 요청이 끼어들어 Lost Update가 발생할 수 있음
  • 상태 조건을 WHERE에 담은 단일 UPDATE는 MySQL이 매칭된 row에 Record Lock을 걸어, 별도 조회 없이 원자적으로 동시성 충돌을 차단