본문 바로가기

Programming/프로그래밍 내용 정리

대규모 트래픽 티켓팅 시스템 설계를 해보다.

 

이번 글에서는 대규모 트래픽이 발생할 수 있는

티켓팅 시스템을 어떻게 설계할지 고민해보고

최종적으로 정리한 아키텍처 구성을 단계적으로 살펴보려고 한다.

 

 


 

📐 전체 아키텍처

 

아래는 설계해 본 티켓팅 시스템 전체 아키텍처를 그려본 것이다.

 

 

 

 


📋 요구사항 정의

 

1️⃣ 계정당 구매할 수 있는 티켓 수량 제한

  • R석과 S석을 포함하여 총 5장까지만 구매 가능해야 한다.

2️⃣ 보유하고 있는 티켓 수량 초과 판매 방지

  • 실제 보유하고 있는 티켓보다 더 많은 티켓이 판매되지 않도록 관리해야 한다.

3️⃣ 초당 최소 5,000장의 티켓 구매 요청 처리

  • 사용자가 몰리는 상황에서도 초당 최소 5,000장의 티켓 구매 요청을 처리할 수 있어야 한다.

 


🎯 설계 방향

 

티켓팅 시스템은 짧은 시간에 대규모 요청이 폭주하는 상황에서

안정적이고 빠르게 처리하는 것이 핵심이다.

특히, 다음과 같은 부분에 집중해서 설계를 진행하려고 한다.

 

  • 대기열 관리
  • 좌석 선택 및 결제 프로세스 관리
  • 실시간 상태 업데이트

 

 


1) 티켓팅 상태 조회

 

 

 

 

티켓팅 시스템에서는

티켓을 구매하려는 사용자의 현재 대기 순번이나

구매한 티켓 수 등을 실시간으로 조회하고

업데이트하는 기능이 필요하다.

 

이를 위해 클라이언트와 서버 간의 실시간 연결이 중요하다.

 

연결 방식은 WebSocket 방식과 Polling 방식을 생각해 보았다.

 

1-1) WebSocket 방식 vs Polling 방식

📊 Polling 방식 vs WebSocket 방식 비교표

구분 Polling 방식 WebSocket 방식
요청 방식 클라이언트가 주기적으로 서버에 요청 서버가 상태 변경 시 클라이언트에 푸시
통신 방향 단방향 (클라이언트 → 서버 → 클라이언트) 양방향 (서버 ↔ 클라이언트)
서버 부하 높음 낮음
실시간성 부족함 실시간 알림 가능

 

 

❌ Polling 의 문제점

 

- 클라이언트가 일정 주기(예: 5초마다)로 서버에 상태를 요청하는 방식이다.

- 대규모 트래픽 발생 시, 서버에 심각한 부하가 걸릴 수 있다.

- 예를 들어, 초당 5,000건 이상의 요청이 몰리면 서버의 처리 용량을 초과할 위험이 크다.

- 이로 인해 서버가 느려지거나, 심한 경우 장애가 발생할 수 있다.

 

 

 

✅ WebSocket 방식 특징

- WebSocket은 한 번 연결이 되면,

서버와 클라이언트가 실시간으로 서로 메시지를 주고받을 수 있음

 

- 클라이언트가 요청을 보낼 필요 없이

서버가 알아서 클라이언트에 상태 변화가 있으면 바로 메시지를 푸시(push)함

 

 

 

✅ WebSocket 방식 흐름

1. 클라이언트 → 서버: "WebSocket 연결할게!"
2. 서버 → 클라이언트: "연결 완료!"
3. 서버 → 클라이언트: "상태가 변경됐어. 너 이제 순번 123이야!"
4. 서버 → 클라이언트: "결제 준비 완료됐어. 좌석 선택 페이지로 가!"

 

 

 

✅ WebSocket 방식의 장점

- WebSocket은 상태가 변경될 때만

서버가 실시간으로 클라이언트에 메시지를 전송한다.

 

- 불필요한 요청을 줄이고, 사용자에게 더 빠른 업데이트를 제공할 수 있다.

 

 

 

 

📩 클라이언트 → 서버

클라이언트가 서버와 WebSocket 연결을 열 때, 사용자 정보를 함께 전송한다.

 

Request
{
  "type": "connect",
  "userId": "user123"
}

 

 

 

 

📤 서버 → 클라이언트

서버는 대기 상태나 사용자 상태가 변경될 때

