Redis로 대기열 시스템 성능 개선 (pub/sub)

smj_716·2025년 8월 7일

한이음 드림업

목록 보기
8/9

수강신청 시스템에서 가장 까다로운 부분은 단순히 대기열을 유지하는 것이 아니라
"입장이 허용된 사용자가 실제로 로그인까지 완료했는지"를 추적하고 "빈자리가 생겼을 때 누굴 입장시킬지" 정확히 판단하는 것이다.

1. 기존 구조: Map 기반 관리의 한계

초기에는 서버 내부에서 아래와 같이 관리했다.

  • 대기열: List<QueueUser> 형태로 메모리에 저장
  • 입장한 사용자: Map<Long studentId, Long timestamp> 으로 추적
  • 빈자리 발생 시: 수강신청 서버가 대기열 서버에 HTTP 요청(/notify)을 보내 ALLOWED 처리를 요청

이 구조는 간단하지만 다음과 같은 명확한 한계가 있었다:

문제점설명
❌ 정합성사용자가 ALLOWED 메시지를 받은 후 실제 로그인하지 않는 경우, 해당 자리는 낭비되며 시스템은 이를 인지할 수 없었다.
❌ 수동 관리미입장 사용자에 대한 회수를 위해 BatchManager나 스케줄러가 필요했고 관리가 번거로웠다.
❌ 강한 결합수강신청 서버가 대기열 서버의 주소와 API 경로(/notify)를 알아야 했고 HTTP 연결 실패 시 장애가 전파될 위험이 있었다.

2. Redis 구조 전환

이러한 문제를 해결하기 위해 시스템은 전체 구조를 Redis 중심으로 재설계했다.
단순한 캐시 용도가 아닌, 실시간 데이터 처리, 자동 만료, 비동기 메시지 전송까지 담당하는 핵심 인프라로 동작하게 되었다.

✅ Redis 구조 요약

목적Redis 구조설명
대기열 순서 관리ListRedis의 List에 사용자 토큰을 RPUSH 하고, 입장 시에는 LPOP으로 제거
빈자리 알림Pub/Sub 채널수강신청 서버에서 PUBLISH로 알림을 보내고, 대기열 서버는 해당 채널을 SUBSCRIBE하여 실시간 메시지를 수신|
인증 사용자 세션Key-Value + TTL사용자가 로그인하면 session:{studentId} 키를 TTL과 함께 Redis에 저장하고, 일정 시간이 지나면 자동으로 삭제되도록 구성
인증 사용자 수 집계Set인증된 사용자 키의 개수를 기반으로 Redis에서 빠르게 입장 인원을 파악

💡 구조 개선 효과

  • 입장 후 로그인 여부를 시스템이 자동으로 판단할 수 있다.
    TTL이 설정된 키를 통해 사용자가 로그인하지 않으면 자동으로 자리가 회수되고 별도의 관리 로직이 필요 없다.

  • 빈자리 발생 시 대기열 서버로 정확하게 알릴 수 있게 되었다.
    기존에는 HTTP 요청으로 수동 알림을 보냈지만, 이제는 Redis Pub/Sub을 사용하여 수강신청 서버가 메시지를 발행하면
    대기열 서버는 이를 구독 중인 상태에서 자동으로 수신하여 실시간으로 반응할 수 있게 되었다.

    👉 서버 간 강한 의존 없이 느슨하게 연결되는 구조(Low Coupling)로 바뀌었다.

  • 순번 계산이 간단하다.
    LPOS, LRANGE 등을 통해 Redis 내부에서 순번을 계산할 수 있어서 별도의 인덱스 관리가 필요 없기에 대기 순번도 자동으로 이동된다.

  • BatchManager가 필요 없게 되었다.
    기존에는 미입장 사용자를 수동으로 관리하기 위해 별도의 로직이 존재하였지만, Redis TTL 구조로 자동 만료가 가능해지면서 이 기능을 제거할 수 있게 되었다.


3. 시스템 흐름

➡️ 대기열 접속 시 해당 사용자의 토큰을 Redis List에 저장

redisTemplate.opsForList().rightPush("queue:waiting", token);

➡️ 사용자에게 SSE를 통해 아래와 같은 메시지를 전송

{
  "queueNumber": 125,  //대기순번
  "status": "WAITING"
}

➡️ 빈자리 발생 → 입장 로직
수강신청 서버에서 로그아웃, TTL 만료 등 변화 발생 시

stringRedisTemplate.convertAndSend("entrance-channel", String.valueOf(currentSessionCount));

대기열 서버는 entrance-channel을 구독 중이며 메시지를 수신하면 다음 순서로 로직을 수행한다:

  • Redis에서 LPOP으로 입장 대상 토큰들을 꺼냄
  • 꺼낸 토큰에 대해 SSE로 status:ALLOWED 전송
  • Redis에 session:{studentId}로 TTL 기반 세션 저장
stringRedisTemplate.opsForValue().set(
  "session:" + studentId,
  "active",
  SESSION_TTL,
  TimeUnit.MILLISECONDS
);

TTL이 지나면 자동 삭제되어 별도 정리 필요 없음 → 빈자리 발생으로 간주 가능
또한 수강신청 서버는 현재 인증된 사용자 수를 Redis에서 즉시 조회 가능


Redis 도입은 단순한 성능 향상이 아닌
정확하게 입장시킬 사람을 판단하고, 로그인하지 않은 사용자의 자리를 시스템이 자동으로 회수할 수 있도록 만든 구조 개편이었다.

또한 Pub/Sub 구조 도입을 통해
수강신청 서버와 대기열 서버 간의 의존성을 줄이고 실시간 메시지 전송을 안정적으로 처리할 수 있게 되었다.

“입장”을 판단하고
“알리는 방식”까지 모두 개선함으로써
수강신청 시스템의 실시간성, 정합성, 확장성이 모두 확보되었다.

0개의 댓글