yeonghoon.kim

  • 게시판
  • 갤러리

Redis 대기열 시스템

김영훈 2026.03.01 15:42 조회 수 : 212

대기열 시스템 학습 프로젝트.

티켓 예매나 한정 판매에서 쓰는 줄서기 구조를 직접 만들어봄.

프로젝트.

0028-queue-system

기술 스택.

FastAPI
Redis
WebSocket
Docker Compose
Caddy

구조.

0028-queue-system/
├── backend/
│   ├── app/
│   │   ├── main.py
│   │   ├── background_tasks.py
│   │   ├── models.py
│   │   ├── queue_manager.py
│   │   ├── redis_client.py
│   │   └── websocket_manager.py
│   ├── requirements.txt
│   └── Dockerfile
├── frontend/
│   ├── index.html
│   ├── app.js
│   ├── style.css
│   ├── admin.html
│   ├── admin.js
│   └── admin.css
├── docker-compose.yml
├── deploy.sh
├── test_api.sh
└── test_admin_api.sh

Redis 데이터 구조.

ZADD queue:waiting {user_id: timestamp}

SADD queue:active {user_id}

HSET queue:meta:{user_id} {field: value}

SET queue:heartbeat:{user_id} {timestamp}

순번 조회.

def _get_position(self, user_id: str) -> int:
    """대기 순번 조회 (1부터 시작)"""
    rank = self.redis.zrank(self.WAITING_QUEUE, user_id)
    return (rank + 1) if rank is not None else 0

WebSocket 연결 관리.

class ConnectionManager:
    def __init__(self):
        self.active_connections: Dict[str, WebSocket] = {}
    
    async def connect(self, user_id: str, websocket: WebSocket):
        await websocket.accept()
        self.active_connections[user_id] = websocket
    
    async def send_personal_message(self, user_id: str, message: dict):
        if user_id in self.active_connections:
            await self.active_connections[user_id].send_json(message)

메시지 예시.

{
  "type": "update",
  "position": 42,
  "ahead": 41,
  "estimated_wait_seconds": 120
}

{
  "type": "admitted",
  "message": "입장이 허용되었습니다!"
}

예상 대기 시간.

def _estimate_wait_time(self, position: int) -> int:
    """예상 대기 시간 계산 (초)
    
    슬라이딩 윈도우: 최근 10분간 실제 세션 시간의 평균 사용
    """
    if position <= 0:
        return 0
    
    ten_minutes_ago = time.time() - 600
    recent_sessions = self.redis.zrangebyscore(
        self.SESSION_TIMES, 
        ten_minutes_ago, 
        '+inf',
        withscores=True
    )
    
    if recent_sessions:
        durations = [float(d) for d, _ in recent_sessions]
        avg_session_time = sum(durations) / len(durations)
    else:
        avg_session_time = 60
    
    return int(position * avg_session_time)

백그라운드 정리.

@repeat_every(seconds=5)
async def cleanup_task():
    result = queue_manager.cleanup_timed_out_users()
    
    if result["waiting_removed"] > 0:
        logger.info(f"대기열에서 {result['waiting_removed']}명 제거")
    
    if result["active_removed"] > 0:
        logger.info(f"활성에서 {result['active_removed']}명 제거")
    
    if result["promoted"] > 0:
        await notify_admitted_users()

Input Validation.

class QueueJoinRequest(BaseModel):
    user_id: str = Field(
        ..., 
        min_length=1, 
        max_length=100,
        pattern="^[a-zA-Z0-9_-]+$"
    )
    metadata: Optional[Dict] = None

CORS.

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://queue.example.com"],
    allow_methods=["GET", "POST", "DELETE"],
    allow_headers=["*"],
)

Rate Limit.

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/queue/join")
@limiter.limit("20/minute")
async def join_queue(...):
    ...

관리 API 보호.

async def verify_admin(
    x_api_key: str = Header(...),
    x_forwarded_for: Optional[str] = Header(None)
):
    real_ip = x_forwarded_for.split(",")[0] if x_forwarded_for else "unknown"
    if real_ip not in ADMIN_ALLOWED_IPS:
        raise HTTPException(status_code=403, detail="Forbidden")
    
    if x_api_key != ADMIN_API_KEY:
        raise HTTPException(status_code=401, detail="Invalid API key")
    
    return True

Docker Compose.

