대용량 트래픽에서 동시성 문제를 고려한 선착순 쿠폰 발급 처리

2025. 2. 13. 20:00·백엔드

 

대용량 트래픽을 처리하는 프로젝트를 진행하며 이때 동시성 문제를 처리한 방법을 공유해보겠습니다.

 

문제 상황

  • 대용량 트래픽이 발생했을 때 동시성 문제를 어떻게 해결할 수 있을까?
  • 선착순 쿠폰을 사용자가 발급 받을 때 단시간 대용량 트래픽이 발생할 수 있다.
  • 이때 스프링과 같은 멀티 쓰레드 기반의 환경에서는 동시에 같은 데이터에 접근하는 Race Condition이 발생할 수 있다.
  • 이로 인해 쿠폰이 정해진 수량을 넘어서 발급될 가능성이 존재하게 된다.

위 문제를 해결하기 위해 다음과 같은 사항들을 고려했다.


고려 사항

1. 프로세스 Lock(Synchronized)

프로세스에서 한 데이터를 쓰레드가 사용하면 다른 쓰레드가 사용하지 못하도록 막는 방식

위 방식은 너무 큰 문제가 있었다. 서버가 여러 개 떠있을 때는(프로세스가 여러개) 프로세스 lock은 전혀 의미가 없게 된다.

내가 진행하는 프로젝트는 기본적으로 대용량 트래픽을 고려하고 설계하는 프로젝트이다. 이런 프로젝트의 아키텍처에서는 상황에 따라 서버를 증설하는 scale-out 방식을 고려한 설계가 필수적이라고 생각했다. 따라서 프로세스 Lock을 통해서는 동시성 문제를 해결할 수 없다고 판단했다.


2. DB Lock

한 트랜재셕인 데이터베이스의 특정 데이터에서 작업을 하고 있다면 다른 트랜잭션이 접근하지 못하도록 막는 방식

많은 참고자료에서 동시성 문제를 해결하기 위해 DB Lock을 사용하는 것을 확인했다.

하지만 나는 DB Lock을 사용하지 않기로 결정했는데 이유는 다음과 같다.

  • 선착순 쿠폰 발급에서 동시성 제어가 필요하면 부분은 쿠폰을 발급 받을 수 있는 수량이 남았는지 데이터를 읽는(read) 부분이다.
  • 하지만 DB lock을 사용한다면 동시성 문제를 해결하기 위해 수량이 남았는지 읽고 쓰는 작업까지 모두 lock을 걸어줘야 한다. 그렇지 않으면 데이터를 읽고 쿠폰이 남았다고 판단하여 쿠폰을 줄이는 도중에 다른 쓰레드가 해당 데이터를 데이터를 다시 읽을 수도 있기 때문이다.
    • ex) 남은 쿠폰 1개,
    • userA가 쿠폰 수량 read -> 1개의 쿠폰이 남음, 쿠폰의 수량을 1개 감소 시킴
      • 이때 userA가 쿠폰을 읽고 수량을 감소시키려는 로직 사이에 userB가 쿠폰의 수량을 read한다면 userB도 쿠폰이 수량이 1로 읽게 되어 쿠폰 수량을 감소시키는 로직을 처리
    • 최종적으로 쿠폰의 수량은 -1이됨
  • 이와 같은 문제로 데이터의 읽기와 쓰기 모두에 Lock을 거는 방식은 대용량 트래픽에서 사용자 경험을 고려하면 좋아보이지 않았다

3.Redis 활용

최종적으로 내가 동시성 문제를 제어하기 위해 활용한 방법은 Redis를 활용하는 것이다.

  • Redis는 단일 쓰레드로 작동. 아무리 많은 요청이 와도 순차적으로 하나씩 처리
  • 따라서 Redis에 쿠폰의 재고를 저장해두고 요청이 올대마다 먼저 redis에 저장된 쿠폰의 재고를 감소시키고 남은 수량을 파악하는 방식으로 동시성을 해결할 수 있었다.

