- Firestore/RTDB read/write 패턴 전수 조사 결과를 문서화 - backend-writes.md, backend-reads.md, data-lifecycle.md, optimization-plan.md 추가
5.8 KiB
5.8 KiB
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위:
voteSummary10초 HTTP 폴링(시트 수만큼 병렬) — 유일한 상시 read 경로.
Tier 1 — 큰 절감 · 낮은 리스크 (우선 착수 권장)
W1. games 동기화에 diff 도입 🔴 최대 효과
- 현재:
syncGamesForMonth(gameSyncService.ts:63)가 매일 02:00 한 달치(수십~150+건)를 변경검사 없이merge:truebatch 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회만 무효화, 또는
forDatelazy 만료에 의존(무효화 제거).
R3. dailyArchive의 /userVotes 전체 트리 read 축소 🟠
- 현재:
dailyArchive.ts:96이 전 유저·전 날짜/userVotes1회 로드(타깃 날짜만 필요). - 변경: 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/player2회 →acePitcher를pitchers파생으로.
Tier 3 — 소규모 · 정리성
- W4:
users동일 cron run 2회 write(rankSnapshot+판정) → 판정 트랜잭션 내 스냅샷 통합(판정 전 rank 제약 유지). - W5:
getMephotoUrl 자동 write 보수화(주기적/클라 명시 갱신). - W6:
kboCachecron 전량 무효화 중game_detail__는 TTL 자연만료에 위임(02:00 직후 미스 폭주 완화). - R5: 스코어보드
getUser.select적용 +merank 짧은 캐시/precompute 포함. - R6:
createMe/updateMe쓰기 후 2차getUser제거(반환값 합성). - R7: rank 다년도
getOrFetch병렬화. - C4: 무캐시 autoDispose provider(
currentUser/scoreboard/userStats)에 SWR/short-TTL,currentUserkeepAlive 승격(스플래시 중복 제거). - Doc: gameList MemCache
maxSize미설정 +/metrics/gameListCache미구현 → 코드 구현 또는CACHING.md정정(문서 드리프트).
운영/보안 (비용 외, 별도 처리 권장)
debug/forceSync·debug/dailyArchive무인증 노출(debugHandlers.ts) —games일괄 write·아카이브 전체 실행 가능. 운영 안정화 후 제거 또는 인증 게이트.
인덱스
- 필요한 복합 인덱스 3종 모두 존재·쿼리 일치, 누락 없음(
firestore.indexes.json).