목차10개 항목
출처와 참고
G마켓 기술 블로그의 「지마켓 대기열 시스템 파헤치기」(2022.09)를 바탕으로 재구성하고, Cloudflare Waiting Room 문서와 Redis 문서로 일반 원리를 보탠 노트다.
가상 대기실이란
가상 대기실(virtual waiting room) 은 서비스가 감당할 수 있는 양보다 많은 사용자가 한꺼번에 몰릴 때, 넘치는 사용자를 대기 화면에 세워 두고 정해진 속도로만 들여보내는 장치다. 대형 할인 행사, 티켓 예매, 한정 상품 판매처럼 트래픽이 짧고 뾰족하게 솟는 서비스에 쓴다.
| 대응 | 문제 |
|---|---|
| 서버 증설(scale-out) | 비용이 크다. 피크가 지나면 늘린 자원은 대부분 논다. DB·외부 결제처럼 늘릴 수 없는 구간이 있으면 효과가 없다 |
| 레이트 리밋(rate limit) | 한도를 넘은 요청을 거절한다. 사용자는 오류를 보고 새로고침을 반복해 부하가 더 커진다 |
| 가상 대기실 | 요청을 버리지 않고 순서대로 줄 세운다. 뒷단의 유입량은 일정하게 유지되고, 사용자는 순번을 보며 기다린다 |
가상 대기실은 처리량을 늘리는 대신 유입량을 조절한다. 뒷단은 평소 용량 그대로 두고 앞단에서 흐름을 깎는다.
구성 요소
| 구성 요소 | 역할 |
|---|---|
| 대기 페이지(프론트엔드) | 순번·예상 대기 시간을 보여 주고 주기적으로 상태를 다시 묻는다 |
| 대기열 API | 진입 등록, 순번 조회, 입장 가능 여부 판정 |
| 이동 작업(job) | 주기적으로 대기 중인 사용자를 정해진 수만큼 입장 상태로 옮긴다 |
| 저장소 | 순번을 빠르게 매기고 조회할 인메모리 저장소(Redis 등) |
| 관리 도구 | 대기열을 켜고 끄고, 초당 유입량을 실시간으로 바꾼다 |
| 모니터링 | 대기·입장·종료 인원 추이, 대상 페이지 트래픽 |
대기열 API는 짧은 요청을 아주 많이 받는다. 그래서 논블로킹 I/O 서버(Node.js 등)로 만들고 컨테이너로 띄워 행사 규모에 맞게 늘린다. 대기열 자체가 가벼워야 뒷단 대신 부하를 받아 낼 수 있다.
왜 Redis Sorted Set인가
Sorted Set은 멤버마다 점수(score) 를 붙여 점수 순으로 정렬된 상태를 유지하는 자료구조다. 대기열에 그대로 대응된다.
| 대기열 개념 | Sorted Set |
|---|---|
| 줄 | 키 하나 (waiting) |
| 사용자 | 멤버 (사용자 ID + 대상 상품 등) |
| 도착 순서 | 점수 = 진입 시각(timestamp) |
| 내 순번 | ZRANK: O(log N) |
| 앞에서 N명 입장 | ZRANGE 0 N-1로 읽고 ZREMRANGEBYRANK로 지우거나, ZPOPMIN으로 한 번에 꺼낸다 |
- RDB에서 매번
ORDER BY로 순번을 세는 것보다 훨씬 빠르다. 추가·순위 조회가 모두 O(log N)이다. - 같은 사용자가 다시 들어와도 멤버가 같으면 중복되지 않는다.
ZADD NX를 쓰면 처음 진입 시각을 덮어쓰지 않는다. - 점수가 같으면 멤버의 사전 순으로 정렬된다. 동시각 도착자의 순서가 문제 되면 점수에 일련번호를 섞는다.
Kafka 같은 메시지 큐는 대기열 저장소로 잘 맞지 않는다. 메시지 큐는 소비자가 꺼내 가는 비동기 처리용이라 「지금 내가 몇 번째인가」를 즉시 답하기 어렵다. 가상 대기실은 순번을 실시간으로 보여 줘야 한다.
동작 흐름
1. 진입: 대기가 필요한가
flowchart TB
u["사용자"] --> pg["상품 페이지 요청"]
pg --> q{"대기열 API: 이 상품에 대기열이 켜져 있나?"}
q -->|"켜져 있지 않음"| agg["대상 트래픽을 집계"]
agg -->|"임계치 미만"| pass["바로 통과"]
agg -->|"임계치 이상"| act["대기열 자동 활성화"]
q -->|"켜져 있음"| zadd["ZADD waiting #lt;now#gt; #lt;userId:itemId#gt;"]
zadd --> zrank["ZRANK로 순번 확인"] --> wait["대기 페이지로 보낸다"]대기열을 미리 켜 두는 수동 방식과, 대상(상품·이벤트)별 트래픽을 세다가 임계치를 넘으면 스스로 켜는 자동 방식을 함께 둔다. 예고 없이 터지는 인기 상품은 자동 방식이 잡는다.
2. 대기: 계속 기다려야 하나
대기 페이지는 일정 간격으로 대기열 API에 상태를 묻는다. 대기열이 꺼졌거나 내 멤버가 입장 상태로 옮겨졌으면 원래 페이지로 보낸다.
3. 이동: 주기 작업이 줄을 줄인다
상태를 키 세 개로 나눈다.
| 키 | 담는 사용자 |
|---|---|
waiting | 줄을 선 사용자 |
running | 대기열을 통과해 서비스를 쓰는 중인 사용자 |
finished | 통과 후 세션이 끝난 사용자 |
매 주기마다:
할당량 = 초당 유입량 ÷ 동시에 도는 작업 수
members = ZRANGE waiting 0 (할당량-1)
ZADD running <now> members...
ZREMRANGEBYRANK waiting 0 (할당량-1)
(별도 작업) 일정 시간이 지난 running → finished작업을 여러 개 돌릴 때는 전체 유입량을 작업 수로 나눠 나눠 맡긴다. 관리 도구에서 초당 유입량을 바꾸면 다음 주기부터 바로 반영된다. 세 키의 크기 추이가 곧 모니터링 지표가 된다.
관리형 서비스의 기준
CDN이 대기실을 기능으로 제공하기도 한다. Cloudflare Waiting Room이 쓰는 기준이 직접 설계할 때도 참고가 된다.
| 설정 | 뜻 |
|---|---|
| 총 활성 사용자(total active users) | 대상 페이지에 동시에 머물 수 있는 목표 인원 |
| 분당 신규 사용자(new users per minute) | 새로 들여보내는 최대 속도 |
| 세션 시간(session duration) | 마지막 요청 뒤 몇 분까지 활성 사용자로 셀지 |
- 방문자마다 쿠키를 발급해 줄의 위치와 입장 여부를 추적한다. 입장한 사용자는 세션 시간 안이라면 나갔다 와도 다시 줄 서지 않는다.
- 판정을 가장 가까운 엣지에서 하므로 원 서버(origin)에는 들여보낸 요청만 닿는다.
입장 증표를 쿠키나 서명된 토큰에 담아 두면, 입장 후 요청마다 저장소를 조회하지 않아도 된다.
설계 시 주의점
| 주의점 | 이유 |
|---|---|
| 대기열이 단일 장애점이 된다 | 대기열이 죽으면 서비스 입구가 막힌다. 저장소 복제와 장애 시 우회 정책(전부 통과 또는 전부 대기)을 정해 둔다 |
| 우회 차단 | 대상 페이지가 입장 증표 없이도 열리면 대기열이 무의미하다. 뒷단에서도 증표를 확인한다 |
| 공정성과 봇 | 먼저 온 순서(FIFO)는 매크로에 유리하다. 봇 차단, 추첨형 입장을 함께 고려한다 |
| 대기 경험 | 순번과 예상 시간을 정직하게 보여 준다. 새로고침해도 순번이 유지돼야 이탈과 재진입 폭주가 줄어든다 |
| 폴링 부하 | 대기 인원이 많으면 상태 조회만으로도 부하가 된다. 조회 간격을 두고, 순번이 멀수록 간격을 늘린다 |
정리
- 가상 대기실은 피크 트래픽을 증설이 아니라 유입량 제어로 다룬다.
- Redis Sorted Set에 도착 시각을 점수로 넣으면 순번 조회와 앞에서 M명 꺼내기가 각각 O(log N), O(log N + M)으로 끝난다.
- 대기 → 입장 → 종료 상태를 나누고, 주기 작업이 정해진 유입량만큼 옮긴다. 유입량은 운영 중에 바꿀 수 있어야 한다.
- 트래픽 임계치로 대기열을 자동으로 켜면 예고 없는 폭주도 막는다.
- 대기열 자체의 가용성, 우회 차단, 공정성을 함께 설계한다.
관련 노트: 큐와 덱, API 보안 체크리스트, 쿠버네티스 위 웹 서비스의 요청 경로와 구성 요소, 웹 확장성의 구성 요소