배경
현재 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 핸들러
구현 순서 (제안)
- 도메인 모델
lol_session.go — SessionState, 상태 전이 규칙
- Redis 어댑터 — 세션 CRUD (저장/조회/삭제, TTL)
- WebSocket Hub — 연결 관리, 브로드캐스트 로직
- 서비스 레이어 — JOIN/LEAVE/BALANCE/RESULT 처리, Hub에 이벤트 발행
- 컨트롤러 — REST 엔드포인트 + WS 업그레이드
- DI 와이어링 —
provider.go, cmd/server.go 등록
- 테스트 — 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 — 팀 배정 로직 (재사용)
배경
현재
lol_custom_matches테이블은 존재하지만, 여러 유저가 동시에 접속해서 상호작용할 수 있는 세션 레이어가 없다.Custom Match를 생성할 때 방장이 URL을 공유하면, 참가자들이 실시간으로 포지션 입력·팀 편성 결과를 함께 볼 수 있어야 한다.
해결해야 할 문제
기술 설계
왜 WebSocket인가?
gorilla/websocket은 Gin 에코시스템에서 표준적으로 사용되며, 이미 Go 생태계에서 검증됨.
아키텍처
세션 상태는 Redis, 결과만 MySQL에 저장한다.
이유: 세션 중간 상태(참가자 목록, 포지션 입력 등)는 임시 데이터이므로 Redis TTL로 관리가 적합하다.
세션 생명주기
WAITING: 방장이 세션 생성 후 참가자 대기BALANCING: 팀 배정 알고리즘 실행 완료, 결과 표시 중FINISHED: 방장이 경기 결과 기록 (winner: TEAM_A / TEAM_B)CANCELLED: 방장이 세션 취소Redis 데이터 구조
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 (세션 생성/조회)
디렉토리 구조 (Hexagonal)
구현 순서 (제안)
lol_session.go— SessionState, 상태 전이 규칙provider.go,cmd/server.go등록고려 사항
?token=...) 또는 첫 메시지로 전달 (WS는 커스텀 헤더 불가)host_user_id만 실행 가능SESSION_CLOSED전송 후 닫기관련 파일
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— 팀 배정 로직 (재사용)