최근 이벤트 항공권을 예약하려다 보니, 항공권을 검색하자마자 대기열 페이지로 안내되었습니다. 한참을 기다리면서 "이 시스템은 어떻게 구현되어 있을까?"라는 궁금증이 생겨서 찾아보게 되었습니다.
타 예약 시스템의 대기열과 입장 시간에 비해 항공권 예약시 대기 시간이 매우 길었는데, 그것에 대한 고민도 포함되어 있습니다.
(저는 4월에 도쿄에 갑니다. 땡큐 진마켓)
항공권 이벤트, 콘서트 티켓 예매, 인기 제품 사전 예약 등 동시 접속자가 폭증할 가능성이 있는 서비스는 대기열 시스템(Waiting Queue)을 통해 서버 과부하를 방지하고 공정한 접속 기회를 제공합니다.
이러한 상황에서 대기열 시스템을 통해 사용자 경험을 개선할 수 있습니다.
예를 들어:
대기열 시스템은 다음과 같은 구조로 설계할 수 있습니다:
💡 핵심 포인트:
💡 핵심 포인트:
503 Service Unavailable
응답 반환💡 핵심 포인트:
💡 핵심 포인트:
💡 핵심 포인트:
구성 요소 | 기술 스택 | 설명 |
---|---|---|
애플리케이션 | Spring Boot + Kotlin | API 서버 구현 |
큐 시스템 | Kafka | 대기열 이벤트 관리 |
캐싱 및 순서 관리 | Redis Sorted Set | 접속 순서 관리 및 TTL 설정 |
데이터 저장 | PostgreSQL | 예약 및 사용자 정보 저장 |
배포 인프라 | AWS ECS + ALB | 트래픽 분산 및 확장성 확보 |
보안 | CloudFront + WAF | 봇 차단 및 비정상 접근 방지 |
모니터링 | Grafana & Prometheus | 성능 및 대기열 상태 모니터링 |
Redis Sorted Set은 점수(Score)와 멤버(Member)를 기반으로 순서 보장을 할 수 있어 대기열 구현에 적합합니다.
ZADD waiting_queue <timestamp> <user_token>
ZRANK waiting_queue <user_token>
ZRANGE waiting_queue 0 0
ZREM waiting_queue <user_token>
💡 핵심 포인트:
@Service
class WaitingQueueService(private val redisTemplate: StringRedisTemplate) {
companion object {
private const val QUEUE_KEY = "waiting_queue"
}
// 사용자 대기열 등록
fun joinQueue(userToken: String) {
val currentTime = System.currentTimeMillis().toDouble()
redisTemplate.opsForZSet().add(QUEUE_KEY, userToken, currentTime)
redisTemplate.expire("waiting_queue:$userToken", Duration.ofSeconds(30))
}
// 사용자 순서 조회
fun getPosition(userToken: String): Long {
return redisTemplate.opsForZSet().rank(QUEUE_KEY, userToken)?.plus(1) ?: -1
}
// 다음 사용자 입장
fun dequeueNextUser(): String? {
val nextUser = redisTemplate.opsForZSet().range(QUEUE_KEY, 0, 0)?.firstOrNull()
if (nextUser != null) {
redisTemplate.opsForZSet().remove(QUEUE_KEY, nextUser)
}
return nextUser
}
}
예약 대기 중 페이지 이탈, 인터넷 연결 끊김 또는 새로고침 시 대기열에서 제거해야 합니다.
// 페이지 이탈 시 서버에 사용자 제거 요청
window.addEventListener('beforeunload', () => {
navigator.sendBeacon('/waiting-room/leave', JSON.stringify({ token: userToken }));
});
// 하트비트(heartbeat) 전송으로 연결 유지
setInterval(() => {
fetch('/waiting-room/heartbeat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: userToken })
});
}, 10000); // 10초마다 하트비트 전송
// 사용자 이탈 처리
@PostMapping("/waiting-room/leave")
fun leaveQueue(@RequestBody request: Map<String, String>) {
val token = request["token"] ?: return
redisTemplate.opsForZSet().remove("waiting_queue", token)
println("User $token left the queue")
}
// 하트비트 요청 처리
@PostMapping("/waiting-room/heartbeat")
fun heartbeat(@RequestBody request: Map<String, String>) {
val token = request["token"] ?: return
redisTemplate.expire("waiting_queue:$token", Duration.ofSeconds(30))
println("Heartbeat received from $token")
}
항공권 예약 페이지의 특성상, 사용자는 여러 항공편 옵션을 탐색하느라 페이지에 오랫동안 머무를 가능성이 높습니다. 이 경우, 대기열이 짧아 보이더라도 대기 시간이 길어질 수 있습니다.
(저도 계속해서 티켓을 살펴보다가 결제까지 완료하고 나서야 페이지를 빠져나와서 새로운 대기열에 입장하게 되었거든요)
이번에 대기열 시스템을 살펴보면서, 단순히 사용자를 줄 세우는 것이 아니라 인프라, 트래픽 관리, 실시간 상태 확인, 사용자 경험 개선 등 다양한 기술적 고민이 필요하다는 점을 깨달았습니다.
또한, 사용해보지 않은 Kafka, Redis Sorted Set을 활용하는 방법을 고민해볼 수 있었고, 모니터링이 단순히 이상감지를 위한 것이 아닌 사용자 행동 패턴 분석을 통해 더 나은 시스템을 만들 수 있는 방법이란 것도 인지하게 되었습니다.
이벤트 항공권 예약 대기열은 단순히 기다리기 이상의 세심한 설계와 운영이 필요한 시스템 같습니다.
.
.
.
챗지피티와 함께 고민한 내용을 다시 한번 정리했습니다.