Files
surinz/MATCH_SYNC.md
T
2026-06-12 10:11:47 +09:00

5.4 KiB
Raw Blame History

매치 상세 동기화 구조

전적검색의 매치 기록(맵/참가자 상세) 데이터를 넥슨 오픈 API에서 수집·캐시하는 구조 문서.

관련 코드: backend/src/modules/sa-match/, frontend/src/pages/profile/


1. 설계 원칙

원칙 내용
불변 데이터 끝난 매치의 상세(맵/참가자)는 절대 변하지 않는다 → 매치당 평생 1회만 넥슨 API 호출, 이후 영원히 DB 재사용
수집이 병목, 연산은 아님 평균·집계는 DB에서 밀리초면 끝난다. 문제는 상세 API가 매치당 1건씩만 제공된다는 것 → "실시간 전수 조회" 대신 점진 수집
0초 대기 상세 적재는 응답을 블로킹하지 않는다(fire-and-forget). 사용자는 항상 즉시 목록을 보고, 맵은 점진적으로 채워진다
쿼터 보호 개발 키는 일일 1,000건 한도 → 환경별 백필 상한으로 보호

2. 데이터 구조

sa_matches                매치 목록 — (matchId, ouid) 단위. 본인 k/d/a·결과·일시 (맵 없음 = 목록 API가 미제공)
sa_match_details          상세 헤더 — matchId 단위. 맵 이름. 행 존재 = 적재 완료 (영구 캐시)
sa_match_participants     상세 참가자 — matchId 단위. 전원 k/d/a·헤드샷·데미지
sa_match_detail_failures  실패 기록 — matchId 단위. failCount 누적, 한도 초과 시 백필 제외

목록 응답의 맵 이름은 sa_matches ⟕ sa_match_details 조인으로 채운다. 캐시 미보유 매치는 빈 문자열('')로 내려가고 프론트는 로 표시한다.


3. 상세 적재 경로 (3가지)

경로 트리거 범위 블로킹
① 행 펼치기 매치 행 클릭 (GET /sa-matches/:matchId) 해당 1건 동기 (사용자가 상세를 보는 중)
② 온디맨드 백필 매치 목록 조회 (getMatches) 맵 미보유 최신 N건 비동기 (fire-and-forget)
③ 야간 크론 매일 03:00 KST (handleDailyMatchSync) 미보유 전량 (환경별 한도) 백그라운드
  • ②: 검색 1회 = 5개 매치 타입 병렬 호출이므로 실제 최대 적재는 한도 × 5
  • ③: 목록 동기화(전 유저 × 5타입) 후 백필. 최신순, matchId 중복 제거(공유 매치는 1회만)
  • ②/③ 공통: backfillInFlight Set으로 동일 매치 동시 적재 방지

환경별 한도

항목 개발 운영 코드 위치
프론트 더보기 노출 4개 20개 MatchHistory.vue (import.meta.env.DEV)
온디맨드 백필 (타입당) 4건 20건 BACKFILL_LIMIT
크론 백필 (유저당) 20건 무제한 CRON_PER_USER_BACKFILL_LIMIT
넥슨 rate limit 5건/초 500건/초 env NEXON_API_RATE_LIMIT_PER_SEC

환경 분기는 백엔드 process.env.NODE_ENV === 'production', 프론트 import.meta.env.DEV 기준.


4. 실패 마킹 (무한 재시도 방지)

넥슨이 상세를 끝내 제공하지 않는 매치(삭제/만료 등)가 매일 재시도되며 쿼터를 낭비하는 것을 막는다.

  • 백필 실패 시 sa_match_detail_failures.failCount 누적 + 마지막 에러 기록
  • MAX_DETAIL_FAILURES(3회) 초과 매치는 백필 대상에서 제외 — 크론은 쿼리(leftJoin), 온디맨드는 후보 선정 시 제외
  • 적재 성공 시 실패 기록 삭제 — 일시 장애로 쌓인 카운트는 복구 후 자동 정리
  • 실패 "기록" 자체의 실패는 백필 흐름을 끊지 않음 (warn 로그만)

5. 성능 특성

rate limiter ≠ 처리량

크론 백필의 실제 처리량은 rate limiter가 아니라 순차 await 루프 구조가 결정한다.

순차 루프: 동시 요청이 항상 1개 → 처리량 = 1 ÷ 호출당 왕복시간(~250ms) ≈ 초당 4건
→ 운영 한도 500/s는 한 번도 닿지 않음 (점유율 ~0.8%)
규모 크론 소요 시간 (순차 기준)
1,000건 ~5분
10,000건 ~50분 (최초 캐치업 1회성. steady state는 일 신규분만 → 수 분)

검색과의 경합 — 운영 기준 없음

자원 크론 점유 한도 판정
넥슨 rate limit ~4건/초 500건/초 0.8% — 경합 없음
Node 이벤트 루프 I/O 대기 위주 await마다 양보, 서버 응답성 유지
DB 커넥션 풀 1개 (순차) 100개 영향 없음

개발(5/s)에서는 크론의 ~4/s가 한도의 80%라 동시 검색이 몇 초 지연될 수 있으나, 유저당 20건 캡 덕분에 크론 자체가 ~수십 초면 끝난다.

과거 "동기화 중 검색 차단(SyncState)" 로직이 있었으나, 위 분석(경합 사실상 없음)에 따라 제거됨.


6. 향후 개선 후보

항목 시점 방법
크론 동시성 일 신규분만으로 크론이 1시간 초과 시 순차 루프 → 동시 8건 풀 (10,000건 ≈ 5분, API 점유 6%)
Redis / BullMQ 서버 다중 인스턴스 도입 시 크론 분산 락 + backfillInFlight 공유 + 백필 큐 영속화
헤드샷/데미지 평균 기능 요구 시 상세 적재 시 본인 참가자 행을 sa_matches에 역정규화 → SQL AVG + 커버리지 표기("적재된 N전 / 전체 M전 기준")
dev 크론 수동 트리거 개발 편의 개발 환경 전용 트리거 엔드포인트