-
Notifications
You must be signed in to change notification settings - Fork 3
Open
Description
[RFC] MatchHandler를 “상태 변화 기반 즉시 푸시”로 전환
배경
현재는 서버가 1초마다 전체 세션에 STATUS를 푸시(서버 사이드 interval push).
트래픽/비용이 불필요하게 발생하고, “실시간”의 본질(이벤트 순간 전달)과도 다름.
목표(Goals)
- 상태 변화 발생 시에만 STATUS 푸시 (JOIN, CANCEL, OPEN→LOCKED 전이 등)
- 초당 브로드캐스트 제거 (핫루프 중단)
- 클라이언트는
openAt/lockAt으로 로컬 카운트다운 수행(remainingTime은 클라 계산)
범위(Non-goals)
- 매칭/배정 알고리즘 변경 아님.
- 기존 메시지 타입/DTO 대폭 변경 아님(가능한 호환 유지).
설계 개요
-
도메인 이벤트 발행
MatchQueueJoinedEvent(userId),MatchQueueCanceledEvent(userId)MatchStateChangedEvent(prevState, newState, roundId)(OPEN↔LOCKED 등)RoundWindowChangedEvent(prevRound, newRound)
발행처:
MatchService(join/cancel),DraftTimingService(라운드/상태 전이 결정 시) -
브로드캐스터 분리
MatchBroadcaster컴포넌트 신설: WebSocket 전송 전담@EventListener로 위 이벤트 수신 → 필요한 세션에만 메시지 푸시- 메시지 조립은 DTO(ObjectMapper)로,
org.json제거
-
상태 스냅샷 & 변경 감지
-
StatusCache(AtomicRef)로 마지막 STATUS 스냅샷 보관 -
새 스냅샷과 비교해 실제 변경이 있을 때만 브로드캐스트
- 예: count, state, round.id, openAt/lockAt 중 하나라도 달라지면 전송
-
-
시간 전이 트리거(스케줄)
TaskScheduler로openAt,lockAt절대시각에 단발 스케줄 등록- 해당 시각 도달 시
MatchStateChangedEvent발행 - 라운드가 바뀌면 기존 스케줄 취소 후 재등록
- (다중 인스턴스) 분산 락:
SETNX match:lock:{roundId}:{phase}+ TTL
-
클라이언트 카운트다운
- STATUS에는
round.openAt/lockAt만 포함(지금도 있음) remainingTime은 클라에서 계산 (서버 1초 푸시 제거)- 과도기: 서버가 보낸
remainingTime이 있어도 클라 우선 계산로직로 동작(하위호환)
- STATUS에는
작업 항목(Tasks)
-
MatchBroadcaster생성:sendStatusToAll(StatusDto),sendToUser(userId, msg) -
StatusCache+StatusChangeDetector(equals 비교로 변경 감지) -
ApplicationEventPublisher도입, 이벤트 클래스 4종 정의 -
MatchService: join/cancel 후Queue*Event발행 -
DraftTimingService: 라운드/상태 전이 판단 시State/Round*Event발행 -
TimingScheduler:TaskScheduler로 openAt/lockAt 시각 예약, 라운드 변경 시 재등록 - (멀티노드) 분산락 util:
RedisLock.tryLock(key, ttl)+ 중복 방지 -
MatchHandler:@Scheduled broadcastStatus()삭제 - 메시지 조립을 DTO + ObjectMapper로 전환
- 프론트: 남은 시간은
openAt/lockAt기준 클라 계산으로 변경(스펙 노트)
리스크 & 완화
- 멀티 인스턴스 중복 전송 → Redis
SETNX락/리더십으로 단일 발행 보장 - 스케줄 시각 표준화 → 모든 예약/전이 계산은 KST 기준으로 일원화, DB는 UTC 저장
- 누락 방지 → 저빈도 헬스 싱크(예: 30~60초) 옵션으로 재동기화 가능(기본 OFF)
수용 기준(Acceptance)
-
JOIN/CANCEL시에만count변화가 즉시 반영되고, 유휴 시 브로드캐스트 없음 -
openAt/lockAt전이 시점 ±1s 내에 상태 변경 푸시 도착 - 초당 스케줄 제거 후 서버 전송 QPS가 기존 대비 유의미하게 감소
- 멀티 인스턴스 환경에서도 상태 전이 푸시 중복 0회
- 회귀: 기존
STATUS/DRAFT_START메시지 타입, 필드 유지
코어 스니펫(축약)
// 1) 이벤트 정의
public record MatchQueueJoinedEvent(String userId) {}
public record MatchQueueCanceledEvent(String userId) {}
public record MatchStateChangedEvent(String prev, String next, UUID roundId) {}
public record RoundWindowChangedEvent(UUID prev, UUID next) {}
// 2) 발행 (MatchService)
publisher.publishEvent(new MatchQueueJoinedEvent(userId));
// 3) 리스너 + 브로드캐스터
@Component
@RequiredArgsConstructor
class MatchEventsListener {
private final MatchService matchService;
private final MatchBroadcaster broadcaster;
private final StatusCache cache;
@EventListener
public void onQueueChanged(Object e) {
var status = matchService.getCurrentStatus();
if (cache.isChanged(status)) {
broadcaster.sendStatusToAll(status);
cache.update(status);
}
}
@EventListener
public void onStateChanged(MatchStateChangedEvent e) {
var status = matchService.getCurrentStatus();
broadcaster.sendStatusToAll(status);
cache.update(status);
// LOCKED 진입 시 배치 트리거 등…
}
}사이징 메모
- 단일 인스턴스: 이벤트/브로드캐스터/스케줄러 분리까지만 → 소~중
- 멀티 인스턴스: Redis 락 + Pub/Sub/Stream 고려 → 중~대
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels