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

79 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Panit read/write 최적화 로드맵
> 근거: [`backend-writes.md`](./backend-writes.md) · [`backend-reads.md`](./backend-reads.md) · [`data-lifecycle.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.
- **변경**: 이미 존재하는 `forceSyncDay``getAll`+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}` 전체 삭제 → 다음 `/stats``voteHistory` 전체 풀스캔(시즌 누적 수십~수백 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**: `rank``h2h` 동일 `/kbo/rank` 중복 → `getRanks` 연도 캐시 또는 단일 keepAlive provider 통합.
- **C3**: `pitchers``acePitcher` 동일 `/kbo/player` 2회 → `acePitcher``pitchers` 파생으로.
---
## 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`).