WebSocket을 통해 클라이언트에 실시간으로 푸시 메시지를 보낸다.

 

Response
{
  "status": "waiting",                 // 대기 상태 (waiting(대기), ready(좌석 선택 가능), seated (좌석선택))
  "currentRank": 1234,              // 현재 대기 순번
  "estimatedWaitTime": 300,     // 예상 대기 시간 (초 단위)
  "totalTicketCount": 4,            // 사용자가 현재 구매한 총 티켓 수
  "nextAction": "wait"               // 다음 행동 (예: wait, move_to_seat_selection)
}

 

 

 

 

 


2) 티켓팅 상태 관리 with Redis

 

 

 

1️⃣ 대기 순번 조회 

🔍 Redis 명령어: ZRANK

ZRANK 명령어는 Redis의 Sorted Set(정렬된 집합)을 사용하여,

사용자의 현재 대기 순번(랭크)을 계산하는 데 사용된다.

 

사용자가 처음 티켓팅 페이지에 접속하면,

서버는 해당 사용자를 Sorted Set에 추가하고,

ZRANK 명령어를 통해 실시간으로 대기 순번을 확인한다.

 

 

 

📩 Redis와 WebSocket 연동

서버는 사용자의 대기 상태가 변경될 때 WebSocket을 통해

실시간 푸시 메시지를 클라이언트에 전송한다.

 

대기 순번이 변경될 때마다 Redis에서 ZRANK 명령어를 다시 실행하고,

변경된 순번을 사용자에게 푸시한다.

 

 

 

🛠️ 실행 흐름

1. 사용자가 티켓팅 페이지에 접속하면, ZADD 명령어로 Redis의 대기열에 추가된다.

 

2. 서버는 사용자의 현재 대기 순번을 ZRANK 명령어로 조회한다.

 

3. 클라이언트는 처음 연결될 때 초기 대기 순번을 서버로부터 전달받고,

이후 대기 순번이 변경될 때마다 실시간 푸시 메시지를 수신한다.

 

 

 

💡 예상 시나리오 (대기 순번 조회)

1️⃣ 사용자 A가 티켓팅 페이지에 접속하여 대기열에 추가됨.

ZADD queue 1692000000 user_A

 

 

 

2️⃣ 서버가 사용자 A의 대기 순번을 조회함.

ZRANK queue user_A

➡️ 사용자 A는 "현재 대기 순번: 1234번" 메시지를 받는다.

 

 

3️⃣ 대기 순번이 변경되면 서버는 WebSocket을 통해 사용자에게 업데이트된 순번을 푸시함.

➡️ 예: "현재 대기 순번: 1001번" 메시지를 사용자에게 전송한다.

 

 

 

 

 


2️⃣ 좌석 LOCK 처리 (Seat Locking)

🔍 Redis 명령어: ZADD, ZREM

  • 좌석 선택 시, 해당 좌석이 다른 사용자에 의해 선택되지 않도록
    • 임시로 좌석을 잠금(Lock) 상태로 설정해야 한다.
  • 이때 ZADD 명령어를 사용해 좌석을 잠금 상태로 저장하고
    • ZREM 명령어로 취소 시 좌석을 해제한다.

 

 

🛠️ 실행 흐름

1. 사용자가 좌석을 선택하면, 해당 좌석 ID가 Redis에 잠금 상태로 저장된다.

 

 

2.다른 사용자가 같은 좌석을 선택하려고 할 때, ZRANK 명령어로 좌석의 잠금 여부를 확인함.

  • 만약 ZRANK 반환 값이 null이면,
    • 좌석이 선택되지 않은 상태이므로 사용자가 선택 가능함.
  • 만약 ZRANK 반환 값이 존재하면,
    • 좌석이 이미 선택된 상태이므로 "이미 선택된 좌석입니다" 메시지를 반환함.

 

3. 사용자가 좌석을 취소하거나 결제 시간이 초과되면, ZREM 명령어를 사용해 좌석 잠금을 해제한다.

 

 

 

 

💡 예상 시나리오 (좌석 LOCK 처리)

1️⃣ 사용자 B가 좌석 F103-25번을 선택함.

 

Redis 명령어

ZADD pending_seats <timestamp + 300> F103-25

 

< 티켓팅 좌석 선택 요청 : POST /api/seat/select >
{
  "seatId": "F103-25",
  "userId": "user123"
}

 

 

 

 

2️⃣ 서버는 해당 좌석이 다른 사용자에게 선택되지 않도록 잠금 상태로 유지함.

 

 

 

