mmday-firebase/docs/data-patterns/optimization-plan.md
윤정민 e76b752a41 docs: add Firebase read/write pattern audit & optimization plan
- Firestore/RTDB read/write 패턴 전수 조사 결과를 문서화
- backend-writes.md, backend-reads.md, data-lifecycle.md, optimization-plan.md 추가
2026-05-28 17:10:19 +09:00

5.8 KiB
Raw Permalink Blame History

Panit read/write 최적화 로드맵

근거: backend-writes.md · backend-reads.md · data-lifecycle.md · client-patterns.md. 영향도(비용 절감) × 리스크(정확성/운영) 기준 우선순위. 각 항목에 권장 변경과 검증 포인트 명시.

요약 — 비용 구조

  • Firestore write 비용 1위: games 매일 월 전량 재기록(대부분 불변 데이터).
  • Firestore read 비용 1위: ① dailyArchive의 유저별 listByDate 중복(U배), ② getStats 무효화 후 voteHistory 전체 풀스캔.
  • RTDB fan-out: 경기 종료마다 투표자 전원 stats 무효화 + result 주입.
  • 클라發 비용 1위: voteSummary 10초 HTTP 폴링(시트 수만큼 병렬) — 유일한 상시 read 경로.

Tier 1 — 큰 절감 · 낮은 리스크 (우선 착수 권장)

W1. games 동기화에 diff 도입 🔴 최대 효과

  • 현재: syncGamesForMonth(gameSyncService.ts:63)가 매일 02:00 한 달치(수십~150+건)를 변경검사 없이 merge:true batch write.
  • 변경: 이미 존재하는 forceSyncDaygetAll+diff 패턴을 월 동기화에도 적용 → 변경된 경기만 batch write. 일일 write 거의 0 수렴.
  • 검증: 신규 일정/시각 변경/상태 전이가 여전히 반영되는지. onGameCompleted는 값이 같으면 점화 안 되므로 영향 없음.

W2. voteHistory 이중 write 통합 🟠

  • 현재: dailyArchive.ts:130이 data만 set → judgeDay(judgmentService.ts:94)가 판정 포함 재set. 동일 doc 2회.
  • 변경: 첫 setDay 생략하고 judgeDay에서 1회만 기록(judgeDay가 voteDoc 인자 보유). 활성유저×매일 write 절반↓.
  • 검증: 판정 실패/멱등 스킵 경로에서 data 유실 없는지(필요 시 첫 write는 복구용으로만 유지).

R1. dailyArchive 날짜별 listByDate 공유 🔴

  • 현재: 유저마다 judgeDay/hasMissedGameDayBetween가 동일 날짜 games 쿼리 1+최대14회 재실행 → U배.
  • 변경: 아카이브 run 시작 시 대상 날짜 범위의 games를 1회 로드해 in-memory map으로 공유, 휴장일 판정 메모이즈.
  • 검증: 유저별 판정 결과 동일성. 유저·시즌 증가 시 가장 빠르게 악화하므로 ROI 최고.

C1. voteSummary 폴링 → RTDB 직접 구독 🔴 (클라)

  • 현재: prediction_providers.dart:31/prediction/summary를 10초 무한 폴링, 시트 수만큼 병렬 CF 호출 → 매번 RTDB read.
  • 변경: votes/{gameId}/counts는 RTDB public read이므로 클라가 onValue 단일 리스너로 직접 구독 → CF 호출 0, 변경분만 push. (차선: 폴링 주기 상향)
  • 검증: 인증 불요(public read), 시트 닫힐 때 리스너 해제, 카운트 표시 일치.

Tier 2 — 중간 절감 (정확성 설계 필요)

R2. getStats 누적 집계 분리 🟠

  • 현재: invalidateStats/cache/stats/{uid} 전체 삭제 → 다음 /statsvoteHistory 전체 풀스캔(시즌 누적 수십~수백 doc).
  • 변경: overall/season 누적치는 판정 시 증분 갱신해 별도 저장, weekly/streak만 재계산. 무효화 범위도 period 단위로 축소.
  • 리스크: 집계 정합성(판정 정정 시 재계산 경로 필요). 설계 검토 후 착수.

W3. stats 무효화 fan-out 유저 단위 1회로 🟠

  • 현재: 경기 종료마다 투표자 전원 무효화 → 하루 다경기 투표 유저는 경기수만큼 중복(gameResultService.ts:42, onGameCompleted.ts:59).
  • 변경: 아카이브/배치 처리 시 유저 단위로 1회만 무효화, 또는 forDate lazy 만료에 의존(무효화 제거).

R3. dailyArchive/userVotes 전체 트리 read 축소 🟠

  • 현재: dailyArchive.ts:96이 전 유저·전 날짜 /userVotes 1회 로드(타깃 날짜만 필요).
  • 변경: shallow 조회 후 유저별 /userVotes/{uid}/{date}만, 또는 날짜 인덱스 경로 도입.

R4. 캐시 스탬피드 폴링 read 완화 🟠

  • 현재: waitForCache/getOrFetchDynamic(≤50회), schedule 월 폴링(≤50×30 doc). 락 경합·콜드스타트에서 대기요청당 수십 read.
  • 변경: 폴링 간격/횟수 백오프(지수), 락 보유자 결과를 pub/sub 알림으로 전달 검토.

C2/C3. 클라 중복 호출 제거 🟠

  • C2: rankh2h 동일 /kbo/rank 중복 → getRanks 연도 캐시 또는 단일 keepAlive provider 통합.
  • C3: pitchersacePitcher 동일 /kbo/player 2회 → acePitcherpitchers 파생으로.

Tier 3 — 소규모 · 정리성

  • W4: users 동일 cron run 2회 write(rankSnapshot+판정) → 판정 트랜잭션 내 스냅샷 통합(판정 전 rank 제약 유지).
  • W5: getMe photoUrl 자동 write 보수화(주기적/클라 명시 갱신).
  • W6: kboCache cron 전량 무효화 중 game_detail__는 TTL 자연만료에 위임(02:00 직후 미스 폭주 완화).
  • R5: 스코어보드 getUser .select 적용 + me rank 짧은 캐시/precompute 포함.
  • R6: createMe/updateMe 쓰기 후 2차 getUser 제거(반환값 합성).
  • R7: rank 다년도 getOrFetch 병렬화.
  • C4: 무캐시 autoDispose provider(currentUser/scoreboard/userStats)에 SWR/short-TTL, currentUser keepAlive 승격(스플래시 중복 제거).
  • Doc: gameList MemCache maxSize 미설정 + /metrics/gameListCache 미구현 → 코드 구현 또는 CACHING.md 정정(문서 드리프트).

운영/보안 (비용 외, 별도 처리 권장)

  • debug/forceSync·debug/dailyArchive 무인증 노출(debugHandlers.ts) — games 일괄 write·아카이브 전체 실행 가능. 운영 안정화 후 제거 또는 인증 게이트.

인덱스

  • 필요한 복합 인덱스 3종 모두 존재·쿼리 일치, 누락 없음(firestore.indexes.json).