⏰ 선착순 강의 대규모 트래픽 처리 – Redis 1차 설계

2026. 2. 14. 23:02·프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트

1️⃣ 왜 이걸 만들었는가? (문제 정의)

내가 만든 “선착순 강의 신청” 기능은 단순 CRUD가 아니라, 본질적으로 이런 문제였다.

  • 짧은 시간에 다수 사용자가 동시에 “신청” 버튼 클릭
  • 요청 순서를 공정하게 보장해야 함
  • 동시에 처리하면 DB에 락/경합 발생 가능
  • 사용자는 “내가 신청되었는지”를 계속 확인함 (새로고침/폴링)

즉,

신청 로직을 DB 트랜잭션에 그대로 몰아넣으면 병목이 생길 수 있다.

그래서 해결 방향을 이렇게 잡았다.

  • DB는 최종 확정만 담당
  • “줄서기”는 Redis가 담당
  • 빠른 인메모리 자료구조로 병목을 분리

2️⃣ 설계에서 가장 많이 고민한 부분

(1) 순서 보장을 어떻게 할 것인가?

선착순의 핵심은:

“누가 먼저 왔는지”를 정렬된 상태로 유지하는 것

그래서 Redis의 **ZSET(Sorted Set)**을 사용했다.

key: queue:lecture:{lectureId}
score: 요청 시간(ms)
member: userKey
  • score = 현재 시간(ms)
  • 먼저 요청한 사람이 score가 작음
  • 자동으로 정렬 유지

👉 별도의 정렬 로직 없이 Redis가 순서를 보장

(2) 사용자의 “내 상태 조회”는 어떻게 처리할까?

사용자는:

  • 새로고침
  • 탭 이동
  • 중복 요청
  • 반복 폴링

이걸 매번 DB에서 계산하면 비용이 커진다.

그래서 Redis에서 바로 순번 조회.

ZRANK
  • ZSET에서 내 현재 rank 반환
  • O(logN) 성능
  • 매우 빠름

(3) “입장 성공”은 어떻게 확정할 것인가? (핵심 설계 포인트)

여기서 가장 큰 고민이 있었다.

스케줄러가 대기열에서 사용자를 pop하면
그 사용자는 ZSET에서 사라진다.

그런데:

ZSET에서 사라졌다고 무조건 성공은 아니다.

가능한 상황:

  • 네트워크 단절
  • 잘못된 userKey
  • 처리 중 예외
  • TTL 만료

그래서 결론은:

“대기열”과 “성공 확정”을 분리하자.

최종 설계

  • queue:lecture:{id} → ZSET (대기 중)
  • admitted:lecture:{id} → SET (입장 확정자)

이렇게 2단계로 분리했다.

(4) Redis 메모리 누적 문제

admitted를 영원히 저장하면 메모리가 누적된다.

해결:

TTL 10분
  • 입장 성공 기록은 일정 시간 후 자동 삭제
  • 메모리 안전성 확보

3️⃣ 전체 아키텍처 흐름

[대기열 ZSET] ──(Scheduler)──▶ [입장권 SET] ──▶ 결제 가능

구성 요소

✅ DB (JPA)

  • Lecture: 강의 정보
  • 정원, 오픈 시간 등 저장

✅ Redis

  • ZSET: 대기열
  • SET: 입장 성공자 기록

✅ Scheduler

  • 3초마다 대기열 앞에서 N명 pop
  • admitted에 기록

✅ Polling API

  • 프론트에서 주기적으로 상태 조회

4️⃣ 요청 시나리오 흐름

  1. 사용자가 “대기열 참가”
  2. 서버는 UUID로 userKey 생성
  3. Redis ZSET에 추가
  4. 프론트는 userKey 저장 후 폴링 시작
  5. 스케줄러가 주기적으로 N명 pop
  6. pop된 user는 admitted SET에 기록
  7. 폴링 시:
    • ZSET에 있으면 → QUEUED
    • ZSET엔 없고 admitted에 있으면 → SUCCESS
    • 둘 다 없으면 → NOT_IN_QUEUE

5️⃣ 구현 디테일

5 - 1 대기열 등록

public Long enqueue(Long lectureId, String userKey) {
    long score = Instant.now().toEpochMilli();
    redisTemplate.opsForZSet().add(key(lectureId), userKey, score);
    Long rank = redisTemplate.opsForZSet().rank(key(lectureId), userKey);
    return rank != null ? rank + 1 : null;
}

핵심 포인트

  • score = 현재 시간
  • 자동 정렬
  • rank 즉시 반환
  • 0-based → +1 처리