services:
  redis:
    image: redis:7-alpine
    networks:
      - queue-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
  
  backend:
    build:
      context: .
      dockerfile: backend/Dockerfile
    networks:
      - queue-network
      - waf-internal
    environment:
      - REDIS_HOST=redis
      - MAX_ACTIVE_USERS=10
      - QUEUE_TIMEOUT_SECONDS=300
    depends_on:
      redis:
        condition: service_healthy

Caddy.

queue.example.com {
    reverse_proxy queue-backend:8000
    
    header /* {
        Strict-Transport-Security "max-age=31536000"
        X-Frame-Options "DENY"
        X-Content-Type-Options "nosniff"
    }
}

배포.

docker-compose up -d

로그.

docker-compose logs -f backend

헬스 체크.

curl http://localhost:8000/

데모 페이지.

http://localhost:8000/demo

관리 페이지.

http://localhost:8000/admin

API 테스트.

curl -X POST http://localhost:8000/api/queue/join \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user-001", "metadata": {"name": "Alice"}}'

curl http://localhost:8000/api/queue/status/user-001

curl -X DELETE http://localhost:8000/api/queue/leave/user-001

관리 API.

curl http://localhost:8000/api/admin/statistics

curl -X POST http://localhost:8000/api/admin/cleanup

curl -X POST http://localhost:8000/api/admin/reset

트러블슈팅.

정적 파일 404.

Before.

app.mount("/static", StaticFiles(directory="frontend"))

HTML.

<link href="style.css">

After.

app.mount("/static", StaticFiles(directory="frontend"))

HTML.

<link href="/static/style.css">

Docker build context 문제.

Before.

build:
  context: ./backend

After.

build:
  context: .
  dockerfile: backend/Dockerfile

Caddy 프록시 IP 문제.

관리 API 403.

X-Forwarded-For로 실제 IP 확인.

real_ip = x_forwarded_for.split(",")[0] if x_forwarded_for else request.client.host

커밋 기록.

2026-03-01 10:01
정적 파일 경로 수정

2026-03-01 10:11
예상 대기 시간을 슬라이딩 윈도우 방식으로 개선

2026-03-01 10:20
Input Validation, CORS, Rate Limiting, 관리 API 보호

2026-03-01 10:29
MAX_ACTIVE_USERS 100 -> 10

2026-03-01 10:31
네트워크 설정 수정

1일짜리 학습 프로젝트.

  • 추천 0

  • 비추천 0
이 게시물을
목록

댓글 0

번호 제목 글쓴이 날짜 조회 수
공지 2025 일본 여행 계획 김영훈 2024.10.10 4119
공지 현금, 저축, 투자, 지출, 예산, 보험 내역(2024-05-30) 김영훈 2024.03.10 3752
17 ARM Kubernetes 구축 1 - VNC & KVM 설치 김영훈 2026.02.14 70
16 ARM Kubernetes 구축 3 - 모니터링 스택 Prometheus Grafana 김영훈 2026.02.16 179
15 ARM Kubernetes 구축 5 - 실전 업그레이드 경험기 v1.29→v1.35 김영훈 2026.02.18 238
14 ARM Kubernetes 구축 4 - 외부 접근 환경 VPN MetalLB Ingress 김영훈 2026.02.18 246
13 ARM Kubernetes 구축 7 - 클러스터 무중단 업그레이드 v1.29→v1.35 김영훈 2026.02.20 252
12 ARM Kubernetes 구축 8 - 실전 보안 RBAC 네트워크 정책 Pod 보안 김영훈 2026.02.20 194
11 ARM Kubernetes 구축 6 - 무중단 업그레이드 김영훈 2026.02.20 208
10 비트코인 자동매매 봇 대시보드 김영훈 2026.02.21 144
9 Apache RewriteRule 쿼리스트링 처리 김영훈 2026.02.28 258
» Redis 대기열 시스템 김영훈 2026.03.01 212
7 ChloeToken 테스트넷 배포 김영훈 2026.03.02 232
6 Tech Support 플랫폼 김영훈 2026.03.04 80
5 WAF 운영 6주 분석 김영훈 2026.03.13 199
4 JSON-LD 학습 - 메일과 Calendar 자동 인식 김영훈 2026.03.28 151
3 7일간의 일본 여행 일정 짜기: 신칸센 + JR 패스 + Google Calendar 활용 프로젝트 김영훈 2026.03.28 128
2 교토 4박 5일 여행 일정: Google Calendar로 관리하는 일정짜기 김영훈 2026.04.05 125
1 OpenClaw 복원과 GLM-5.1 전환 김영훈 2026.04.07 145
쓰기 태그
 첫 페이지 7 8 9 10 11 12 13 14 15 16 끝 페이지