3️⃣ 사용자 C가 같은 좌석을 선택하려고 시도하면,

Redis에서 ZRANK 명령어로 좌석 잠금 상태를 확인하고 

"이미 선택된 좌석입니다" 메시지를 반환한다.

 

Redis 명령어

ZRANK pending_seats F103-25

 

 

Response (좌석 선택 불가능한 경우)
{
  "status": "error",
  "message": "Seat selection expired. Please choose a different seat."
}

 

 

 

4️⃣ 사용자가 좌석을 취소하거나, 5분이 지나면 좌석 잠금이 자동 해제됨.

 

Redis 명령어

ZREM pending_seats F103-25

 

 

 

 


 

3️⃣ 취소표 처리 (Canceled Seats Handling)

🔍 Redis 명령어: ZREM

  • 사용자가 좌석 선택 후 결제 창까지 넘어갔지만
    • 결제를 완료하지 않고 취소하거나
    • 시간 초과로 인해 좌석이 해제되는 상황을 처리해야 한다.
  • Redis에서 ZREM 명령어를 사용해 좌석 잠금 상태를 해제하고, 좌석을 다시 선택 가능 상태로 전환한다.

 

 

🛠️ 실행 흐름

1.사용자가 좌석을 선택하면, Redis의 ZADD 명령어를 통해 결제 대기 시간(예: 5분)이 설정됨.

  • 좌석이 선택된 상태로 5분간 유지되며, 시간이 초과되면 자동으로 잠금이 해제됨.
  • Redis TTL 설정을 통해 좌석 잠금 시간 제한을 설정할 수 있음.

 

2. 사용자가 결제 페이지를 나가거나 POST /api/seat/cancel API 요청을 통해 좌석을 취소하면,
서버는 Redis에서 ZREM 명령어로 해당 좌석을 제거함.

 

 

3. 좌석이 다시 선택 가능 상태로 전환됨.

 

 

 

💡 예상 시나리오 (취소표 처리) 

1️⃣사용자 D가 좌석 G101-10번을 선택 후, 결제 페이지에 접속함.

  • 서버는 Redis에 좌석 잠금 상태와 5분 TTL을 설정함.
ZADD pending_seats 1692000300 G101-10
EXPIRE pending_seats G101-10 300  # 5분 TTL 설정

 

 

 

2️⃣사용자 D가 결제 페이지에서 창을 닫거나, 취소 버튼을 눌러 API를 호출함.

  • 클라이언트가 POST /api/seat/cancel 요청을 서버로 보냄.
POST /api/seat/cancel

{
  "seatId": "G101-10",
  "userId": "user123"
}


 

 

3️⃣ 서버는 Redis에서 해당 좌석을 ZREM 명령어로 제거함.

좌석 G101-10번이 다시 선택 가능 상태가 됨.

ZREM pending_seats G101-10



 

 

 


 

4️⃣ 구매 제한 관리 (Ticket Purchase Limit)

🔍 Redis 명령어: HGET, HINCRBY

사용자가 구매한 티켓 수를 Redis의 Hash 구조로 관리하고,

총 구매 티켓 수가 제한 수량(5장)을 초과하면

WebSocket을 통해 "구매 불가" 메시지를 전송한다.

 

 

 

🛠️ 실행 흐름

1. 사용자가 티켓을 구매할 때마다, Redis의 Hash에 해당 사용자의 구매 수량이 증가함.

 

2. 서버는 HGET 명령어로 사용자의 총 구매 티켓 수를 확인함.

 

3. 만약 총 구매 수량이 5장을 초과하면,

서버는 WebSocket을 통해 클라이언트에 "구매 불가" 메시지를 전송함.

 

 

 

💡 예상 시나리오 (구매 제한)

 

1️⃣ 사용자 E가 5장을 구매한 후, 추가로 구매하려고 시도함.

 

HINCRBY user_ticket_count user123 1

 

 

 

2️⃣ 서버는 Redis에서 사용자의 총 구매 수량을 확인함.

 

HGET user_ticket_count user123

 

 

 

3️⃣ 총 구매 수량이 5장을 초과하면, 서버는 "구매 불가" 메시지를 반환함.

 

Response:
{ 
    "status": "error", 
    "message": "Purchase limit exceeded. You cannot buy more tickets." 
}

 

 

 

4️⃣ 클라이언트는 "구매 제한 초과" 메시지를 사용자 화면에 표시함.