Continuous Challenge

[항해플러스 7기 백엔드] 6주차 - 주요 기능(포인트 충전/차감, 상품 재고 차감, 선착순 쿠폰 발급)에 대한 동시성 제어 방식 비교 본문

Study/항해플러스 7기

[항해플러스 7기 백엔드] 6주차 - 주요 기능(포인트 충전/차감, 상품 재고 차감, 선착순 쿠폰 발급)에 대한 동시성 제어 방식 비교

응굥 2025. 1. 21. 23:42

주요 기능 분석

1. 포인트 충전

  • 사용자가 자신의 계정에 포인트를 충전. 충전 금액이 포인트 잔액에 추가됨.
  • 단일 계정 기준으로 충전이 수행 되므로 충돌 가능성 낮음.
  • 충전 요청이 동시에 발생할 가능성은 상대적으로 낮으며, 발생하더라도 포인트는 독립적으로 누적 처리

2. 결제 시 포인트 차감

  • 사용자가 결제 시 포인트 차감. 잔액 부족 시 차감 불가.
  • 단일 계정 기준으로 차감이 수행되므로 충돌 가능성 낮음.

3. 주문 시 상품 재고 차감

  • 주문 시 선택한 상품의 재고를 차감. 재고 부족 시 차감 불가.
  • 동일 상품에 대한 다수의 주문 요청이 동시에 발생할 가능성 높음.

4. 선착순 쿠폰 발급

  • 쿠폰 발급 시 쿠폰의 잔여수량을 차감하고 사용자에게 쿠폰을 발급함.
  • 다수의 사용자가 동시에 요청하여 쿠폰 초과 발급 가능성이 매우 높음.

동시성 제어 방식 비교

1. 낙관적 락

  • 충돌 가능성이 낮다고 가정하고 작업을 진행. 커밋 시점에 충돌 여부 검증.
  • 데이터 갱신 후 버전 번호 비교로 충돌 검증.
  • 동시성 처리 효율적. Deadlock 방지.
  • 적합한 상황 : 충돌 가능성이 낮은 환경. 읽기 작업 비중이 높은 경우 -> 포인트 차감

2. 비관적 락

  • 충돌 가능성이 높다고 가정하고 작업 중에 잠금을 걸어 충돌 방지.
  • 데이터 접근 시 잠금을 설정하여 다른 트랜잭션의 접근 차단.
  • 데이터 무결성 보장. 충돌 방지로 롤백 비용 감소.
  • 동시성 처리 성능 저하. 잠금 유지로 자원 낭비 가능.
  • 적합한 상황 : 충돌 가능성이 높은 환경. 쓰기 작업 비중이 높은 경우 -> 상품 재고 차감, 선착순 쿠폰 발급

3. 분산 락

  • 분산 시스템에서 공유 자원에 대한 잠금 관리.
  • 중앙화된 락 서비스(Redis, Zookeeper 등)를 사용해 잠금 관리.
  • 분산 환경에서의 일관성 보장. 자원 접근 제어 가능.
  • 락 관리 시스템 필요. 네트워크 지연이 성능에 영향.
  • 적합한 상황 : 분산 시스템에서 자원 동시 접근 관리 필요 -> 상품 재고 차감, 선착순 쿠폰 발급

주요 기능별 락킹 방식 도입 및 비교

 주요 기능에 각 락킹 방식을 도입한 후 JMeter를 사용해 테스트 해보았다.

JMeter 지표

Summary Report
- Label : Sampler 명
- Samples : 샘플 실행 수 (Number of Threads X Ramp-up period)
- Average : 평균 걸린 시간 (ms)
- Min : 최소
- Max : 최대
- Std. Dev. : 표준편차
- Error % : 에러율
- Throughput : 초당 처리량 (bps) = JMeter에서는 시간 단위를 보통  TPS (Transaction Per Second)로 표현
- Received KB/sec : 초당 받은 데이터량 
- Sent KB/sec : 초당 보낸 데이터량
- Avg. Bytes : 서버로부터 받은 데이터 평균

1. 포인트 충전  (비관적 락)

포인트 충전은 충돌 가능성이 낮으며 충돌이 발생하더라도(같은 계정으로 동시에 충전 시도) 실패하지 않고 모든 요청이 성공해야 한다.

이 정도의 비교를 통해서도 비관적 락을 선택할 것 같다.

 

포인트 충전에 대해서는 동일한 유저가 동시에 5번(충돌 가능성 낮음을 가정)을 호출하는 케이스를 테스트하였다.

낙관적락

비관적락

 

5번 호출로 테스트해서인지 낙관적락 비관적락 결과의 차이는 크지 않았다. 

  • 낙관적락 - 충돌 시 예외 발생. 재시도 로직 구현 필요
  • 비관적락 - 대기 후 실행. 
  • 분산락 - 필요성에 비해 고비용.

하지만 낙관적 락의 경우 재시도 로직을 구현해야 하는 구현 복잡도가 발생하므로 비관적 락을 도입하기로 하였다.

2. 결제 시 포인트 차감 (비관적 락)

포인트 차감은 충돌 가능성이 낮으며 충돌이 발생할 경우(같은 계정으로 동시에 다른 주문에 대한 결제 시도) 꼭 성공하지 않아도 된다. (예외를 발생시켜 오류 메시지를 반환한다.)

 

포인트 차감에 대해서는 동일한 유저가 동시에 5번(충돌 가능성 낮음을 가정)을 호출하는 케이스를 테스트하였다.

