← 개발일지

매칭 연결이 가끔 안 됐다 — 상태머신으로 갈아엎기까지

증상: 가끔 연결이 안 된다

매칭은 되는데 게임이 시작 안 되는 케이스가 간헐적으로 보고됐다. 재현이 안 돼서 로그부터 붙였다. 구독이 걸리기 전에 candidate가 먼저 도착해 유실되는 경로, 정상 종료를 오탐으로 잡아 끊는 경로 두 개를 찾았다.

패치가 패치를 부른다

레이스 하나를 고치면 인접한 레이스가 또 보였다. 시그널링 백업 폴링을 realtime 단독 의존에서 분리했고, 폴백 매칭이 ICE 연결 결과를 기다리지 않고 먼저 취소해버리는 경합도 순서를 정리했다. 전수 조사해보니 잔여 케이스가 더 있었다.

패치를 세 번째로 또 돌리느니 상태 전이 자체를 표준 패턴으로 다시 짜기로 했다.

DB를 단일 진실 공급원으로

1단계: 큐의 status를 단조 증가시켰다. 폴백과 라이브가 상호 배제라는 규칙을 클라이언트 판단이 아니라 DB 제약으로 옮겼다. 2단계: 서버에 matches 세션 테이블과 TTL을 두고, 클라이언트는 isConnected 같은 자기 연결 상태 대신 서버가 내려주는 status로 라이브/폴백 라우팅을 나눴다.

그래도 테스트가 안 됐다

상태 전이 로직이 MatchingModal 안에 타이머·이펙트와 뒤섞여 있어서 로직만 떼서 유닛테스트를 돌릴 방법이 없었다. 매치메이킹·듀얼 상태머신은 이 프로젝트의 테스트 필수 경계인데, 선언만 있고 실제로는 테스트 불가능했다.

상태머신을 (state, event) => [state, command[]] 형태 순수 리듀서로 분리했다. MatchingModal은 커맨드 실행만 맡는다. 리빌 연출(매칭 확정 후 모드 공개) 구간도 리듀서 안으로 들어와서, 화면을 안 띄워도 이벤트 시퀀스별 다음 상태를 유닛테스트로 돌릴 수 있다.