본문 바로가기
Server/Infra

[Redis] 대용량 트래픽에도 동작하는 좌석 예매 시스템 구축 (Lua, Zset)

by promedev 2026. 2. 4.

 

안녕하세요 프로미 입니다.

어느 날 유튜브에서 redis 관련 동영상을 보게 되었는데요,

직접 구현해보고 싶은 생각이 들어 redis를 활용한 좌석 예매 시스템을 구축해 보았습니다!

 

이 글은 Redis를 이용해
“동시에 몰리는 요청을 어떻게 제어할 수 있을까?”를
직접 구현과 부하 테스트로 검증해 본 기록입니다.

- Redis Sorted Set으로 대기열을 어떻게 구성했는지
- polling, 스케줄링 중복 같은 실제 문제를 어떻게 해결했는지

 

를 다루고 있어요

평소에 대용량 트래픽 + 대기열 처리에 관심 많으셨던 분들에게 추천드려요

 

1. 대용량 트래픽에도 동작하는 좌석 예매 시스템 구축

https://youtu.be/c-ERjEodn_o?si=JmjSM1BeHLVzbw9W
- 코딩하는 기술사

 

우연히 “수백만 동시 접속을 처리하는 예매 시스템 아키텍처 설계”

라는 동영상을 보게 되었다.

동영상에서는 Redis를 사용해서 대기열을 처리하는 방법을 설명한다.

 

이전에 대기번호를 나타내기 위해 Kafka 대신 Redis 를 사용한다는 글을 본 적이 있었다. 

Redis는 한 번도 사용해 본 적이 없어서 추상적으로 느껴졌다. 

 

이번 기회에 동영상에 나온 아키텍처를 구축해 보며

Redis를 공부해보고 싶어서 프로젝트를 시작하게 되었다.

 

2. BACKGROUND: 알아야 하는 개념들

Redis, Lua Script

 

Redis - Remote Dictionary Server

키-값 구조의 비정형 데이터를 저장하는 인메모리 데이터베이스다.

 

이번 프로젝트에서는 Redis를 캐시가 아니라
대기열의 단일 진실 소스(Single Source of Truth)로 사용했다.

 

Redis 를 사용하면, 
- Sorted Set으로 대기 순서를 정확히 유지할 수 있다.
- 단일 스레드 + Lua Script로 원자적 처리가 가능하다.

 

Lua Script

원자적으로 실행할 명령들을 Script로 작성한다.

좌석 확인과 소유권 부여 과정을 원자적으로 하기 위해 사용한다.

 

3. PLAN: 좌석 예매 시스템 구상

목표: 터지는 지점(병목)을 확인해 보자

 

핵심 요구사항

  1. 남은 대기 인원을 확인할 수 있다.
  2. 남은 시간을 확인할 수 있다.
  3. 어떤 상황에서도 1명의 사용자만 좌석 예매에 성공해야 한다.

 

유튜브에 나온 아키텍처

유튜브 사진 캡쳐

1. 사용자 요청 -> 대기열 서버: 대기열에 사용자를 넣기 위한 클라이언트 요청 전달
2. 대기열 서버 -> redis 대기열: redis 대기열에 사용자 넣기
3. 스케줄러: 스케줄러를 통해 대기열에 있는 사용자를 예매 가능 상태로 변경
4. 사용자 요청 -> 예매 서버: 예매 가능 상태의 사용자가 예매를 요청
5. 예매 서버 -> redis: 좌석 확인 + 소유권 부여
6. 예매 서버 -> (MQ) -> RDB: 1명의 사용자만 좌석 예매  성공

 

요약하면,
- 대기열 진입과 상태 관리: Redis
- 실제 좌석 소유권 결정: Lua Script
- 최종 저장: DB
로 역할을 분리했다.

 

최대한 간단하게 구성하기

  1. L/B는 제외한다.
  2. 모니터링을 붙여서 부하테스트 결과를 확인한다.
  3. 대기열 서버와 예매 서버는 분리한다. 

LB를 제외한 이유는 최대한 로컬에서 구현하고 싶었기 때문이다.

모니터링으로는

- 컨테이너 정보를 수집하기 위한 cAdvisor,

- 시계열 데이터 수집을 위한 prometheus,

- 결과 확인을 위한 grafana를 이용했다.

 

내가 그린 아키텍처

 

4. IMPLEMENT

자세한 구현은 깃허브 링크 참고

 

https://github.com/seoyeonjin/redis-study

 

GitHub - seoyeonjin/redis-study: 레디스 공부 내용을 정리한 레포입니다 .

레디스 공부 내용을 정리한 레포입니다 . Contribute to seoyeonjin/redis-study development by creating an account on GitHub.

github.com

 

docker-compse 활용

  • Redis, Spring-a, Spring-b, Posgresql, cAdvisor, Prometheus, Grafana, k6 사용

대시보드는 K6 부하테스트 기본 대시보드 사용(19665번)

  • redis 지표보다는 Spring 지표를 확인하는 것을 목적으로 했다.
  • cAdvisor는 참고용으로 사용했다.

 

5. CLIENT TEST

시뮬레이션해보기 ~

 

테스트 시뮬레이션 화면

 

간단한 프론트 화면을 만들어서 테스트해 봤다.

코드의 localhost:8080/index.html 에서 확인할 수 있다.

2명의 사용자가 거의 동시에 요청했을 때, 하나의 요청만 성공한 것을 확인할 수 있다.

 