5 - 2 폴링 조회

Long position = queueService.getPosition(lectureId, userKey);

if (position != null) {
    status = QUEUED;
} else if (queueService.isAdmitted(lectureId, userKey)) {
    status = SUCCESS;
} else {
    status = NOT_IN_QUEUE;
}

설계 장점

  • 상태 판별이 명확
  • 새로고침에도 성공 여부 유지
  • 로직이 단순하고 예측 가능

5 - 3 스케줄러 입장 처리

@Scheduled(fixedDelay = 3000)
public void processQueue() {
    Set<String> admitted = queueService.popFront(lectureId, batchSize);
    if (!admitted.isEmpty()) {
        queueService.markAdmitted(lectureId, admitted);
    }
}
public Set<String> popFront(Long lectureId, int n) {
    return redisTemplate.opsForZSet()
        .popMin(key(lectureId), n)
        .stream()
        .map(tuple -> tuple.getValue())
        .collect(toSet());
}

중요한 점

  • ZPOPMIN은 조회 + 삭제가 원자적
  • 중복 입장 방지
  • “조회 후 삭제”보다 안전

5 - 4 입장 확정 기록

public void markAdmitted(Long lectureId, Set<String> userKeys) {
    redisTemplate.opsForSet()
        .add(admittedKey(lectureId), userKeys.toArray(new String[0]));
    redisTemplate.expire(admittedKey(lectureId), Duration.ofMinutes(10));
}

왜 SET인가?

  • 순서 필요 없음
  • 존재 여부만 빠르게 확인하면 됨
  • O(1) 조회

6️⃣ 이 설계의 핵심 가치

① DB 병목 분리

  • 줄서기는 Redis
  • 최종 정원 체크는 DB

트래픽이 몰려도 DB 락을 줄일 수 있다.

② 순서 공정성 보장

  • timestamp 기반 정렬
  • Redis가 자동 관리

③ 상태 판별 안정성

  • queue와 admitted 분리
  • 폴링 시 상태 흔들림 없음

④ 원자적 pop

  • 중복 입장 위험 감소
  • 동시성에 강함

7️⃣ 이 설계의 한계 (솔직한 정리)

이 구조는 1차 구조다.

아직:

  • 강의 정원과 Redis pop 수를 완전히 동기화하진 않음
  • Redis 장애 시 대안 구조 필요
  • 분산 환경에서 다중 인스턴스 스케줄러 처리 고려 필요

하지만 1차 목표였던:

대규모 동시 요청에서 DB 병목을 줄이고
공정한 대기열을 만드는 것

은 성공적으로 달성했다.

'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글

⏰ 선착순 강의 대규모 트래픽 처리 – Redis 2차 설계  (0) 2026.02.15
⏰🚨 신청 버튼을 누르지도 않았는데 자동으로 신청되는 문제 (Redis 대기열 + localStorage 이슈)  (0) 2026.02.14
👥🚨 좋아요 상태 변경 후 본인 게시글 삭제 오류 해결 (FK 제약 & DB CASCADE)  (0) 2026.02.14
👥💡 좋아요(Like) 기능 설계: 상태, 집계, 동시성까지 고려한 구현 정리  (0) 2026.02.14
👥🚨 같은 유저가 좋아요를 동시에 연타할 때 동시성 테스트가 깨지는 이유  (0) 2026.02.14
'프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
  • ⏰ 선착순 강의 대규모 트래픽 처리 – Redis 2차 설계
  • ⏰🚨 신청 버튼을 누르지도 않았는데 자동으로 신청되는 문제 (Redis 대기열 + localStorage 이슈)
  • 👥🚨 좋아요 상태 변경 후 본인 게시글 삭제 오류 해결 (FK 제약 & DB CASCADE)
  • 👥💡 좋아요(Like) 기능 설계: 상태, 집계, 동시성까지 고려한 구현 정리
hak0622
hak0622
개발하면서 “이게 뭐지?”라는 순간마다 궁금한 점을 바탕으로 정리한 개발 블로그입니다.
  • hak0622
    궁금한 개발 이야기 Why?
    hak0622
  • 전체
    오늘
    어제
    • 분류 전체보기 (68)
      • 공부 (36)
        • 1. 자바 ORM 표준 JPA 프로그래밍 - 기본.. (35)
        • 시험 (1)
      • 프로젝트 (32)
        • 스프링 부트 3 백엔드 개발자 되기 Blog + .. (32)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hak0622
⏰ 선착순 강의 대규모 트래픽 처리 – Redis 1차 설계
상단으로

티스토리툴바