@fluojs/cache-manager 1.0.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -13,9 +13,13 @@
13
13
  - [애플리케이션 레벨 캐싱](#애플리케이션-레벨-캐싱)
14
14
  - [공통 패턴](#공통-패턴)
15
15
  - [Redis 저장소 사용](#redis-저장소-사용)
16
+ - [TTL 지터](#ttl-지터)
16
17
  - [쿼리 매개변수 기반 캐싱](#쿼리-매개변수-기반-캐싱)
17
18
  - [캐시 소유권과 reset 범위](#캐시-소유권과-reset-범위)
19
+ - [캐시 작업 관찰](#캐시-작업-관찰)
20
+ - [비동기 설정](#비동기-설정)
18
21
  - [수동 모듈 조합](#수동-모듈-조합)
22
+ - [NestJS 캐시 마이그레이션](#nestjs-캐시-마이그레이션)
19
23
  - [공개 API 개요](#공개-api-개요)
20
24
  - [관련 패키지](#관련-패키지)
21
25
  - [예제 소스](#예제-소스)
@@ -26,14 +30,18 @@
26
30
  npm install @fluojs/cache-manager
27
31
  ```
28
32
 
29
- root `@fluojs/cache-manager` import는 memory-only 설치에서도 안전합니다. Redis peer는 Redis 저장소 경로를 명시적으로 선택할 때만 필요합니다.
33
+ `@fluojs/cache-manager`는 Node.js `>=24.0.0 <27`을 지원하며 `engines.node`로 정확히 이 범위를 선언합니다. 이 package-owned 지원 계약에 따라 Node 24 미만과 Node 27 이상은 제외됩니다. 이전 1.x 릴리스는 `engines.node >=20.0.0`을 광고했지만, 이는 실제 dependency floor와 일치한 적이 없습니다.
30
34
 
31
- Redis 기반 캐싱을 사용하는 경우:
35
+ root `@fluojs/cache-manager` import는 memory-only 설치에서도 안전합니다. Redis client는 Redis 저장소 경로를 명시적으로 선택할 때만 필요합니다.
36
+
37
+ Lifecycle이 관리되는 `@fluojs/redis` client로 Redis 기반 캐싱을 사용하는 경우:
32
38
 
33
39
  ```bash
34
40
  npm install @fluojs/cache-manager @fluojs/redis ioredis
35
41
  ```
36
42
 
43
+ 대신 애플리케이션이 소유하는 compatible client를 `redis.client`로 직접 전달할 수 있습니다. 이 경로에는 `@fluojs/redis`가 필요하지 않습니다. 필수 `get`, `set`, `del`, tuple-returning `scan` operation을 제공하는 client package를 설치하고, 해당 client는 애플리케이션 lifecycle에서 닫으세요.
44
+
37
45
  ## 사용 시점
38
46
 
39
47
  - 비용이 많이 드는 데이터베이스 쿼리나 외부 API 응답을 캐싱하고 싶을 때 사용합니다.
@@ -96,16 +104,30 @@ class UserService {
96
104
 
97
105
  ### Redis 저장소 사용
98
106
 
99
- Redis를 사용하려면 `@fluojs/redis`가 설정되어 있어야 하며, `store` 옵션을 `'redis'`로 설정합니다.
107
+ `store: 'redis'`를 설정한 지원되는 client 통합 경로 중 하나를 선택합니다.
108
+
109
+ 1. 기본 또는 named raw client를 `@fluojs/redis`로 등록하고 cache module이 DI를 통해 해석하도록 합니다.
110
+ 2. 애플리케이션이 소유하는 `RedisCompatibleClient`를 `redis.client`로 직접 전달합니다.
100
111
 
101
112
  memory-only 소비자는 `@fluojs/redis`나 `ioredis`를 설치하지 않아도 `@fluojs/cache-manager`를 계속 import할 수 있습니다. 이 optional peer들은 Redis 저장소 경로를 선택할 때만 해석됩니다.
102
113
 
103
114
  ```typescript
104
- CacheModule.forRoot({
105
- store: 'redis',
106
- ttl: 600,
107
- keyPrefix: 'myapp:cache:',
115
+ import { Module } from '@fluojs/core';
116
+ import { CacheModule } from '@fluojs/cache-manager';
117
+ import { RedisModule } from '@fluojs/redis';
118
+
119
+ @Module({
120
+ imports: [
121
+ RedisModule.forRoot({ name: 'cache', host: 'localhost', port: 6379 }),
122
+ CacheModule.forRoot({
123
+ store: 'redis',
124
+ ttl: 600,
125
+ keyPrefix: 'myapp:cache:',
126
+ redis: { clientName: 'cache' },
127
+ }),
128
+ ],
108
129
  })
130
+ class AppModule {}
109
131
  ```
110
132
 
111
133
  여러 Redis 클라이언트를 등록했다면 `redis.clientName`으로 사용할 `@fluojs/redis` 연결을 지정할 수 있습니다.
@@ -119,13 +141,54 @@ CacheModule.forRoot({
119
141
  })
120
142
  ```
121
143
 
122
- `redis.client`는 여전히 가장 높은 우선순위의 명시적 override입니다. DI 기반 선택을 완전히 우회해야 때만 사용하세요.
144
+ `redis.client`는 가장 높은 우선순위의 override이며 DI 기반 client 선택을 완전히 우회합니다. Export된 `RedisCompatibleClient` 계약을 만족하는 모든 client를 받을 수 있고, 이 경로에서는 `@fluojs/redis`를 load하거나 요구하지 않습니다. 직접 전달한 client의 connection startup과 shutdown은 애플리케이션이 소유합니다.
145
+
146
+ ```typescript
147
+ import Redis from 'ioredis';
148
+ import { Module } from '@fluojs/core';
149
+ import { CacheModule } from '@fluojs/cache-manager';
150
+
151
+ const cacheClient = new Redis({ host: 'localhost', port: 6379 });
152
+
153
+ @Module({
154
+ imports: [
155
+ CacheModule.forRoot({
156
+ store: 'redis',
157
+ keyPrefix: 'myapp:cache:',
158
+ redis: { client: cacheClient },
159
+ }),
160
+ ],
161
+ })
162
+ class AppModule {}
163
+ ```
123
164
 
124
165
  내장 `RedisStore`는 엔트리를 `JSON.stringify(...)`로 저장합니다. 따라서 캐시 값은 JSON 호환 형태여야 합니다. 일반 객체, 배열, 문자열, 숫자, 불리언, `null`은 안정적으로 round-trip 되지만, `Date`는 JSON 결과(예: ISO 문자열)로 돌아오고, 함수/`undefined`/`symbol`은 유지되지 않으며, `bigint`나 순환 그래프처럼 직렬화 불가능한 값은 캐싱 전에 정규화해야 합니다.
125
166
 
126
167
  양수 Redis TTL 값은 초 단위로 받으며 소수도 허용됩니다. Redis `EX`는 정수 초를 사용하므로 Redis 만료 시간은 다음 정수 초로 올림하지만, fluo는 저장된 엔트리 안에 밀리초 정밀도의 만료 timestamp도 기록하고 해당 timestamp에 도달하면 값을 만료된 것으로 처리합니다. Redis 만료를 의도적으로 사용하지 않으려면 `ttl: 0`을 사용하세요.
168
+ 예외적으로 큰 유한 TTL 값은 두 내장 store 모두에서 가장 큰 안전한 JavaScript 만료 timestamp로 제한되므로 Redis JSON metadata는 유한하게 유지되고 memory 경로와 일치합니다.
169
+
170
+ Redis reset 소유권은 기본값이 `fluo:cache:`이며 내장 `RedisStore` namespace로 전달되는 top-level `keyPrefix` 옵션으로 제한됩니다. Redis 기반 저장소에서 `CacheService.reset()`은 해당 prefix 아래의 키만 삭제하므로, cache prefix 밖의 애플리케이션 소유 Redis 데이터는 유지됩니다. 비어 있지 않은 prefix의 Redis glob metacharacter(`*`, `?`, `[`, `]`, `\`)는 `SCAN` 전에 escape되므로 설정한 prefix가 reset 소유권을 넓히지 않고 literal namespace로 유지됩니다. 의도적으로 빈 `keyPrefix`를 설정하면 reset은 `*`를 scan하지 않고 현재 `RedisStore` 인스턴스가 쓴 키로만 제한됩니다. 재시작 이후나 여러 프로세스에 걸친 캐시 엔트리까지 reset해야 한다면 비어 있지 않은 애플리케이션 전용 prefix를 사용하세요.
171
+
172
+ ### TTL 지터
127
173
 
128
- Redis reset 소유권은 기본값이 `fluo:cache:`이며 내장 `RedisStore` namespace로 전달되는 top-level `keyPrefix` 옵션으로 제한됩니다. Redis 기반 저장소에서 `CacheService.reset()`은 해당 prefix 아래의 키만 삭제하므로, cache prefix 밖의 애플리케이션 소유 Redis 데이터는 유지됩니다. 의도적으로 `keyPrefix`를 설정하면 reset은 `*`를 scan하지 않고 현재 `RedisStore` 인스턴스가 쓴 키로만 제한됩니다. 재시작 이후나 여러 프로세스에 걸친 캐시 엔트리까지 reset해야 한다면 비어 있지 않은 애플리케이션 전용 prefix를 사용하세요.
174
+ 함께 기록된 인기 키는 같은 시점에 만료되어 origin 부하를 동기화할 있습니다. `ttlJitter`를 사용하면 양수 TTL 지터를 중앙에서 opt-in할 있습니다. `CacheService`는 memory, Redis 또는 custom store에 쓰기를 넘기기 전에 유효 TTL을 계산합니다.
175
+
176
+ ```typescript
177
+ CacheModule.forRoot({
178
+ store: 'redis',
179
+ ttl: 600,
180
+ ttlJitter: {
181
+ ratio: 0.1,
182
+ mode: 'symmetric',
183
+ },
184
+ });
185
+ ```
186
+
187
+ `ratio`는 `0`보다 크고 `1` 이하여야 합니다. 기본 `symmetric` mode는 `ttl ± (ttl * ratio)` 범위에서 값을 뽑고, `shorten`은 TTL을 줄이기만 하며 `lengthen`은 늘리기만 합니다. `CacheService.set(...)` 또는 `remember(...)`의 per-call TTL override가 있으면 module 기본값 대신 해당 값에 지터를 적용합니다. `ttl: 0`은 계속 만료 없음 쓰기이며, 음수 또는 유한하지 않은 TTL 값은 여전히 쓰기를 건너뜁니다.
188
+
189
+ `ttlJitter`를 생략하거나 `undefined`로 설정한 경우에만 지터가 비활성화됩니다. `null`, primitive, array 및 invalid option field는 module 등록 중 거부됩니다. Optional `random` 함수는 deterministic test seam이며 `[0, 1]` 범위의 유한한 값을 반환해야 합니다. Invalid sample은 coercion하지 않고 write를 거부합니다. Production code에서는 일반적으로 기본 `Math.random`을 유지하세요.
190
+
191
+ 지터가 적용된 모든 양수 TTL은 선택한 방향 범위 안에서 양수이자 유한한 값으로 유지됩니다. 완전히 단축된 TTL은 no-expiry sentinel이 되지 않고 JavaScript의 가장 작은 양수 유한값을 사용하며, 표현 가능한 범위를 넘는 증가 결과는 `Number.MAX_VALUE`에서 포화됩니다. TTL 지터는 만료 시점을 분산할 뿐입니다. Distributed locking, refresh-ahead caching 또는 cross-instance stampede coordination이 아닙니다.
129
192
 
130
193
  ### 쿼리 매개변수 기반 캐싱
131
194
 
@@ -140,7 +203,7 @@ CacheModule.forRoot({
140
203
  })
141
204
  ```
142
205
 
143
- 완전히 다른 키 전략이 필요하다면 `httpKeyStrategy`에 함수를 전달하거나, literal key 또는 key factory를 받는 `@CacheKey(...)`를 사용하세요. 요청을 인식하는 cache key를 만들 때 지원되는 확장 경로는 이러한 function-based hook이며, cache key 생성만 바꾸기 위해 `CacheInterceptor`를 subclass하지 않습니다.
206
+ 완전히 다른 키 전략이 필요하다면 `httpKeyStrategy`에 함수를 전달하거나, literal key 또는 key factory를 받는 `@CacheKey(...)`를 사용하세요. 빈 literal `@CacheKey('')`도 명시적인 key로 유지되며, decorator metadata가 없을 때만 설정된 `httpKeyStrategy`를 선택합니다. 요청을 인식하는 cache key를 만들 때 지원되는 확장 경로는 이러한 function-based hook이며, cache key 생성만 바꾸기 위해 `CacheInterceptor`를 subclass하지 않습니다.
144
207
 
145
208
  ```typescript
146
209
  CacheModule.forRoot({
@@ -170,7 +233,9 @@ HTTP 인터셉터는 나중에 재사용할 수 있는 값이 있는 성공한,
170
233
 
171
234
  ### 캐시 소유권과 reset 범위
172
235
 
173
- `CacheService.reset()`은 관련 없는 애플리케이션 상태가 아니라 설정된 store가 소유한 엔트리만 삭제합니다. 또한 진행 중인 `remember(...)` bookkeeping을 제거하므로 reset 전에 시작된 loader가 reset 완료 stale 엔트리를 다시 채우지 못합니다. 내장 메모리 저장소에서는 해당 store 인스턴스가 보유한 in-process 엔트리를 의미합니다. Redis에서는 설정된 `keyPrefix` namespace가 소유권 경계입니다. 공유 Redis 배포에서는 기본 `fluo:cache:`를 유지하거나 `myapp:cache:`처럼 전용 prefix를 선택하세요.
236
+ 일반적인 `get(...)`, `set(...)`, `del(...)` 호출은 설정된 store에 대해 동시에 실행되므로, 키의 느린 store 호출이 관련 없는 키를 지연시키지 않습니다.
237
+
238
+ `CacheService.reset()`은 관련 없는 애플리케이션 상태가 아니라 설정된 store가 소유한 엔트리만 삭제합니다. 또한 reset 경계에서 store read/write를 직렬화하고 진행 중인 `remember(...)` loader를 무효화하므로, reset 전에 시작된 loader가 reset 완료 후 stale 엔트리를 다시 채우지 못합니다. 내장 메모리 저장소에서는 해당 store 인스턴스가 보유한 in-process 엔트리를 의미합니다. Redis에서는 설정된 `keyPrefix` namespace가 소유권 경계입니다. 공유 Redis 배포에서는 기본 `fluo:cache:`를 유지하거나 `myapp:cache:`처럼 전용 prefix를 선택하세요.
174
239
 
175
240
  ```typescript
176
241
  CacheModule.forRoot({
@@ -181,10 +246,89 @@ CacheModule.forRoot({
181
246
 
182
247
  Redis cache prefix를 cache가 아닌 데이터와 공유하지 마세요. `del(key)`은 이 패키지가 해석한 정확한 캐시 키를 삭제하고, `reset()`은 위에서 설명한 store 소유 캐시 namespace만 삭제합니다.
183
248
 
184
- 애플리케이션이 종료될 때 `CacheService`는 `close()` 또는 `dispose()`를 노출하는 custom store로 shutdown을 전달합니다. store가 socket, pool, timer 또는 기타 외부 리소스를 소유한다면 이 optional hook 중 하나를 사용하세요.
249
+ 애플리케이션이 종료될 때 `CacheService`는 새 store read/write를 중단하고 이미 시작된 store 작업을 기다린 뒤, `close()` 또는 `dispose()`를 노출하는 custom store로 shutdown을 전달합니다. 동시에 또는 반복해서 호출된 `close()`와 lifecycle hook은 첫 teardown의 완료 및 실패를 공유하므로, store teardown은 한 번만 실행되고 모든 호출자가 같은 shutdown 경계를 관찰합니다. store가 socket, pool, timer 또는 기타 외부 리소스를 소유한다면 이 optional hook 중 하나를 사용하세요.
185
250
 
186
251
  `CacheStore` 계약을 구현한 custom store는 `store` 옵션에 직접 전달할 수 있습니다. in-process LRU store, Redis 외 원격 캐시, 또는 cache operation을 관찰해야 하는 테스트 더블에 적합합니다.
187
252
 
253
+ ### 캐시 작업 관찰
254
+
255
+ 플랫폼 status helper는 캐시 가용성만 보고합니다. hit rate, latency, error outcome을 측정하려면 `CacheModule.forRoot(...)`에 opt-in `observer`를 전달하세요. 이 observer는 `@fluojs/metrics`와 독립적이므로, 애플리케이션이 이미 사용하는 metrics backend에 자유롭게 연결할 수 있습니다.
256
+
257
+ ```typescript
258
+ import { CacheModule, type CacheObservation } from '@fluojs/cache-manager';
259
+
260
+ CacheModule.forRoot({
261
+ store: 'memory',
262
+ observer: {
263
+ onCacheOperation(observation: CacheObservation) {
264
+ cacheOperationCounter.inc({
265
+ operation: observation.operation,
266
+ outcome: observation.outcome,
267
+ });
268
+ cacheOperationLatency.observe(observation.durationMs);
269
+ },
270
+ },
271
+ });
272
+ ```
273
+
274
+ 이 계약은 의도적으로 좁게 정의되어 있습니다.
275
+
276
+ - **프라이버시**: observation은 `operation`, `outcome`, `durationMs`만 전달합니다. cache key, 캐시된 값, loader 결과, error 객체는 observer로 전달되지 않으므로 계측이 애플리케이션 데이터를 유출할 수 없습니다.
277
+ - **operation taxonomy**: `operation`은 `get`, `set`, `del`, `remember`, `reset`, `close` 중 하나입니다. `remember`는 호출당 한 번 보고되며, 내부 read는 별도의 `get`으로 보고되지 않습니다.
278
+ - **outcome**: `CacheObservation`은 discriminated union입니다. read 작업(`get`, `remember`)은 `hit`, `miss`, `error`만 보고할 수 있고, write, invalidation, lifecycle 작업은 `success`, `error`만 보고할 수 있습니다. 같은 key의 in-flight load에 합류한 `remember` 호출은 캐시된 값을 읽지 않았으므로 `miss`를 보고합니다.
279
+ - **timing**: `durationMs`는 런타임의 monotonic `performance.now()` clock을 사용하여 store queue 직렬화를 포함한 전체 `CacheService` 작업 시간을 측정합니다.
280
+ - **실패 격리**: observer 오류는 삼켜집니다. throw된 error나 rejected promise는 caller가 받는 값을 바꾸지 않고 unhandled rejection으로도 노출되지 않습니다. observer 작업은 cache 작업이 await하지 않습니다.
281
+ - **HTTP fail-soft 상호작용**: `CacheInterceptor`는 여전히 store 실패를 삼켜서 캐시 문제가 정상 핸들러를 실패시키지 않도록 합니다. observer는 그 실패를 `error` observation으로 확인하므로, 요청 처리를 유지하면서 저하된 캐시를 알림하는 지원 경로가 됩니다.
282
+
283
+ `observer`를 설정하지 않으면 캐시는 관찰 작업 없이 기존 코드 경로 그대로 동작합니다.
284
+ Lifecycle diagnostic은 shutdown이 실제로 사용하는 teardown 소유자를 그대로 보고합니다. `createCacheManagerPlatformStatusSnapshot(...)`은 모든 non-memory store를 같게 취급하지 않고 lifecycle 책임에서 소유권을 해석합니다.
285
+
286
+ - 내장 메모리 store는 프레임워크가 in-process로 생성하고 보유하므로 `framework` 소유입니다.
287
+ - Custom store는 `CacheService.close()`가 optional `close()` 또는 `dispose()` hook으로 teardown을 전달할 책임을 가지므로 기본적으로 `framework` 소유입니다.
288
+ - Redis store는 client를 닫지 않는 `CacheService`에 대해 `external`입니다. Cache module이 `@fluojs/redis`를 통해 client를 해석하면 해당 integration이 lifecycle을 소유하고, `redis.client`로 client를 직접 전달하면 애플리케이션이 lifecycle을 소유합니다.
289
+
290
+ 명시적인 `storeOwnershipMode`는 store 기본값보다 우선합니다. 애플리케이션이 custom store의 lifecycle 책임을 의도적으로 유지하는 경우 `external`로 설정하세요.
291
+
292
+ ### 비동기 설정
293
+
294
+ 최종 store, TTL, `keyPrefix`, key strategy를 DI나 비동기 bootstrap 작업에서 결정해야 한다면 `CacheModule.forRootAsync(...)`를 사용합니다. 의존성 토큰을 `inject`에 나열하고 `useFactory`에서 일반 `CacheModuleOptions`를 반환하면, module이 `CacheModule.forRoot(...)`와 동일한 기본값으로 그 결과를 정규화합니다.
295
+
296
+ ```typescript
297
+ import { Module } from '@fluojs/core';
298
+ import { CacheModule } from '@fluojs/cache-manager';
299
+
300
+ import { CacheSettingsService } from './cache-settings.service';
301
+
302
+ @Module({
303
+ imports: [
304
+ CacheModule.forRootAsync({
305
+ inject: [CacheSettingsService],
306
+ useFactory: async (settings: CacheSettingsService) => ({
307
+ store: 'redis',
308
+ ttl: await settings.resolveTtlSeconds(),
309
+ keyPrefix: settings.keyPrefix,
310
+ redis: { clientName: 'cache' },
311
+ }),
312
+ }),
313
+ ],
314
+ })
315
+ class AppModule {}
316
+ ```
317
+
318
+ Inject한 토큰은 cache module을 생성하는 container에 보여야 합니다. Cache options provider가 resolve되기 전에 bootstrap runtime provider로 제공하거나 globally visible한 imported module에서 export하세요. Import하는 parent module에만 local인 provider나 일반 sibling/parent export는 async cache module에 보이지 않습니다. Factory는 cache provider가 처음 resolve될 때 등록마다 한 번 실행되며, factory가 reject되면 부분적으로 설정된 cache를 등록하지 않고 bootstrap이 실패합니다.
319
+
320
+ 모듈 가시성은 등록 호출이 소유합니다. 전역으로 노출하려면 `CacheModule.forRootAsync({ global: true, ... })`처럼 전달하세요. `useFactory`는 `global` property를 포함한 준비된 `CacheModuleOptions` 값을 반환할 수 있으며, module metadata는 factory 실행 전에 확정되므로 반환된 `global`은 무시됩니다.
321
+
322
+ ```typescript
323
+ CacheModule.forRootAsync({
324
+ global: true,
325
+ inject: [CacheSettingsService],
326
+ useFactory: (settings: CacheSettingsService) => ({ store: settings.store }),
327
+ })
328
+ ```
329
+
330
+ 비동기 경로도 `forRoot(...)`와 동일한 store 선택을 지원합니다. `'memory'`, DI로 해석하거나 직접 전달한 client를 사용하는 `'redis'`, 그리고 모든 custom `CacheStore` instance를 사용할 수 있습니다.
331
+
188
332
  ### 수동 모듈 조합
189
333
 
190
334
  일반적인 애플리케이션 설정과 커스텀 `defineModule(...)` 조합에서는 `CacheModule.forRoot(...)`를 사용합니다.
@@ -201,6 +345,33 @@ defineModule(ManualCacheModule, {
201
345
  });
202
346
  ```
203
347
 
348
+ ### NestJS 캐시 마이그레이션
349
+
350
+ `@nestjs/cache-manager`와 `@fluojs/cache-manager`는 cache 개념이 일부 겹치지만 option 이름, 단위, 기본값, 소유권이 모두 그대로 유지되지는 않습니다. 아래 항목을 각각 변환하고, 전체 마이그레이션 계약은 [NestJS → fluo Migration Map](../../docs/getting-started/migrate-from-nestjs.ko.md)을 참고하세요.
351
+
352
+ | NestJS option 또는 decorator | fluo 대응 | 변환 규칙 |
353
+ | --- | --- | --- |
354
+ | 설치된 underlying `cache-manager` generation이 millisecond를 사용하는 경우의 `ttl` | 초 단위 `ttl` | 설치된 underlying `cache-manager` dependency/version을 확인하세요. 해당 generation이 TTL을 millisecond로 정의할 때에만 1000으로 나눕니다. `ttl`을 생략하면 memory 경로는 `300`초를, `redis` 및 custom-store 경로는 `0`을 적용합니다. |
355
+ | `ttl: 0` | `ttl: 0` | "캐싱하지 않음"이 아니라 만료 없음을 뜻합니다. 음수이거나 유한하지 않은 값은 잘못된 값으로 처리되어 `CacheService.set(...)`은 쓰기를 건너뛰고 `CacheInterceptor`는 해당 handler의 cache 읽기와 쓰기를 모두 건너뜁니다. |
356
+ | `@CacheTTL(...)` | `@CacheTTL(ttlSeconds: number)` | 정적 숫자 하나만 받습니다. 요청마다 달라지는 lifetime은 `CacheService.set(key, value, ttlSeconds)`로 옮기세요. |
357
+ | 암묵적 query 민감 key | `httpKeyStrategy` | 기본값은 path만 사용하는 `'route'`입니다. 응답이 query parameter에 따라 달라지면 `'route+query'`(또는 `'full'`), function strategy, `@CacheKey(...)` 중 하나를 선택하세요. |
358
+ | `isGlobal: true` | `global: true` | NestJS `isGlobal`과 fluo `global`은 모두 기본값이 `false`이므로, 명시적으로 opt-in하거나 cache provider를 resolve하는 모든 module에 import하지 않으면 두 cache module 모두 module-local로 유지됩니다. |
359
+ | `cache-manager-redis-store` 같은 NestJS store adapter | `store: 'redis'` 또는 `CacheStore` 객체 | NestJS adapter는 `CacheStore` 계약을 만족하지 않습니다. 내장 Redis 경로를 쓰거나 callback/options 완료를 Promise로 변환하고, `ttlSeconds`를 legacy TTL 초 단위로 매핑하며, `reset()`이 cache namespace만 비우도록 adapter를 감싸세요. `reset()`을 whole-database `flushDb`로 무분별하게 전달하면 안 됩니다. |
360
+ | adapter가 소유하던 client teardown | store의 `close()` / `dispose()` | 애플리케이션 shutdown은 이 optional hook에만 teardown을 전달합니다. `redis.client`로 전달한 raw client는 애플리케이션 소유로 남아 애플리케이션 lifecycle에서 닫아야 합니다. |
361
+
362
+ ```typescript
363
+ CacheModule.forRoot({
364
+ // 설치된 underlying cache-manager generation이 milliseconds를 사용할 때
365
+ // NestJS `ttl: 60_000`은 60초가 됩니다.
366
+ ttl: 60,
367
+ // NestJS `isGlobal: true` becomes `global: true`.
368
+ global: true,
369
+ // Opt in explicitly when responses vary by query parameters.
370
+ httpKeyStrategy: 'route+query',
371
+ store: 'redis',
372
+ })
373
+ ```
374
+
204
375
  ### 메모리 저장소 운영 한계
205
376
 
206
377
  내장 메모리 저장소는 단일 프로세스의 bounded cache 용도로 설계되어 있습니다.
@@ -211,29 +382,52 @@ defineModule(ManualCacheModule, {
211
382
 
212
383
  ### 지연 삭제 시점
213
384
 
214
- `@CacheEvict(...)`가 붙은 non-GET 핸들러는 응답이 성공적으로 commit된 뒤에 캐시를 삭제합니다. `response.send(...)`가 reject되면 지연 eviction을 취소하여 실패한 commit이전 캐시된 읽기 결과를 삭제하지 않도록 합니다. 어댑터 경로가 `response.send(...)`를 호출하지 않더라도, 인터셉터는 bounded fallback timer를 통해 성공한 쓰기 이후 stale 엔트리가 무기한 남지 않도록 보장합니다. 또한 지연 eviction 실패는 인터셉터 내부에 containment되어 cache key factory나 cache store 삭제 오류가 응답 이후 unhandled promise rejection으로 노출되지 않습니다.
385
+ `@CacheEvict(...)`는 범용 service-method decorator가 아니라 HTTP route metadata입니다. `CacheInterceptor`가 non-GET controller handler를 감싸 실행될 때만 metadata를 소비합니다. Service method나 HTTP interceptor pipeline 밖의 호출에서는 `CacheService`를 주입하고 `del(...)`을 명시적으로 호출하세요.
386
+
387
+ ```typescript
388
+ import { CacheEvict, CacheInterceptor } from '@fluojs/cache-manager';
389
+ import { Controller, Post, UseInterceptors } from '@fluojs/http';
390
+
391
+ @Controller('/products')
392
+ @UseInterceptors(CacheInterceptor)
393
+ class ProductController {
394
+ @Post('/refresh')
395
+ @CacheEvict('/products')
396
+ refresh() {
397
+ return { refreshed: true };
398
+ }
399
+ }
400
+ ```
401
+
402
+ 이렇게 지원되는 HTTP 경로에서는 framework response writer가 성공적으로 settle되고 response가 commit 완료를 보고할 때까지 cache eviction을 지연합니다. Writer가 reject되거나 commit 확인 없이 settle되거나, disconnect 또는 shutdown으로 commit 전에 request가 abort되면 지연 eviction을 취소하여 이전 cached read 결과를 유지합니다. `response.send(...)`를 호출하지 않고 commit하는 adapter 경로도 bounded 5초 fallback을 유지합니다. 이 fallback은 deadline에 `response.committed`가 이미 commit을 확인한 경우에만 eviction을 실행하고, 확인되지 않은 response는 취소하므로 경과 시간만으로 이후의 실패한 commit보다 먼저 cache를 삭제하지 않습니다. Fallback timer는 Node.js에서 unref되고 response writer가 settle되면 clear되므로 pending fallback work가 process shutdown을 계속 붙잡지 않습니다. 또한 지연 eviction 실패는 interceptor 내부에 containment되어 cache key factory나 cache store 삭제 오류가 response 이후 unhandled promise rejection으로 노출되지 않습니다.
215
403
 
216
404
  ## 공개 API 개요
217
405
 
218
406
  ### 모듈
219
- - `CacheModule.forRoot(options)`: 캐시 저장소(memory/redis/custom), 기본 TTL, 키 전략, `global`, `principalScopeResolver`, Redis namespace `keyPrefix`, `redis.scanCount` 같은 Redis 옵션을 설정합니다.
407
+ - `CacheModule.forRoot(options)`: 캐시 저장소(memory/redis/custom), 기본 TTL, opt-in `ttlJitter`, 키 전략, `global`, `principalScopeResolver`, Redis namespace `keyPrefix`, `redis.scanCount` 같은 Redis 옵션을 설정합니다.
220
408
  애플리케이션 모듈에서 사용하는 기본 패키지 진입점입니다.
409
+ - `CacheModule.forRootAsync({ inject, useFactory, global? })`: cache 설정을 DI나 비동기 bootstrap 작업에서 만드는 애플리케이션을 위해 동일한 옵션을 injected factory로 해석합니다. `global`은 이 등록 호출이 소유하며, factory가 reject되면 bootstrap이 실패합니다.
221
410
 
222
411
  ### 공개 타입
223
- - `CacheModuleOptions`: `CacheModule.forRoot(...)`가 받는 애플리케이션-facing 설정입니다.
412
+ - `CacheModuleOptions`: `CacheModule.forRoot(...)`가 받는 애플리케이션-facing 설정이며 optional `ttlJitter`와 `observer`를 포함합니다.
413
+ - `CacheTtlJitterOptions`, `CacheTtlJitterMode`: Opt-in 양수 TTL 지터의 범위, 방향, deterministic randomness seam을 정의합니다.
414
+ - `NormalizedCacheTtlJitterOptions`: 기본값이 적용된 정규화 TTL 지터 설정입니다.
415
+ - `CacheObserver`: 단일 `onCacheOperation(observation)` 메서드를 가지는 opt-in 관찰 hook입니다.
416
+ - `CacheObservation`: 각 operation category를 유효한 outcome과 결합하고 `durationMs`를 전달하는 privacy-safe discriminated union입니다.
417
+ - `CacheAsyncModuleOptions`: `CacheModule.forRootAsync(...)`가 받는 injected-factory 설정입니다. `useFactory`는 `CacheModuleOptions`를 반환하며, module visibility는 등록 수준의 `global`만 따릅니다.
224
418
  - `NormalizedCacheModuleOptions`: 기본값이 적용된 정규화 설정 모양과 일치하는 compatibility-only type export입니다. 애플리케이션 코드에서는 `CacheModuleOptions`를 우선 사용하세요. 이 타입은 이전에 배포된 declaration surface를 참조한 소비자가 계속 컴파일되도록 공개 상태를 유지합니다.
225
419
 
226
420
  ### 서비스
227
- - `CacheService`: 수동 캐시 작업(`get`, `set`, `del`, `remember`, `reset`, `close`)을 위한 기본 API입니다. 애플리케이션 shutdown은 같은 `close()` 경로를 호출하며, 이 경로는 `close()` 또는 `dispose()`를 노출하는 custom store로 teardown을 전달합니다.
421
+ - `CacheService`: 수동 캐시 작업(`get`, `set`, `del`, `remember`, `reset`, `close`)을 위한 기본 API입니다. 애플리케이션 shutdown은 같은 `close()` 경로를 호출하며, 이 경로는 `close()` 또는 `dispose()`를 노출하는 custom store로 teardown을 전달하고 동시에 또는 반복해서 호출한 caller가 첫 teardown 완료를 공유하도록 합니다.
228
422
 
229
423
  ### 데코레이터
230
424
  - `@CacheTTL(seconds)`: 특정 핸들러의 TTL을 설정합니다.
231
425
  - `@CacheKey(key)`: 특정 핸들러의 custom cache key 또는 key factory를 설정합니다.
232
- - `@CacheEvict(key)`: 성공적인 non-GET 핸들러가 완료된 뒤 하나 이상의 cache key삭제합니다.
426
+ - `@CacheEvict(key)`: 성공적인 non-GET controller handler가 완료된 뒤 `CacheInterceptor`가 소비하는 HTTP route metadata저장합니다. 임의의 service call을 intercept하지 않습니다.
233
427
  - `cacheRouteMetadataKey`, `getCacheKeyMetadata(...)`, `getCacheTtlMetadata(...)`, `getCacheEvictMetadata(...)`: 캐시 데코레이터 metadata key를 다시 구현하지 않고 cache decorator metadata를 검사해야 하는 first-party interceptor 통합, 진단, 고급 tooling을 위해 공개된 low-level metadata helper입니다.
234
428
 
235
429
  ### 인터셉터
236
- - `CacheInterceptor`: 자동 GET 응답 캐싱 삭제 로직을 처리합니다.
430
+ - `CacheInterceptor`: 자동 GET 응답 캐싱을 처리하고 non-GET HTTP handler에서 `@CacheEvict(...)` metadata를 소비합니다.
237
431
 
238
432
  ### 저장소와 status helper
239
433
  - `MemoryStore`, `RedisStore`: 내장 store 구현입니다.
@@ -242,7 +436,7 @@ defineModule(ManualCacheModule, {
242
436
 
243
437
  ## 관련 패키지
244
438
 
245
- - `@fluojs/redis`: Redis 저장소 사용 필요합니다.
439
+ - `@fluojs/redis`: Lifecycle-managed Redis client를 위한 optional 통합입니다. `redis.client`로 애플리케이션 소유 `RedisCompatibleClient`를 직접 전달하면 필요하지 않습니다.
246
440
  - `@fluojs/http`: HTTP 인터셉터 및 데코레이터 사용 시 필요합니다.
247
441
 
248
442
  ## 예제 소스
@@ -251,3 +445,4 @@ defineModule(ManualCacheModule, {
251
445
  - `packages/cache-manager/src/interceptor.test.ts`: HTTP 캐싱 및 삭제 테스트.
252
446
  - `packages/cache-manager/src/service.ts`: 코어 `CacheService` 구현.
253
447
  - `packages/cache-manager/src/status.test.ts`: status 및 diagnostic helper 테스트.
448
+ - `packages/cache-manager/src/cache-observer.test.ts`: 캐시 관찰 계약 테스트.