TTL 설정하기

남은 시간은 예매를 시도할 수 있는 시간을 나타낸다.

300초 동안만 예매를 시도할 수 있고, 이 시간이 넘어가면 다시 줄을 서야 한다.

대기열에서 벗어난 사용자를 처리하기 위함이다.

 

대기열

캡처 화면은 초기 화면이라 대기열은 없지만,

아래 동영상을 보면 대기열도 잘 동작하는 것을 확인할 수 있다.

 

POLLING 방식으로 업데이트하기

상태를 업데이트하기 위해 polling 방식으로 데이터를 가져온다.

polling 주기를 너무 짧게 하면 부하가 커서 1초 정도로 조절했다.

(초기에는 0.3초였지만, 부하가 심해 1초로 조절했다.)

 

6. K6 TEST

로컬에서 도커로 구성하고, K6로 테스트를 진행했다.

 

(1) 테스트환경 구성

점진적으로 유저가 증가하는 상황을 가정했다. 

export const options = {
  stages: [
   { duration: '30s', target: 50 },
   { duration: '60s', target: 70 },
   { duration: '60s', target: 70 },
  ]
};

 

(2)  요청 트래픽 분산

요청 트래픽을 2개의 Spring 서버로 분산했다.

const BASE_URLS = [
  '<http://seat-backend-a:8080>',
  '<http://seat-backend-b:8080>',
];

 

(3) 시나리오

좌석 1에 대해 대기열 입장부터 좌석 선점까지의 과정을 테스트했다.

  • 입장 만료 시간(TTL)은 300초로 설정했다.
  • 입장이 만료됐는지 상태를 1초마다 polling 한다.

결과를 보면,

  • 총 22500 개 요청이 전달되었고, 4675개의 오류가 발생했다.
  • 전체 요청 중에 1명의 사용자만 좌석 선점에 성공했다.
  • 로컬에서 수행하여 지연이나 P99 같은 수치들은 모두 좋게 나왔다.
  • Request Duration은 polling과 TTL의 영향을 많이 받았다.

모니터링 결과 화면 1
모니터링 결과 화면 2
1개의 예매만 성공한 모습

 

 

7. TROUBLE SHOOTING

핫이슈

 

(1) 모든 사용자가 좌석 예매에 실패하는 이슈

초기에 polling 주기를 0.3 초로 설정하고 테스트를 진행했다.

하지만, 모두 좌석 예매를 실패하는 이슈가 있었다.

요청 수도 42481로 높게 나타났다.

테스트 결과 (개선 전)

 

polling 요청 처리로 인해 서버에 부하가 발생했고,

좌석 예매 요청이 TTL 시간 안에 처리되지 못했다.

 

TTL 시간을 300초→ 3600초로 늘려 테스트해 봤다.

그럼에도 모든 좌석 예매가 실패했다.

 

결론적으로 요청이 많이 발생하는 polling 주기를 줄여야겠다고 생각했다.

기존에는 0.3초로 요청하고 있었지만 1초로 주기를 변경했다.

 

k6 결과를 확인했을 때 전체 요청 수가 줄었고(22500),

좌석 예매도 성공한 것을 확인할 수 있었다.

 

테스트 화면 (개선)

 

(2) 스케줄링 여러 번 실행되는 이슈

Spring 컨테이너를 2대 띄워서 분산 서버 환경을 가정하였다.

하지만, 스케줄링이 2대에서 모두 실행되는 이슈가 있었다.

 

2배 빠르게 대기열 처리 스케줄링이 동작하면서 불필요한 부하가 발생했다.

이슈를 해결하기 위해 스케줄러 시작 시점에 한해 Redisson 기반 분산 락을 적용하여,

동일 시점에 하나의 인스턴스만 스케줄링 로직을 수행하도록 제한했다.

 

8. 앞으로 해볼 것&느낀 점

Redis 딥다이브

 

해보고 싶은 것들

MQ도 적용해서 아키텍처 변경해 보기

Redis 분산락 한계 이해해 보기

Lua Script, Redis Zset 동작 깊게 공부해 보기

캐싱 목적으로 Redis 사용해 보기 (캐시 스탬피드 현상 등.. 공부해 보기)

 

...

 

느낀 점

Redis 쓰임이 굉장히 많다는 생각이 들었다.

분산락의 한계에 대해서도 사실 잘 이해를 못 했는데, 추가적인 공부를 통해 딥다이브 해보고 싶다.

제대로 된 부하테스트를 처음 해보면서 다양한 지표를 보려고 했으나 생각보다 지표를 분석하는 게 어려웠다.

지표를 보고 어떤 부분에서 병목이 생기는지 파악할 수 있는 개발자가 되고 싶다.

 

생각했던 것보다 많은 요청을 받지는 못해서 아쉽기도 했다. 

인덱스, 비동기 처리, MQ 등을 통해 성능을 개선해보고 싶다. 

 

Ref

https://mangkyu.tistory.com/311

https://mangkyu.tistory.com/420

https://www.youtube.com/watch?v=a4yX7RUgTxI

https://www.youtube.com/watch?v=c-ERjEodn_o

https://grafana.com/grafana/dashboards/19665-k6-prometheus/

https://grafana.com/docs/k6/latest/results-output/real-time/grafana-cloud-prometheus/#visualize-test-results

 

*잘못된 내용은 댓글로 알려주시면 글에 반영하도록 하겠습니다. 감사합니다.