선착순 시스템 완전 정리: 6가지 구현 방식과 선택 가이드

선착순 시스템 완전 정리: 6가지 구현 방식과 선택 가이드


서론

이전 글에서 FOR UPDATE의 한계를 다뤘다. 동시 100명이 몰리면 99명이 대기하고, 커넥션 풀이 고갈되고, 데드락 위험까지 따라온다. DB 락만으로는 트래픽이 높은 선착순 시스템을 감당할 수 없다.

이번 글에서는 한 발짝 물러서서 전체 그림을 본다 — 선착순 시스템이 해결해야 하는 문제가 뭐고, 어떤 방식들이 있고, 각각 언제 쓰는 게 맞는지.


1. 선착순 시스템이란?

콘서트 티켓 예매, 한정판 스니커즈 구매, 쿠팡 로켓딜 — 모두 정해진 수량을 먼저 신청한 사람에게 배정하는 시스템이다. 단순해 보이지만, 수천~수만 명이 동시에 몰리는 순간 3가지 핵심 문제가 발생한다.

핵심 3대 문제

문제설명안 풀면 어떻게 되나?
동시성 제어수천 명이 같은 순간에 같은 재고를 차감하려 한다재고 100개인데 150명이 구매 성공 (초과 판매)
정확한 재고 차감차감 연산이 원자적이지 않으면 숫자가 꼬인다재고가 음수가 되거나, 두 요청이 동시에 같은 값을 읽어서 하나만 차감됨
중복 구매 방지같은 사용자가 여러 번 요청하면 중복 당첨될 수 있다한 명이 10개를 가져감

비유: 빵집에서 한정 100개 빵을 판다고 생각해보자. 줄을 안 세우면(동시성 제어 없음) 사람들이 뒤엉켜서 빵을 집어간다. 카운터 숫자를 안 세면(재고 차감 오류) 101번째 사람도 빵을 받는다. 번호표 검사를 안 하면(중복 방지 없음) 한 사람이 줄을 여러 번 선다.


2. 6가지 구현 방식

2.1 DB 비관적 락 (SELECT FOR UPDATE)

핵심 원리: 재고 행을 먼저 잠그고, 차감하고, 잠금을 풀어서 한 번에 하나의 요청만 처리한다.

단계클라이언트서버 (DB)상태
1구매 요청SELECT stock FOR UPDATE → 행 잠금🔒
2stock > 0 확인 → UPDATE stock = stock - 1✅ 차감
3COMMIT → 잠금 해제🔓
4다음 요청이제야 락 획득 가능 → 반복
장점단점
추가 인프라 불필요 (DB만 있으면 됨)동시 요청이 직렬화됨 (99명 대기)
구현이 단순함데드락 위험
데이터 정합성 확실DB 커넥션 풀 고갈 위험

적합한 상황: 동시 접속 수십 명 이하, 추가 인프라를 도입하기 어려운 초기 서비스

2.2 Redis 원자 연산 (DECR)

핵심 원리: Redis의 DECR 명령어는 싱글 스레드에서 원자적으로 실행되므로, 락 없이도 안전하게 재고를 차감한다.

단계클라이언트서버 (Redis → DB)상태
1구매 요청DECR stock_key → 결과값 확인
2결과 ≥ 0이면 → 구매 성공, DB에 주문 저장
3결과 < 0이면 → INCR stock_key (복구) → 품절 응답
장점단점
초당 수만 TPS 처리 가능Redis 장애 시 데이터 유실 가능
락 없이 원자적 처리Redis와 DB 간 데이터 정합성 관리 필요
구현이 비교적 단순재고 복구 로직 필요 (결과 < 0일 때)

적합한 상황: 수백~수천 명 동시 접속, 빠른 응답이 중요한 서비스

2.3 Redis + Lua 스크립트

핵심 원리: 재고 확인과 차감을 하나의 Lua 스크립트로 묶어서 Redis에서 원자적으로 실행한다. 단순 DECR과 달리 “확인 → 차감”이 한 덩어리로 실행되어 음수 재고가 원천 차단된다.

단계클라이언트서버 (Redis → DB)상태
1구매 요청Lua 스크립트 실행 시작 (원자적)🔒
2stock > 0 확인 + 중복 구매 확인 + DECR → 한 번에 실행
3결과: 성공이면 DB에 주문 저장, 실패면 즉시 응답✅ / ❌
장점단점
재고 확인 + 차감 + 중복 체크를 원자적으로Lua 스크립트 디버깅이 어려움
음수 재고 원천 차단Redis 장애 시 데이터 유실 가능
복구 로직 불필요 (확인 후 차감이니까)스크립트가 길어지면 Redis 블로킹 위험

적합한 상황: 높은 트래픽 + 중복 구매 방지까지 Redis 레벨에서 처리하고 싶을 때

2.4 메시지 큐 (Kafka / RabbitMQ)

핵심 원리: 구매 요청을 큐에 넣고, 컨슈머가 순서대로 하나씩 꺼내서 처리한다. 요청 자체를 직렬화하는 방식이다.

단계클라이언트서버 (큐 → 컨슈머 → DB)상태
1구매 요청큐에 메시지 발행 → 즉시 “접수 완료” 응답📨
2컨슈머가 순서대로 메시지 소비
3재고 확인 → 차감 → 주문 생성✅ / ❌
4결과 확인 (폴링 / 웹소켓)처리 결과 전달📬
장점단점
트래픽 폭주에 강함 (버퍼 역할)응답이 비동기 (즉시 결과를 모름)
서버 부하 분산구현 복잡도 높음 (큐 + 컨슈머 + 결과 전달)
컨슈머를 늘려 처리량 조절 가능추가 인프라 필요 (Kafka / RabbitMQ)

