Document the multi-layered caching architecture for KBO data.
- KBO 데이터 조회 성능 최적화를 위한 3계층 캐싱 전략 및 구조를 상세히 문서화했습니다. - Firestore 기반의 일 단위 스케줄 캐시와 경기 상태에 따른 동적 TTL 적용 로직을 명시했습니다. - 라이브 경기 정보 조회를 위한 10초 만료 메모리 캐시와 RTDB 기반의 메트릭 수집 방식을 설명했습니다. - 분산 락을 활용한 동시성 제어 및 매일 새벽 수행되는 캐시 무효화와 프리워밍 과정을 포함했습니다.
This commit is contained in:
parent
742d57c56d
commit
6145af7159
120
src/kbo/CACHING.md
Normal file
120
src/kbo/CACHING.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# KBO 캐싱 로직
|
||||||
|
|
||||||
|
KBO 외부 API 호출을 줄이기 위한 3계층 캐시 구조를 정리한다.
|
||||||
|
|
||||||
|
## 계층 요약
|
||||||
|
|
||||||
|
| 대상 | 저장소 | TTL | 관련 파일 |
|
||||||
|
|------|--------|-----|-----------|
|
||||||
|
| 스케줄 (경기 일정) | Firestore `kboCache` | 동적 (30s ~ 7d) | `repositories/kboRepository.ts` |
|
||||||
|
| 게임센터 (라이브) | Functions 메모리 (Map) | 10s | `services/gameListService.ts` |
|
||||||
|
| 팀 순위 / 선수 기록 | Firestore `kboCache` | 1h 고정 | `repositories/kboRepository.ts` |
|
||||||
|
|
||||||
|
공용 저수준 유틸: `repositories/kboCacheRepository.ts` (락·read·write·getOrFetch).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 스케줄 — Firestore 일 단위 캐시 + 동적 TTL
|
||||||
|
|
||||||
|
### 키 구조
|
||||||
|
- `schedule_day__{YYYYMMDD}__{team}__{series}` — 일자별 1개 doc
|
||||||
|
- 락: `schedule_month__{YYYY}__{M}__{team}__{series}` — 월 단위 fetch 직렬화
|
||||||
|
|
||||||
|
### doc 스키마
|
||||||
|
```ts
|
||||||
|
{ data: ScheduleGame[], updatedAt: Timestamp, ttlMs: number }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 동적 TTL 규칙 (`dayTtlMs`)
|
||||||
|
| 일자 | TTL |
|
||||||
|
|------|-----|
|
||||||
|
| 어제 이전 | 7d (단, 미종료 경기 남으면 30s — 자정 넘긴 연장 대응) |
|
||||||
|
| 오늘, 가장 이른 경기 시작 전 | `min(1h, earliestStart − now)` |
|
||||||
|
| 오늘, 경기 진행 중 | 30s |
|
||||||
|
| 오늘, 모든 경기 종료/취소 | 7d |
|
||||||
|
| 내일 (D+1) | 6h |
|
||||||
|
| D+2 이후 | 7d |
|
||||||
|
|
||||||
|
- `earliestStart` = 그날 경기들의 `time` 중 최솟값 (주말·더블헤더 등 18:30 외 시작 대응)
|
||||||
|
- "모두 종료" 판정: `status === "completed" || "cancelled"`
|
||||||
|
|
||||||
|
### 조회 흐름
|
||||||
|
|
||||||
|
**월 조회** (`?year=&month=`):
|
||||||
|
1. 해당 월의 모든 일자 키를 병렬 read
|
||||||
|
2. 모두 유효 → 합쳐 반환
|
||||||
|
3. 하나라도 만료/없음 → 월 단위 lock 획득 후 KBO 월 API 1회 호출
|
||||||
|
4. 응답을 일자별로 그룹화, **만료된 일자 doc만** set (유효한 doc은 쓰지 않음)
|
||||||
|
5. 합쳐 반환
|
||||||
|
|
||||||
|
**일 조회** (`?year=&month=&day=`):
|
||||||
|
1. 해당 일자 doc 1개 read (Firestore read **1회**)
|
||||||
|
2. 유효 → 반환
|
||||||
|
3. 만료/없음 → 월 흐름 재사용 (외부 1회 + 일자별 set) → 해당 일자만 재read하여 반환
|
||||||
|
|
||||||
|
### 비용 효과
|
||||||
|
- 일 조회: 30 reads → **1 read** (월 대비 1/30).
|
||||||
|
- 경기 중에도 오늘 doc만 30s 주기로 갱신. Firestore write는 10s~30s당 1건.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 게임센터 — 메모리 캐시 (10초)
|
||||||
|
|
||||||
|
### 저장
|
||||||
|
Functions 인스턴스의 `Map<string, { data, expiresAt }>`.
|
||||||
|
- 키: `${date}|${series}|${league}`
|
||||||
|
- TTL: 10초
|
||||||
|
- 사이즈 cap: 100개 (초과 시 가장 오래된 항목 evict)
|
||||||
|
|
||||||
|
### 조회 흐름
|
||||||
|
1. `memGet(key)` — TTL 이내면 즉시 반환 (Firestore/외부 API 호출 0)
|
||||||
|
2. 미스 → `fetchGameList()` 호출 → `memSet()`
|
||||||
|
|
||||||
|
### 특성
|
||||||
|
- **라이브 데이터 특성에 맞춘 짧은 TTL**. 10초 이내 재요청은 외부 호출 안 나감.
|
||||||
|
- 인스턴스별로 캐시가 흩어짐. 인스턴스가 cold start되면 캐시 초기화.
|
||||||
|
- CLI는 서비스 레이어를 우회하므로 캐시 적용 안 됨 (HTTP `/kbo/games`만 적용).
|
||||||
|
|
||||||
|
### 메트릭
|
||||||
|
`/metrics/gameListCache/{YYYYMMDD}/{hit|miss}` (RTDB).
|
||||||
|
`ServerValue.increment(1)`로 원자적 증가, fire-and-forget (오류 무시).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 팀 순위 / 선수 기록 — Firestore 1시간 캐시
|
||||||
|
|
||||||
|
`getOrFetch(key, ttlMs, fetcher)`로 처리. 기존 단순 TTL 캐시.
|
||||||
|
|
||||||
|
- 키: `rank__{year}`, `player__{type}__{year}__...`
|
||||||
|
- TTL: **1시간 고정**
|
||||||
|
- 파일: `repositories/kboRepository.ts`의 `fetchRankFromKbo`, `fetchPlayerFromKbo`
|
||||||
|
|
||||||
|
동적 TTL 대상이 아님 — 두 데이터는 하루 단위로 갱신되므로 1시간이면 충분.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 공용 인프라 (`kboCacheRepository.ts`)
|
||||||
|
|
||||||
|
### `getOrFetch(key, ttlMs, fetcher)`
|
||||||
|
캐시 우선 → 없으면 락 획득 후 `fetcher` 실행 → 결과 저장. 락 실패 시 다른 요청이 채운 캐시를 최대 25초 폴링 후 폴백 fetch.
|
||||||
|
|
||||||
|
### 분산 락
|
||||||
|
- 컬렉션: `kboLocks`
|
||||||
|
- TTL: 30초 (만료된 락은 트랜잭션 내에서 자동 갱신)
|
||||||
|
- `acquireLock` / `releaseLock` export됨
|
||||||
|
|
||||||
|
### 일일 무효화 크론 (`scheduled/kboRefresh.ts`)
|
||||||
|
매일 **02:00 KST** 실행.
|
||||||
|
- `invalidateByPrefix("rank__")` — 순위 전체 무효화
|
||||||
|
- `invalidateByPrefix("schedule_day__")` — 스케줄 전체 무효화
|
||||||
|
- 이후 현재 연도/월 prewarm (rank, schedule) + `syncGamesForMonth` 실행
|
||||||
|
|
||||||
|
이는 동적 TTL로 놓친 엣지 케이스(예: 장기간 trafficless인 키)를 위한 안전망.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 의도적 비캐싱
|
||||||
|
|
||||||
|
- **팀 순위/선수 기록의 연도별 분기** — 별 캐시 doc이라 문제 없음.
|
||||||
|
- **게임센터의 Firestore 캐시** — 라이브 데이터라 도입 안 함. 메모리 10s로 충분.
|
||||||
|
- **CLI 경로** — 프로세스가 매번 새로 뜨므로 메모리 캐시 의미 없음. Firestore 캐시는 CLI도 혜택.
|
||||||
Loading…
x
Reference in New Issue
Block a user