[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 제거
-
상태 스냅샷 & 변경 감지
-
시간 전이 트리거(스케줄)
TaskScheduler로 openAt, lockAt 절대시각에 단발 스케줄 등록
- 해당 시각 도달 시
MatchStateChangedEvent 발행
- 라운드가 바뀌면 기존 스케줄 취소 후 재등록
- (다중 인스턴스) 분산 락:
SETNX match:lock:{roundId}:{phase} + TTL
-
클라이언트 카운트다운
- STATUS에는
round.openAt/lockAt만 포함(지금도 있음)
remainingTime은 클라에서 계산 (서버 1초 푸시 제거)
- 과도기: 서버가 보낸
remainingTime이 있어도 클라 우선 계산로직로 동작(하위호환)
작업 항목(Tasks)
리스크 & 완화
- 멀티 인스턴스 중복 전송 → Redis
SETNX 락/리더십으로 단일 발행 보장
- 스케줄 시각 표준화 → 모든 예약/전이 계산은 KST 기준으로 일원화, DB는 UTC 저장
- 누락 방지 → 저빈도 헬스 싱크(예: 30~60초) 옵션으로 재동기화 가능(기본 OFF)
수용 기준(Acceptance)
코어 스니펫(축약)
// 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 고려 → 중~대
[RFC] MatchHandler를 “상태 변화 기반 즉시 푸시”로 전환
배경
현재는 서버가 1초마다 전체 세션에 STATUS를 푸시(서버 사이드 interval push).
트래픽/비용이 불필요하게 발생하고, “실시간”의 본질(이벤트 순간 전달)과도 다름.
목표(Goals)
openAt/lockAt으로 로컬 카운트다운 수행(remainingTime은 클라 계산)범위(Non-goals)
설계 개요
도메인 이벤트 발행
MatchQueueJoinedEvent(userId),MatchQueueCanceledEvent(userId)MatchStateChangedEvent(prevState, newState, roundId)(OPEN↔LOCKED 등)RoundWindowChangedEvent(prevRound, newRound)브로드캐스터 분리
MatchBroadcaster컴포넌트 신설: WebSocket 전송 전담@EventListener로 위 이벤트 수신 → 필요한 세션에만 메시지 푸시org.json제거상태 스냅샷 & 변경 감지
StatusCache(AtomicRef)로 마지막 STATUS 스냅샷 보관새 스냅샷과 비교해 실제 변경이 있을 때만 브로드캐스트
시간 전이 트리거(스케줄)
TaskScheduler로openAt,lockAt절대시각에 단발 스케줄 등록MatchStateChangedEvent발행SETNX match:lock:{roundId}:{phase}+ TTL클라이언트 카운트다운
round.openAt/lockAt만 포함(지금도 있음)remainingTime은 클라에서 계산 (서버 1초 푸시 제거)remainingTime이 있어도 클라 우선 계산로직로 동작(하위호환)작업 항목(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 시각 예약, 라운드 변경 시 재등록RedisLock.tryLock(key, ttl)+ 중복 방지MatchHandler:@Scheduled broadcastStatus()삭제openAt/lockAt기준 클라 계산으로 변경(스펙 노트)리스크 & 완화
SETNX락/리더십으로 단일 발행 보장수용 기준(Acceptance)
JOIN/CANCEL시에만count변화가 즉시 반영되고, 유휴 시 브로드캐스트 없음openAt/lockAt전이 시점 ±1s 내에 상태 변경 푸시 도착STATUS/DRAFT_START메시지 타입, 필드 유지코어 스니펫(축약)
사이징 메모