적합한 상황: 대규모 트래픽, 즉시 응답이 필수가 아닌 경우 (쿠폰 발급, 이벤트 응모)

2.5 대기열 (Waiting Queue)

핵심 원리: 사용자를 대기열에 넣고 순번을 부여한 뒤, 순서가 되면 구매 페이지로 입장시킨다. 네이버 예매, 인터파크 티켓팅에서 흔히 보는 “앞에 N명 대기 중” 방식이다.

단계클라이언트서버 (Redis Sorted Set)상태
1접속ZADD queue timestamp userId → 대기열 진입🕐 대기
2”앞에 342명” 표시ZRANK queue userId → 현재 순번 조회
3순번 도달대기열에서 제거 → 구매 페이지 입장 허용🎫 입장
4구매 진행재고 차감 (Redis 또는 DB)✅ / ❌
장점단점
서버 부하를 일정하게 유지사용자 대기 경험 (UX 비용)
공정한 순서 보장구현 복잡도 높음 (대기열 + 입장 제어 + 만료 처리)
트래픽을 두 단계로 분산대기 이탈 시 슬롯 낭비 처리 필요

적합한 상황: 수만 명 이상 동시 접속, 공정한 순서가 중요한 티켓팅/예매 시스템

2.6 토큰 발급 방식

핵심 원리: 먼저 입장 토큰을 발급하고, 토큰을 가진 사용자만 구매할 수 있게 한다. 트래픽을 “토큰 발급”과 “실제 구매” 두 단계로 완전히 분리한다.

단계클라이언트서버상태
1토큰 요청토큰 발급 서버: 재고 수량만큼 토큰 발급 → 초과 시 거절🎟️ / ❌
2토큰으로 구매 요청구매 서버: 토큰 유효성 검증 → 재고 차감 → 주문 생성
3토큰 만료/사용 처리🔒
장점단점
구매 서버에 트래픽이 집중되지 않음토큰 발급 서버 + 구매 서버 분리 필요
토큰 수 = 재고 수 → 초과 판매 원천 차단토큰 만료/도용 방지 로직 필요
서버 분리로 부하 분산사용자 경험이 2단계로 나뉨

적합한 상황: 한정판 판매, 사전 예약, 트래픽을 물리적으로 분리해야 하는 대규모 시스템


3. 한눈에 비교

방식처리량 (TPS)구현 복잡도추가 인프라데이터 정합성적합 규모
DB 비관적 락낮음 (수십~수백)없음높음소규모
Redis DECR높음 (수만)⭐⭐Redis중간중규모
Redis + Lua높음 (수만)⭐⭐⭐Redis높음중~대규모
메시지 큐높음 (확장 가능)⭐⭐⭐⭐Kafka/RabbitMQ높음대규모
대기열높음 (제어 가능)⭐⭐⭐⭐Redis높음대규모
토큰 발급높음 (분산)⭐⭐⭐⭐토큰 서버높음대규모

처리량은 단순 수치 비교가 아니다. DB 락은 “순서대로 하나씩”이라 TPS가 낮고, Redis 방식은 “락 없이 원자적”이라 TPS가 높다. 메시지 큐와 대기열은 컨슈머 수를 늘려서 처리량을 조절할 수 있다는 점에서 “확장 가능”이다.


4. 어떤 방식을 선택할까?

의사결정 흐름

단계질문답변 → 방향
1동시 접속자가 몇 명인가?수십 명 → DB 비관적 락으로 충분
2추가 인프라(Redis) 도입이 가능한가?불가 → DB 비관적 락
3즉시 응답이 필수인가?필수 → Redis DECR 또는 Redis + Lua
4중복 구매 방지를 Redis에서 처리하고 싶은가?예 → Redis + Lua
5트래픽이 수만 명 이상이고, 비동기 응답이 괜찮은가?예 → 메시지 큐
6공정한 대기 순서가 중요한가?예 → 대기열
7구매 트래픽을 물리적으로 분리해야 하는가?예 → 토큰 발급

실전에서는 조합한다

실제 대규모 시스템은 하나의 방식만 쓰지 않는다. 예시:

시스템조합
쿠팡 로켓딜대기열 (입장 제어) + Redis (재고 차감)
콘서트 티켓팅대기열 (순번 관리) + 토큰 (입장 권한) + DB 락 (최종 결제)
한정판 스니커즈토큰 발급 (봇 방지) + Redis + Lua (재고 차감)
소규모 이벤트 쿠폰Redis DECR 하나로 충분

핵심은 **“어떤 방식이 최고인가”가 아니라 “우리 상황에 뭐가 맞는가”**다. 동시 접속 50명인 서비스에 Kafka를 도입하면 과잉 설계이고, 10만 명이 몰리는 티켓팅에 DB 락만 쓰면 서버가 다운된다.


정리

핵심 포인트내용
선착순의 3대 문제동시성 제어, 정확한 재고 차감, 중복 구매 방지
6가지 방식DB 락, Redis DECR, Redis + Lua, 메시지 큐, 대기열, 토큰 발급
규모에 맞게 선택소규모 → DB 락, 중규모 → Redis, 대규모 → 큐/대기열/토큰
실전은 조합대기열 + Redis, 토큰 + DB 락 등 여러 방식을 섞어 쓴다
과잉 설계 주의트래픽 규모와 인프라 여건에 맞는 가장 단순한 방식부터

다음 글에서는 4편: DB 락으로 선착순 시스템 구현을 다룬다. 가장 단순한 방식부터 직접 코드로 구현하고, 동시성 테스트로 한계를 확인해본다.

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.