파트너 출금 승인에서 동시 요청 처리 시 상태 경합 수정
목차
결제 플랫폼의 파트너포털 출금 승인 기능에서 발견한 동시성 버그를 고쳤다. 여러 요청이 거의 동시에 같은 출금 건을 승인하려 할 때, 상태 체크와 업데이트 사이의 간격으로 인해 중복 처리나 잔액 불일치가 생기는 문제였다.
파트너 출금 시스템과 동시성의 필연성
파트너포털에서 판매자나 가맹점주가 정산금을 출금 신청하면, 운영팀이 그 요청을 승인하는 플로우가 있다. 단순해 보이지만 실제로는 여러 운영자가 관리 대시보드를 띄우고 거의 동시에 같은 출금 건을 처리하려 할 수 있다. 특히 배치 처리로 자동 승인하는 로직이 있다면, 웹 UI에서의 수동 승인과 배치가 겹칠 수도 있다. 이런 상황에서 상태 관리가 느슨하면 출금 상태는 "승인됨"이 되었는데 잔액은 복구되지 않거나, 반대로 상태는 그대로인데 잔액만 두 번 빠지는 불일치가 생긴다.
상태 경합의 구조적 문제
이 버그의 핵심은 체크-업데이트 사이의 타이밍 갭(check-then-act 안티패턴)이었다. 로직은 대체로 이랬다:
1. 출금 건의 현재 상태 조회 (대기 중인가?)
2. 상태가 "대기"면 → "승인 처리 중" 으로 변경
3. 잔액 복구 처리
4. 최종 상태를 "승인됨" 으로 업데이트
문제는 1번과 2번 사이에 다른 스레드가 끼어들 수 있다는 것이다. 요청 A가 "상태가 대기인지 확인"까지는 가고, 상태를 변경하기 전에 요청 B가 같은 확인을 해버린다. 둘 다 "아, 상태가 대기네. 내가 처리해야겠다"라고 판단하고 독립적으로 잔액을 복구하려 한다. 결과적으로 실제로는 한 건의 출금이 두 번 승인된 것 같은 상태가 된다.
해결: CAS 패턴으로 상태를 원자적으로 선점
CAS(Compare-And-Swap)는 데이터베이스 레벨에서 제공하는 원자성 연산이다. "지금 값이 A라면 B로 변경하고, 성공 여부를 반환해" 하는 식의 명령이다. 이를 활용하면:
// UPDATE ... SET state = '처리중' WHERE id = ? AND state = '대기'
// 영향받은 행이 1이면 내가 성공적으로 선점한 것
// 0이면 다른 요청이 이미 상태를 바꿨다는 뜻
이 방식이라면 A와 B가 동시에 날아와도 하나는 성공하고 하나는 실패한다. 실패한 요청은 더 이상 진행하지 않으니 중복 처리가 원천 차단된다.
추가로 트랜잭션 격리 수준(isolation level)도 함께 검토했다. 최소 READ_COMMITTED 는 필요하고, 더 견고하게 가려면 REPEATABLE_READ 정도는 고려할 만하다. 잔액 복구 로직이 여러 테이블에 걸쳐 있다면 더욱 그렇다.
배운 점: 코드 검증 단계의 가치
이 버그는 단위 테스트나 통합 테스트만으로는 잡기 어려웠을 가능성이 높다. 동시성 버그는 특정 타이밍에만 재현되기 때문에, 순차적인 테스트 시나리오에서는 나타나지 않을 수 있다. 다행히 코드 검증 단계(정적 분석, 코드 리뷰, 아키텍처 검토)에서 발견되었다.
이것도 일종의 회고인데, 상태 변경이 필요한 비즈니스 로직은 설계 단계부터 동시성을 염두에 두고 구현해야 한다는 점을 다시 확인했다. 출금, 결제, 포인트 차감 같은 금전 거래 관련 기능은 특히 더 그렇다. 팀에 이런 패턴이 또 있을 수 있으니, 앞으로는 코드 리뷰 체크리스트에 "상태 업데이트가 원자적인가?" 를 명시적으로 넣기로 했다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.