✈️ 항공권 이벤트의 예약 시스템은 어떻게 만들어져 있을까

yesjm·2025년 2월 21일
0

SpringBoot/Kotlin

목록 보기
7/7

최근 이벤트 항공권을 예약하려다 보니, 항공권을 검색하자마자 대기열 페이지로 안내되었습니다. 한참을 기다리면서 "이 시스템은 어떻게 구현되어 있을까?"라는 궁금증이 생겨서 찾아보게 되었습니다.
타 예약 시스템의 대기열과 입장 시간에 비해 항공권 예약시 대기 시간이 매우 길었는데, 그것에 대한 고민도 포함되어 있습니다.

(저는 4월에 도쿄에 갑니다. 땡큐 진마켓)


🧩 대기열 시스템이 필요한 이유

항공권 이벤트, 콘서트 티켓 예매, 인기 제품 사전 예약 등 동시 접속자가 폭증할 가능성이 있는 서비스는 대기열 시스템(Waiting Queue)을 통해 서버 과부하를 방지하고 공정한 접속 기회를 제공합니다.

대기열이 필요한 상황

  • 이벤트 항공권 예약: 특정 시간에 수만 명이 동시에 접속
  • 인기 콘서트 티켓 예매: 서버 과부하로 인한 장애 발생 가능성
  • 인기 제품 사전 예약(아이폰, 한정판 스니커즈 등)

이러한 상황에서 대기열 시스템을 통해 사용자 경험을 개선할 수 있습니다.

예를 들어:

  • 이벤트 항공권 예약 페이지에 수만 명이 동시에 접속할 경우, 서버 부하로 페이지가 멈출 수 있음
  • 대기열 시스템을 통해 순차 입장을 유도하여 안정적 예약 진행

🛠️ 대기열 시스템 아키텍처 설계

대기열 시스템은 다음과 같은 구조로 설계할 수 있습니다:

1. 접속 관리 (Access Management)

  • 사용자가 특정 페이지에 접속하면 토큰 발급
  • 사용자에게 현재 대기열에서 순서 및 예상 대기 시간 표시
  • Redis Sorted Set을 사용하여 접속 시점 기준 순서 관리
  • IP 제한이나 쿠키 기반 세션 관리로 중복 접속 방지

💡 핵심 포인트:

  • 동일 사용자가 여러 번 접속 시 하나의 토큰만 유지
  • 악의적인 트래픽(봇, 매크로)을 식별하여 차단

2. 트래픽 제어 (Traffic Control)

  • CloudFront + WAF로 비정상 접근 및 봇 트래픽 차단
  • Rate Limiting(속도 제한) 적용으로 동시 접속 수 제한
  • 사용자 Agent 분석 및 ReCAPTCHA 적용을 통해 자동화 접근 방지

💡 핵심 포인트:

  • 트래픽 초과 시 503 Service Unavailable 응답 반환
  • 봇 탐지 로직을 로그 기반으로 지속 개선

3. 대기열 관리 (Queue Management)

  • Kafka를 사용하여 대기열 이벤트 관리
  • Redis Sorted Set에 사용자 등록 및 하트비트(heartbeat) 기반 연결 유지
  • 사용자의 대기 상태를 주기적으로 클라이언트에 전달

💡 핵심 포인트:

  • Kafka를 통해 이벤트 순서 보장 및 확장성 확보
  • Redis에서 TTL 설정으로 유령 세션 제거

4. 예약 페이지 입장 (Reservation Access)

  • 대기열 최상단 사용자가 순서가 되면 1회성 JWT 토큰 발급
  • 토큰 만료 시 입장 불가로 설정하여 공정성 확보
  • 사용자가 입장 후 일정 시간 내에 행동하지 않으면 강제 퇴장

💡 핵심 포인트:

  • JWT 발급 시 IP 바인딩 적용
  • 예약 페이지는 1회성 접근 토큰으로만 접근 가능

5. 데이터 저장 및 모니터링 (Data Management & Monitoring)

  • DB 에 예약 정보 저장
  • Kafka Consumer로 대기열 상태를 실시간 모니터링
  • Grafana 및 Prometheus를 활용해 트래픽 패턴 분석

