캐시와 원격 페치를 조합할 때 로직이 뒤섞이기 쉽다. 오케스트레이션 함수로 묶으면 캐시 히트·미스 두 경로를 정리하고, 메트릭 계측과 부작용 위임도 한 곳에서 관리할 수 있다.
타입 정의
interface CacheProvider<T> { get(key: string): Promise<T | undefined>; set(key: string, value: T): Promise<void>;}
interface RemoteProvider<T> { fetch(key: string): Promise<T>;}
interface MetricsReporter { recordCacheHit(key: string, latencyMs: number): void; recordCacheMiss(key: string): void; recordFetchLatency(key: string, latencyMs: number): void; recordOutcome(key: string, outcome: 'success' | 'error', latencyMs: number): void;}오케스트레이션 함수
async function orchestrate<T>( key: string, cache: CacheProvider<T>, remote: RemoteProvider<T>, metrics: MetricsReporter,): Promise<T> { const start = performance.now();
// 빠른 경로: 캐시 히트 const cached = await cache.get(key); if (cached !== undefined) { metrics.recordCacheHit(key, performance.now() - start); return cached; }
// 느린 경로: 라이브 페치 metrics.recordCacheMiss(key); const fetchStart = performance.now(); try { const data = await remote.fetch(key); metrics.recordFetchLatency(key, performance.now() - fetchStart);
// 부작용은 외부에 위임 (fire and forget) void cache.set(key, data); metrics.recordOutcome(key, 'success', performance.now() - start); return data; } catch (error) { metrics.recordOutcome(key, 'error', performance.now() - start); throw error; }}사용 예
const result = await orchestrate( 'user:42', redisCache, userApiClient, datadogMetrics,);해설
- 빠른 경로: 캐시에 히트하면 즉시 반환. 원격은 호출하지 않는다.
- 느린 경로: 캐시 미스 시에만 원격으로 폴백. 데이터를 가져온 후 캐시를 업데이트한다 (fire and forget).
- 메트릭: 캐시 히트·미스·페치 레이턴시·최종 결과를 모두 계측.
MetricsReporter인터페이스에 위임하므로 Datadog / Prometheus / 로그 등 구현에 의존하지 않는다. - 부작용 위임: 캐시 쓰기와 메트릭 전송은 오케스트레이션 함수 외부로 분리. 테스트가 용이하고 구현을 교체하기 쉽다.
응용
이 패턴은 다음과 같은 처리에 적합하다.
- 임포터: 외부 데이터 소스에서 가져올 때 이미 처리된 데이터를 빠르게 건너뜀
- 데이터 보강 작업: 이미 완성된 엔티티를 재처리하지 않음
- 동기화 핸들러: 중복 페치를 방지하면서 메트릭으로 차이를 추적
- 비용이 높은 UI 작업: 최초 로드만 원격을 호출하고 이후에는 캐시로 응답
CacheProvider / RemoteProvider / MetricsReporter는 각각 인터페이스로 분리되어 있어, Redis/in-memory, REST/gRPC, Datadog/StatsD 등 임의의 구현을 조합할 수 있다.
hsb.horse