낙관적락

비관적락

 

  • 낙관적락 - 충돌 시 예외 발생. 재시도 로직 불필요. 호출 건 일부 실패. 평균 소요시간 짧음.
  • 비관적락 - 모든 호출에 대해 성공.
  • 분산락 - 필요성에 비해 고비용.

결제의 경우에는 5번 호출임에도 Average, Max 에서 차이가 발생하는 것을 확인할 수 있었다. (낙관적 락 우세)

하지만 낙관적 락을 도입할 경우 에러가 60% 발생하는 반면, 비관적 락은 에러 0% 발생으로 모든 호출이 성공하는 것을 확인할 수 있다.

충돌 가능성이 낮기 때문에 실패가 발생하는 것보다는 모든 호출이 성공하는 것(모든 결제 건 성공)이 더 안정적인 서비스를 제공한다고 판단하여 비관적 락을 도입하기로 하였다. (낙관적 락의 실패는 재시도로직을 통해 보완 가능)

3. 주문 시 상품 재고 차감 (비관적 락)

재고가 30개인 경우 100명의 서로 다른 유저가 동시에 결제(재고 차감) API를 호출하는 케이스를 테스트하였다.

낙관적락

Thread 100 일 때 (재시도 로직 없을 경우)
Thread 1000 일 때

비관적락

Thread 100 일 때
Thread 1000 일 때

분산락

Thread 100 일 때
Thread 1000 일 때

모수가 작았을 땐(스레드 100개) 성능이 비관적 락 > 낙관적 락 >= 분산락 처럼 보였는데, 모수를 늘리니(스레드 1000개) 낙관적 락 > 분산 락 >= 비관적 락 순으로 보여졌다. (평균 소요시간 기준으로 비교)

  • 낙관적락 - 충돌 시 예외 발생. 재시도 로직 구현 필요. 재시도 수행 시 충돌 가능성 존재. 재시도 로직으로 인한 성능 저하 우려.
  • 비관적락 - 낙관적 락에 비해 소요 시간 긺. 조회 시 락을 걸어 충돌 발생을 방지.
  • 분산락 - 필요성에 비해 고비용. 평균 소요시간 가장 짧음. 모수가 낮을 경우 비관적락이 더 우세할 수 있음. 하나의 주문에 대해 여러 재고 조회 필요 & 여러 재고에 대한 멀티락 구현 필요.

상품 재고 차감의 경우, 인기 상품의 한정 판매와 같은 경우를 제외하고는 충돌 발생 가능성이 낮다고 판단하여 성능도 가장 우세하고, 충돌 가능성이 낮은 상황에 유리한 낙관적 락을 도입하기로 하였다. 

 

동시성 테스트 도중 낙관적 락은 메커니즘 상 최초의 커밋만을 인정한다는 사실을 발견하였고, 이를 해결하기 위해서는 재시도(retry) 로직을 구현해야 했다.

재시도 로직을 구현한 후, JMeter 를 사용해 성능 테스트를 해보니 재시도 로직이 없을 때에 비해 성능이 현저히 떨어지는 것을 확인할 수 있었다. 재시도 로직을 실행하는 도중에도 충돌이 발생할 가능성이 있고, 재시도를 수행하는 시간을 설정하는 것 또한 기준을 잡기가 모호했다. 

Thread 100 일 때 (재시도 로직 있을 경우)

따라서 스레드가 100일 경우 성능 테스트의 순위는 비관적 락 > 분산락 >>> 낙관적 락 으로 바뀌게 되어 비관적 락 방식을 사용하는 것으로 변경하였다.

만약 인기 상품의 한정 판매와 같은 이벤트의 빈도가 높고 사용자의 수가 많은 서비스였다면 분산락을 사용했을 것이다. 

4. 선착순 쿠폰 발급 (분산 락)

쿠폰의 잔여 수량이 30개인 경우 100명의 서로 다른 유저가 동시에 쿠폰 발급 API를 호출하는 케이스를 테스트하였다.

낙관적락

Thread 100개 일 때
Thread 1000개 일 때

비관적락

Thread 100개 일 때
Thread 1000개 일 때

분산락

Thread 100개 일 때
Thread 1000개 일 때

선착순 쿠폰 발급 역시 모수가 작았을 땐(스레드 100개) 성능이 비관적 락 > 분산락 >= 낙관적 락처럼 보였는데, 모수를 늘리니(스레드 1000개) 분산 락  > 낙관적 락 > 비관적 락 순으로 보여졌다. (평균 소요시간 기준으로 비교)

  • 낙관적락 - 충돌 시 예외 발생. 재시도 로직 불필요(에러메시지 리턴).
  • 비관적락 - 낙관적 락에 비해 소요 시간 긺.
  • 분산락 - 평균 소요시간 가장 짧음. 모수가 낮을 경우 비관적락이 더 우세할 수 있음.

하지만 트래픽이 대량으로 들어올 수 있는 확률이 가장 높은 기능이므로 분산 락을 사용하기로 하였다. (적용 경험도 해볼 겸..)


후기

로컬 환경에서 테스트한 결과 분산락의 성능이 모수에 따라 비관적 락이나 낙관적 락에 비해 우세하지 않을 수도 있다는 것을 알았다. 이론으로 생각했을 때와 실제 도입하여 테스트한 결과가 다를 수 있다는 것을 알았고, 도입 테스트 과정이 필요하다는 것을 느꼈다.

728x90
728x90
Comments