이때 주의점은 redis에서 쿠폰의 재고를 감소시키기 전에 먼저 수량을 읽는 작업을 진행하게 된다면, 위에와 같이 읽고 쓰는 중간 과정에 다른 사용자가 데이터를 읽는 작업이 진행될 수 있다. 이러면 다시 한 번 동시성 문제가 발생하게 된다.

따라서 Redis에서 먼저 수량을 감소시키고 감소한 수량이 음수인지 확인하는 방식으로 동시성 문제를 해결했다.

코드를 간단히 공유하자면 큰 흐름은 아래와 같다.

  1. 쿠폰 타입(전체 적용, 특정 공연에 적용 등)에 맞춰 쿠폰을 생성하고 생성된 쿠폰을 Redis에 저장한다.
    • cacheRepository.save(coupon)
    • 이때 TTL은 쿠폰의 만료 날짜로 저장했다.
  2. 쿠폰 발급 요청이 오면 선착순 쿠폰일 경우 redis에서 수량을 감소시키고 재고를 확인한다.
  3. 이후 발급이 가능하다면 쿠폰 발급 메시지를 발행한다.

 

실제 위와 같이 시스템을 구축후 테스트 했을 때 다음과 같은 결과를 얻을 수 있었다.


Test

Test 시나리오 1

  • 100명의 유저가 쿠폰 발급을 요청, 쿠폰의 발급 가능 수량은 50개
  • 쓰레드 : 100,, ramp-up 시간 1, loop-count: 10
  • 전체 쿠폰 발급 수량은 50개여야 함(동시성 문제)

Jmeter

  • 에러율 : 95% -> 쿠폰이 발급된 50 요청 외에는 재고 없음(에러)을 응답해야함
  • 처리량 : 531./sec

Database

  • 쿠폰 발급 수 : 50개

 


Test 시나리오 2

  • 1000명의 유저가 쿠폰 발급을 요청
  • 쓰레드 : 100
  • ramp-up 시간 : 1
  • loop-count : 10
  • 1초안에 100명의 유저가 총 10번 시도 -> 1000개의 쿠폰이 발급되어야 함

Jmeter

  • 에러율 : 0.00%
  • 처리량 : 762.2/sec

Database

  • 쿠폰 발급 수 : 1000개

 

'백엔드' 카테고리의 다른 글

Redis + Ip 기반 조회 수 어뷰징 방지  (0) 2025.03.16
목록 조회 시 페이징 쿼리 최적화를 통한 최소 10배 성능 개선  (0) 2025.03.12
github packages를 통한 공통 모듈 개발기  (2) 2025.02.13
CustomArgumentResolver를 통해 Pageable 입력 받기  (0) 2025.02.13
Spring Adapter-Handler를 흉내낸, 확장 가능성을 고려한 전략 패턴 기반 회원가입 컴포넌트 설계  (0) 2025.02.13
'백엔드' 카테고리의 다른 글
  • 목록 조회 시 페이징 쿼리 최적화를 통한 최소 10배 성능 개선
  • github packages를 통한 공통 모듈 개발기
  • CustomArgumentResolver를 통해 Pageable 입력 받기
  • Spring Adapter-Handler를 흉내낸, 확장 가능성을 고려한 전략 패턴 기반 회원가입 컴포넌트 설계
shoon95
shoon95
shoon95 님의 블로그 입니다.
  • shoon95
    shoon95 님의 블로그
    shoon95
  • 전체
    오늘
    어제
    • 분류 전체보기 (18)
      • 백엔드 (10)
      • 프로젝트 (1)
      • 트러블 슈팅 (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
    • 학습 정리
  • 공지사항

  • 인기 글

  • 태그

    동시성
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
shoon95
대용량 트래픽에서 동시성 문제를 고려한 선착순 쿠폰 발급 처리
상단으로

티스토리툴바