동시성 문제라고 하면 보통 여러 사용자가 동시에 상품을 구매하면서 재고를 감소시키는 상황을 먼저 떠올린다.
나 역시 처음에는 선착순 구매 과정만 안전하게 만들면 된다고 생각했다.
하지만 특가상품 구매 이후 결제에 실패하면 차감했던 재고를 다시 증가시켜야 한다.
이 재고 복구 과정에서도 동시성 문제가 발생할 수 있었다.
이번 글에서는 결제 실패 처리 과정에서
문제를 어떻게 처리하고 테스트했는지 정리한다.
특가상품 재고가 10개 있다고 생각해보자.
사용자가 상품 하나를 구매한다.
재고 10
↓
주문 생성
↓
재고 9
이후 결제에 실패하면 차감했던 재고를 다시 복구해야 한다.
9 → 10
그런데 동일 주문에 대한 결제 실패 요청이 두 번 들어온다면 어떻게 될까?
아무런 검증이 없다면
첫 번째 실패 요청
9 → 10
두 번째 실패 요청
10 → 11 ❌
실제 초기 재고보다 더 많은 재고가 생길 수 있다.
따라서 동일한 주문의 결제 실패가 이미 처리됐는지 확인하고 한 번만 재고가 복구되도록 해야 했다.
이번에는 서로 다른 두 주문이 같은 특가상품을 하나씩 구매했다고 생각해보자.
초기 재고 : 10
주문 A 구매
10 → 9
주문 B 구매
9 → 8
이후 두 주문의 결제가 거의 동시에 실패한다.
최종적으로는 두 재고가 모두 돌아와야 하기 때문에
8 → 9 → 10
이 되어야 한다.
하지만 두 Thread가 동시에 현재 재고를 8로 읽는다면 다음과 같은 문제가 생길 수 있다.
Request A
현재 재고 8 읽음
→ 9 저장
Request B
현재 재고 8 읽음
→ 9 저장
두 번의 복구가 발생했지만 최종 재고는 9가 된다.
한 번의 업데이트가 사라지는 Lost Update다.
먼저 동일한 주문이 두 번 실패 처리되는 것을 막아야 했다.
결제 실패 처리 시 주문 Row Lock을 획득하고 현재 주문과 결제 상태를 확인했다.
이미 실패 처리가 완료된 주문이라면 다시 실패 처리하지 않는다.
결제 실패 요청
↓
주문 Row Lock
↓
이미 처리된 주문인가?
↓
YES → 중복 처리 차단
NO
↓
결제 실패 처리 진행
이렇게 동일한 주문이 여러 번 처리되면서 재고가 반복적으로 복구되는 것을 방지했다.
하지만 주문 Row Lock만으로는 두 번째 문제가 해결되지 않는다.
주문 A와 주문 B는 서로 다른 주문이기 때문이다.
주문 A Lock
주문 B Lock
→ 서로 다른 Row이므로 동시에 획득 가능
하지만 두 주문이 변경하려는 특가상품 재고는 동일하다.
그래서 실제 재고를 복구할 때는 특가상품 Row에도 Lock을 적용했다.
Order A Lock
\
→ 동일 Promotion Product Lock
/
Order B Lock
같은 상품의 재고를 수정하는 작업을 순서대로 처리해 Lost Update를 방지했다.
결제 실패 시에는 단순히 재고만 증가시키는 것이 아니다.
다음 작업들이 함께 수행된다.
실패 Payment 저장
+
재고 복구
+
StockHistory 복구 이력 저장
만약
Payment 저장 성공
재고 복구 성공
StockHistory 저장 실패
처럼 일부만 DB에 반영된다면 이후 재고가 왜 변경됐는지 추적하기 어려워진다.
그래서 세 가지 작업을 하나의 트랜잭션으로 묶었다.
BEGIN
실패 Payment 저장
재고 +1
재고 복구 StockHistory 저장
COMMIT
중간에 하나라도 실패하면 전체 DB 변경을 Rollback하도록 구성했다.
테스트 조건은 다음과 같이 구성했다.
초기 이벤트 재고 : 10개
주문 A 구매 이후 : 9개
주문 B 구매 이후 : 8개
동시에 처리할 결제 실패 요청 : 2건
두 결제 실패 요청을 동시에 실행했다.
정상적인 결과는 다음과 같아야 한다.
8 → 9 → 10
테스트 결과 최종 재고는 정확히 10개로 복구됐다.
추가로 다음 데이터도 함께 확인했다.
FAILED Payment
→ 2건
StockHistory RESTORE
→ 2건
재고 이력에도
8 → 9
9 → 10
순서로 정확하게 기록되는 것을 확인했다.
재고 동시성은 재고를 줄이는 순간에만 발생하는 문제가 아니었다.
같은 재고 값은 여러 기능에서 변경된다.
구매 성공
→ 재고 감소
결제 실패
→ 재고 증가
환불
→ 재고 증가
선착순 구매에서 재고 감소만 안전하게 만든다고 해서 전체 재고 정합성이 보장되는 것이 아니었다.
이번 경험을 통해 공유 데이터를 변경하는 기능을 구현할 때는
"이 데이터를 변경하는 다른 기능은 없는가?"
를 함께 확인해야 한다는 기준이 생겼다.
하나의 API만 정상적으로 동작하는지 보는 것이 아니라 같은 데이터를 변경하는 여러 기능이 동시에 실행됐을 때도 결과가 일관적인지 확인하는 것이 중요하다는 것을 배웠다.