Skip to content

(레거시) 주문 처리로 인한 재고 차감에서 발생하는 동시성 이슈 문제를 어떻게 해결할까?

daadaadaah edited this page Sep 20, 2024 · 1 revision

🧨 문제

  • 주문 처리로 인한 재고 차감에서 발생하는 동시성 이슈 문제를 어떻게 해결할까?

🎈 동시성 이슈가 문제되는 로직과 상황

로직

1. 딜 상품 조회 및 검증
2. 딜 상품 재고 감소
3. 딜 상품 정보 업데이트

구체적인 예시 상황

/*
 * 1. [A 트랜잭션] 딜 상품 조회 : 2개
 * 2.                                  [B 트랜잭션] 딜 상품 조회 : 2개
 * 3. [A 트랜잭션] 주문량(2개) 만큼 재고 감소
 *    : 2(조회 결과) - 2(주문 수량) = 0
 * 4.                                  [B 트랜잭션] 주문량(1개) 재고 감소
 *                                       : 2(조회 결과) - 1(주문 수량) = 1 
 * 5. [A 트랜잭션] 재고량 0으로 딜 상품 업데이트 
 * 6.                                  [B 트랜잭션] 재고량 1로 딜 상품 업데이트 // 🧨 데이터 일관성 깨짐, 실제 재고량이 0으로 품절인데, 1로 업데이트 되는 문제 발생
 * /

🚀 해결책

  • 다음과 같은 생각의 흐름으로 딜 상품과 재고 분리하는 구조Redis DECR 활용해서 재고 사후 검증 후 rollback 시키는 방법으로 동시성 이슈 문제를 해결했습니다.

🎈 생각 1. 분산락 사용하자.

  • 맨 처음에는 동시성 이슈 문제를 해결할 때 가장 흔하게 사용하는 분산락을 사용해보자라는 생각을 하였습니다.
  • 그런데, 분산락을 사용할 경우, 작업을 복잡하게 만들고, 디버깅하기 어려운 문제가 있습니다.
  • 멀티쓰레드 환경에서 발생하는 버그들이 재현하기도 함들고, 특수한 케이스에서만 발생되기 때문에, 오랜 시간이 지나서야 결함이 발견되는 문제가 있습니다.
  • 또한, 설정한 락 임대하는 시간보다 실제로 더 걸려서, 로직이 다 수행하기도 전에 락이 해제되고 다른 쓰레드가 락을 잡게 될 경우, 개발자가 의도하지 않은 대로 동작할 수 있는 문제가 있습니다.

🎈 생각 2. 분산락을 사용하지 않고 동시성 문제를 해결하는 방법이 없을까?

  • 위 같은 분산락의 문제점 때문에, 가능한 락 로직을 배제하는 방법이 없을까에 대해 고민하면서 왜 락이 필요한지에 대해 생각해봤습니다.
  • 분산락이 필요한 이유는 아래 3가지 로직이 원자성 있게 처리되어야 되기 때문이었습니다.
1. 딜 상품 조회
2. 딜 상품 재고 감소
3. 딜 상품 정보 업데이트
  • 그러면, 위 3가지 로직을 단순화 시키는 방법이 없을까? 라는 생각한 결과, 2번 로직과 3번 로직을 한번에 처리해 주는 Redis의 DECR 를 사용해볼까? 라는 생각을 했습니다.

🎈 생각 3. 2번 로직과 3번 로직을 한번에 처리해 주는 Redis의 DECR 를 사용해볼까?

  • 그런데, Redis의 DECR를 사용하게 되면, 딜 상품에서 재고를 분리시켜야 했습니다.
  • 그래서, 다음과 같이 표를 만들어, 딜 상품과 재고를 통합했을 때와 분리했을 때의 각각의 장단점에 대해 생각해봤습니다.

딜 상품과 재고 통합형 저장 구조 vs 딜 상품과 재고 분리형 저장 구조

구조 (A 구조) 딜 상품과 재고 통합형 저장 구조 (B 구조) 딜 상품과 재고 분리형 저장 구조
장점 1. CRUD 상황에서 ‘CRD 상황’(예: 상품 등록, 상품 조회, 상품 삭제 API)에서 B 구조보다 유리하다.
(1) 딜 상품과 재고량이 이미 통합되어 있으므로, 개발자가 직접 원자성 있게 트랜잭션 처리를 고려하지 않아도 되므로, 애플리케이션 로직이 간단하다.
(2) Redis에 1번만 Access하면 되므로, B 구조에 비해 읽기 또는 쓰기 부하가 덜하고, CRD 상황에서 B 구조보다 높은 TPS를 가질 수 있다.
1. CRUD 상황에서 ‘U 상황’(예: 주문 API)에서 A 구조보다 유리하다.
(1) 동시성 제어 로직이 필요없으므로, 애플리케이션 로직의 복잡도가 A 구조에 비해 간단하고, U 상황에서 A 구조보다 높은 TPS를 가질 수 있다.
2. 추후에 재고 도메인이 확장되어 분리해야 되는 상황이 발생할 때 A 구조보다 작업량이 줄어든다.
단점 1. CRUD 상황에서 U 상황(예: 주문 API)에서 B 구조보다 불리하다.
(1) 동시성 제어 로직이 필요하므로, 애플리케이션 로직의 복잡도가 증가한다.
(2) 동시성 제어 로직으로 인해 U 상황에서 B 구조보다 낮은 TPS를 가질 수 있다.
(3) 경우에 따라서 Redis 로직이 필요하므로, B 구조보다 높은 Redis 쓰기 부하가 발생할 수 있다.
2. 추후에 재고 도메인이 확장되어 분리해야 되는 상황이 발생할 때 B 구조보다 작업량이 많아지고, 그로 인한 실수도 발생할 수 있다.
1. CRUD 상황에서 ‘CRD 상황’(예: 상품 등록, 상품 조회, 상품 삭제 API)에서 A 구조보다 불리하다.
(1) 딜 상품과 재고량을 개발자가 직접 원자성 있게 트랜잭션 처리를 고려해야 하므로, 애플리케이션 로직의 복잡도가 증가한다.
(2) 트랜잭션 실패로 인해 재시도 로직이 필요하고, 그로 인해 A 구조에 비해 읽기 부하가 발생할 수 있다.
(3) Redis에 2번씩 Access해야 하므로, A 구조에 비해 읽기 또는 쓰기 부하가 더 높을 수 있다.
(4) CRD 상황에서 B 구조보다 낮은 TPS를 가질 수 있다.

딜 상품과 재고 통합형 저장 구조 와 딜 상품과 재고 분리형 저장 구조의 CRUD 상황별 로직 정리

스크린샷 2023-08-29 오후 7 29 28

  • 그렇게 장단점을 비교해본 결과, CRD 상황에서 트랜잭션 처리로 복잡한 로직을 취하지만, U 상황에서 분산락 없는 간단한 로직을 취할 수 있는 딜 상품과 재고 "분리형" 저장 구조를 선택했습니다.
  • 왜냐하면, 현재 저희 서버 시나리오의 경우, U 상황은 트래픽이 몰리는 시나리오고, CRD 상황은 비교적 서버가 여유로운 시나리오입니다.
  • 따라서, 트랙픽이 몰리는 U 상황에서 복잡한 로직을 취하는 딜 상품과 재고 "통합형" 저장 구조 보다 비교적 서버가 여유로운 CRD 상황에서 복잡한 로직을 취하는 딜 상품과 재고 "통합형" 저장 구조이 더 낫다고 생각했습니다.
  • 왜냐하면, 트래픽이 몰리는 U 상황에서 문제가 발생하면, 개발자가 대응하기 어렵고, 또한, 이때 문제가 발생하면 돈과 관련있는 로직이라서 비즈니스적으로도 리스크가 있다고 생각했습니다.
  • 따라서, 서버가 비교적 여유로울 때 CRD 상황에서 문제가 발생하더라도 개발자가 대응하고 쉽고, 문제가 발생하더라도 비즈니스적으로 문제가 되는 포인트가 덜한 딜 상품과 재고 "분리형" 저장 구조를 선택했습니다.
  • 그 결과, 로직은 다음과 같이 단순해졌습니다.
1. 재고 조회 및 검증
2. 재고 감소
  • 그런데, 여전히 분산락을 통해 묶어주지 않으면, 다음과 같은 상황에서 데이터의 일관성이 안맞는 문제가 발생하는 상황이었습니다.
/*
 * 1. [A 트랜잭션] 재고 조회 : 2개
 * 2.                                           [B 트랜잭션] 재고 조회 : 2개
 * 3. [A 트랜잭션] 주문량(2개) 만큼 재고 감소 : 현재 재고량(2) -> 차감 후 재고량(0)
 * 4.                                           [B 트랜잭션] 주문량(1개) 재고 감소 : 현재 재고량(0) -> 차감 후 재고량(-1) 개 // 🧨 차감이 되지 않아야 하는데, 차감 됨
 * /
  • 이때, 재고 데이터의 일관성을 보장하기 위해 분산락을 사용하는 건데, 분산락을 사용하지 않으면서 일관성 보장하는 방법은 없을까? 라는 생각을 하였습니다.

🎈생각 4. 재고 데이터의 일관성을 보장하기 위해 분산락을 사용하는 건데, 분산락을 사용하지 않으면서 일관성 보장하는 방법은 없을까?

  • Redis의 DECR 명령어 실행 후 return 값을 활용하여 재고를 사후 검증하는 방법으로 하고, 잘못 감소가 이루어진 경우, Rollback 해준다면, 최종적으로 데이터의 일관성을 맞춰지게 되어, 분산락을 사용하지 않으면서 데이터 일관성 보장할 수 있었습니다.
/*
 * 1. [A 트랜잭션] 재고 조회 : 2개
 * 2.                                           [B 트랜잭션] 재고 조회 : 2개
 * 3. [A 트랜잭션] 실제 주문량(2개) 만큼 재고 감소 : 현재 재고량(2) -> 차감 후 재고량(0)
 * 4.                                           [B 트랜잭션] 실제 주문량(1개) 재고 감소 : 현재 재고량(0) -> 차감 후 재고량(-1) 개 // 🧨 차감이 되지 않아야 하는데, 차감 됨
 * 5.                                           [B 트랜잭션] 실제 주문량(1개) 만큼 rollback // ✨ 차감되지 않아야 했기 때문에, 재고 원상 복귀 시킴
 * /
Clone this wiki locally