Skip to content

feat: LoL Custom Match 실시간 세션 기능 구현 (WebSocket) #87

@Sunja-An

Description

@Sunja-An

배경

현재 lol_custom_matches 테이블은 존재하지만, 여러 유저가 동시에 접속해서 상호작용할 수 있는 세션 레이어가 없다.
Custom Match를 생성할 때 방장이 URL을 공유하면, 참가자들이 실시간으로 포지션 입력·팀 편성 결과를 함께 볼 수 있어야 한다.


해결해야 할 문제

요구사항 설명
공유 URL 세션 ID가 포함된 URL로 누구나 접근 가능
실시간 상태 공유 참가자 입력/팀 배정 결과를 전체 세션에 즉시 브로드캐스트
동시 접속 N명이 동시에 같은 세션에 연결 가능
세션 생명주기 생성 → 참가 대기 → 팀 배정 → 결과 기록 → 종료

기술 설계

왜 WebSocket인가?

방식 장점 단점 적합도
WebSocket 양방향 실시간, 낮은 오버헤드 연결 관리 필요 ✅ 최적
SSE 서버→클라이언트 단방향 클라이언트 입력은 REST 별도 필요, 세션 상호작용에 어색
Long Polling 구현 단순 지연 높음, 부하 큼

gorilla/websocket은 Gin 에코시스템에서 표준적으로 사용되며, 이미 Go 생태계에서 검증됨.


아키텍처

Client A ──WS──┐
Client B ──WS──┤─── SessionHub (in-memory) ──→ Redis Pub/Sub ──→ (수평 확장 시)
Client C ──WS──┘
                         │
                    Redis (세션 상태 저장, TTL 2h)
                         │
                    MySQL (최종 결과만 persist)

세션 상태는 Redis, 결과만 MySQL에 저장한다.
이유: 세션 중간 상태(참가자 목록, 포지션 입력 등)는 임시 데이터이므로 Redis TTL로 관리가 적합하다.


세션 생명주기

WAITING → BALANCING → FINISHED / CANCELLED
  • WAITING: 방장이 세션 생성 후 참가자 대기
  • BALANCING: 팀 배정 알고리즘 실행 완료, 결과 표시 중
  • FINISHED: 방장이 경기 결과 기록 (winner: TEAM_A / TEAM_B)
  • CANCELLED: 방장이 세션 취소

Redis 데이터 구조

lol:session:{sessionID}            → JSON (SessionState)   TTL 2h
lol:session:{sessionID}:events     → Redis Pub/Sub 채널 (수평 확장용)

SessionState (Redis JSON)

{
  "session_id": "uuid-v4",
  "match_id": 42,
  "host_user_id": 1,
  "status": "WAITING",
  "players": [
    {
      "user_id": 1,
      "username": "Faker",
      "tag": "KR1",
      "rank": "CHALLENGER",
      "positions": ["MID", "TOP", "JUNGLE", "BOT", "SUPPORT"]
    }
  ],
  "balance_result": null,
  "created_at": "2026-04-05T00:00:00Z"
}

WebSocket 메시지 프로토콜

클라이언트 → 서버 (Action):

{ "type": "JOIN",    "payload": { "username": "Faker", "tag": "KR1", "rank": "CHALLENGER", "positions": ["MID","TOP","JG","BOT","SUP"] } }
{ "type": "LEAVE",   "payload": {} }
{ "type": "BALANCE", "payload": {} }
{ "type": "RESULT",  "payload": { "winner": "TEAM_A" } }

서버 → 전체 브로드캐스트 (Event):

{ "event": "PLAYER_JOINED",   "data": { "players": [...] } }
{ "event": "PLAYER_LEFT",     "data": { "players": [...] } }
{ "event": "BALANCE_DONE",    "data": { "team_a": [...], "team_b": [...] } }
{ "event": "RESULT_RECORDED", "data": { "winner": "TEAM_A", "match_id": 42 } }
{ "event": "SESSION_CLOSED",  "data": {} }
{ "event": "ERROR",           "data": { "message": "권한 없음" } }

REST API (세션 생성/조회)

POST   /api/v1/lol/sessions               → 세션 생성 (방장, JWT 필요)
GET    /api/v1/lol/sessions/{session_id}  → 현재 세션 상태 조회 (Public)
DELETE /api/v1/lol/sessions/{session_id}  → 세션 취소 (방장만)

WS     /api/v1/lol/sessions/{session_id}/ws  → WebSocket 연결

디렉토리 구조 (Hexagonal)

internal/lol/
  domain/
    lol_session.go              ← SessionState, 세션 생명주기 규칙
  application/
    lol_session_service.go      ← 세션 CRUD, JOIN/LEAVE 처리
    port/
      lol_session_port.go       ← Redis 세션 저장소 인터페이스
  infra/
    persistence/
      lol_custom_match_adapter.go   (기존)
    redis/
      lol_session_redis_adapter.go  ← Redis 세션 저장소 구현
    ws/
      lol_session_hub.go        ← WebSocket Hub (in-memory, broadcast)
  presentation/
    lol_session_controller.go   ← REST + WS 핸들러

구현 순서 (제안)

  1. 도메인 모델 lol_session.go — SessionState, 상태 전이 규칙
  2. Redis 어댑터 — 세션 CRUD (저장/조회/삭제, TTL)
  3. WebSocket Hub — 연결 관리, 브로드캐스트 로직
  4. 서비스 레이어 — JOIN/LEAVE/BALANCE/RESULT 처리, Hub에 이벤트 발행
  5. 컨트롤러 — REST 엔드포인트 + WS 업그레이드
  6. DI 와이어링provider.go, cmd/server.go 등록
  7. 테스트 — Hub 단위 테스트, 서비스 단위 테스트

고려 사항

  • 인증: WS 연결 시 JWT를 쿼리 파라미터(?token=...) 또는 첫 메시지로 전달 (WS는 커스텀 헤더 불가)
  • 방장 권한: BALANCE, RESULT, 세션 종료는 host_user_id만 실행 가능
  • 최대 인원: 세션당 10명 제한 (5v5)
  • Graceful shutdown: 서버 종료 시 모든 WS 연결에 SESSION_CLOSED 전송 후 닫기
  • 수평 확장: 단일 인스턴스에서는 in-memory Hub로 충분, 멀티 인스턴스 시 Redis Pub/Sub으로 브로드캐스트 확장 가능 (처음부터 인터페이스로 추상화)

관련 파일

  • db/migrations/000026_create_lol_custom_matches_table.up.sql — 이미 존재
  • internal/lol/domain/lol_custom_match.go — Match 도메인 엔티티
  • internal/lol/application/lol_team_balance_service.go — 팀 배정 로직 (재사용)

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions