- Firestore/RTDB read/write 패턴 전수 조사 결과를 문서화 - backend-writes.md, backend-reads.md, data-lifecycle.md, optimization-plan.md 추가
79 lines
5.8 KiB
Markdown
79 lines
5.8 KiB
Markdown
# 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`).
|