[RateLimit] 같은 영화 스케줄은 10초에 한 번만 예약 가능하다 - Redis + Lua로 실현한 비즈니스 제약

y001·2025년 4월 19일
0
post-thumbnail

1. 문제 제기: 인기 스케줄 예약, 어떻게 제어할 것인가?

콘서트, 극장, 이벤트 예매 시스템에서 흔히 발생하는 이슈는 다음과 같다:

  • 사용자 혹은 봇이 같은 스케줄에 대해 짧은 시간 내 반복 예약 시도
  • 모바일 앱의 중복 클릭, 네트워크 재전송, 또는 티켓을 구매해 되파는 매크로 사용자
  • 단일 서버 환경에서는 DB 트랜잭션으로도 막을 수 있지만,
  • 멀티 인스턴스 환경에서는 동시성 보장이 어려움

이러한 상황에서는 단순히 DB 트랜잭션으로는 부족하다. 예약 시도 자체를 사전에 차단할 수 있어야 한다. 이를 위해 우리는 Redis + Lua를 사용하여 같은 유저가 같은 스케줄에 대해 10초에 한 번만 예약 시도할 수 있도록 비즈니스 제약을 걸어보았다.


2. Redis + Lua로 구현한 예약 제한 구조

  1. 예약 시도 시, ratelimit:user:{userId}:schedule:{scheduleId} 키를 구성한다.
  2. 해당 키가 존재하면 즉시 실패 응답
  3. 존재하지 않으면 예약 로직을 수행한 뒤, Redis에 키를 저장하고 TTL(10초)을 설정
  4. 이 모든 과정을 Lua 스크립트로 원자적으로 처리

이 방식의 핵심은 예약 전에 이미 제약 조건을 Redis에서 빠르게 판단할 수 있다는 점이다. 또한 key 존재 여부 확인 → SET + TTL 설정까지를 한 번의 명령으로 묶어 race condition도 제거할 수 있다.


3. Lua 스크립트 코드 예시 및 설명

-- KEYS[1] = 예약 제약 키
-- ARGV[1] = 현재 시간 (timestamp)
-- ARGV[2] = 제한 시간 TTL (ms)

local key = KEYS[1]
local exists = redis.call('EXISTS', key)

if exists == 1 then
    return 0
else
    redis.call('SET', key, '1', 'PX', ARGV[2])
    return 1
end
  • EXISTS는 키 존재 여부를 체크
  • 존재하지 않으면 SET + PX를 이용해 제한 키를 저장 (PX = milliseconds)
  • 결과적으로 Redis에는 TTL이 걸린 키가 생성되어 재시도 불가


4. 실전 테스트: K6로 10초 내 중복 요청 검증

import http from 'k6/http';
import { sleep } from 'k6';

export default function () {
    const payload = JSON.stringify({ scheduleId: 1001, seatNumber: 'A3' });
    const headers = { 'Content-Type': 'application/json', 'x-user-id': '1' };
    const res = http.post('http://localhost:8080/api/v4/reservations', payload, { headers });
    console.log(`응답 코드: ${res.status}`);
    sleep(5);
}
  • 첫 번째 요청은 예약 성공 → 응답 200 OK
  • 두 번째 요청은 10초 내 재시도 → 429 Too Many Requests
  • Redis 키:
127.0.0.1:6379> KEYS ratelimit:user:*:schedule:*
"ratelimit:user:1:schedule:1001"

TTL 확인:

127.0.0.1:6379> TTL ratelimit:user:1:schedule:1001
(integer) 7

이렇게 Redis에 저장된 키가 10초 후 자동 삭제되기 때문에, 이후에는 재예약 시도가 가능해진다.

0개의 댓글