💡 핵심 포인트:

  • 대기열 이벤트(입장, 이탈, 실패 등) 로깅
  • 사용자 행동 패턴 분석으로 대기열 개선

⚙️ 기술 스택 및 구성 요소

구성 요소기술 스택설명
애플리케이션Spring Boot + KotlinAPI 서버 구현
큐 시스템Kafka대기열 이벤트 관리
캐싱 및 순서 관리Redis Sorted Set접속 순서 관리 및 TTL 설정
데이터 저장PostgreSQL예약 및 사용자 정보 저장
배포 인프라AWS ECS + ALB트래픽 분산 및 확장성 확보
보안CloudFront + WAF봇 차단 및 비정상 접근 방지
모니터링Grafana & Prometheus성능 및 대기열 상태 모니터링

🔑 대기열 관리 로직: Redis Sorted Set 활용

Redis Sorted Set은 점수(Score)와 멤버(Member)를 기반으로 순서 보장을 할 수 있어 대기열 구현에 적합합니다.

주요 명령어

  • ZADD: 사용자 추가 → ZADD waiting_queue <timestamp> <user_token>
  • ZRANK: 사용자의 현재 순서 조회 → ZRANK waiting_queue <user_token>
  • ZRANGE: 대기열 최상단 사용자 가져오기 → ZRANGE waiting_queue 0 0
  • ZREM: 대기열에서 사용자 제거 → ZREM waiting_queue <user_token>

💡 핵심 포인트:

  • 사용자 별 TTL(Time To Live) 설정(예: 30초)
  • 하트비트(heartbeat) 요청이 없으면 자동 만료 처리

Kotlin 코드 예제: 대기열 서비스

@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
    }
}

👀 사용자 이탈 및 연결 끊김 감지

예약 대기 중 페이지 이탈, 인터넷 연결 끊김 또는 새로고침 시 대기열에서 제거해야 합니다.

클라이언트 측 이벤트 감지 (JavaScript)

// 페이지 이탈 시 서버에 사용자 제거 요청
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")
}

🔍 항공권 예약 시 대기열이 길어지는 이유: 낮은 페이지 이탈률

항공권 예약 페이지의 특성상, 사용자는 여러 항공편 옵션을 탐색하느라 페이지에 오랫동안 머무를 가능성이 높습니다. 이 경우, 대기열이 짧아 보이더라도 대기 시간이 길어질 수 있습니다.
(저도 계속해서 티켓을 살펴보다가 결제까지 완료하고 나서야 페이지를 빠져나와서 새로운 대기열에 입장하게 되었거든요)

이와 관련된 주요 원인

  • 사용자가 다양한 항공편 검색을 반복하며 페이지를 떠나지 않음
  • 결제 단계에서 항공편 선택 및 예약 완료에 시간이 소요
  • 탐색 중에도 페이지를 갱신하지 않고 장시간 머무름

해결 방법

  1. 탐색 시간 제한: 페이지에 세션 타임아웃(예: 20분) 설정
  2. 탐색 중에도 대기열 갱신: 하트비트 주기를 단축하여 장기 접속 사용자 파악
  3. 탐색 세션 구분: 탐색 세션과 예약 세션을 분리하여, 탐색 사용자는 대기열에 포함하지 않도록 개선
  4. 사용자 행동 분석: 페이지 탐색 패턴을 분석해 불필요한 장기 체류 방지

결론

이번에 대기열 시스템을 살펴보면서, 단순히 사용자를 줄 세우는 것이 아니라 인프라, 트래픽 관리, 실시간 상태 확인, 사용자 경험 개선 등 다양한 기술적 고민이 필요하다는 점을 깨달았습니다.

또한, 사용해보지 않은 Kafka, Redis Sorted Set을 활용하는 방법을 고민해볼 수 있었고, 모니터링이 단순히 이상감지를 위한 것이 아닌 사용자 행동 패턴 분석을 통해 더 나은 시스템을 만들 수 있는 방법이란 것도 인지하게 되었습니다.

이벤트 항공권 예약 대기열은 단순히 기다리기 이상의 세심한 설계와 운영이 필요한 시스템 같습니다.

.
.
.

챗지피티와 함께 고민한 내용을 다시 한번 정리했습니다.

profile
yesjm's second brain

0개의 댓글