❓ 기존 코드에서는 service layer에서 캐싱된 데이터가 있는지 확인하고, 있으면 해당 캐싱 데이터를 return해주고 없으면 비지니스 로직 수행 후 해당 return값을 캐싱해주었다.
그러나, 이는 service layer에서 캐시를 처리하는 부분이 처리하는 비지니스 로직과 함께 들어있기 때문에 유지보수면에서든, 코드 이해도면에서든 비효율적이므로, 캐시를 따로 처리해주는 cache interceptor를 만들어 controller에서@UseInterceptors()
를 통해 사용하였다.
@Injectable()
export class CacheInterceptor implements NestInterceptor {
protected cacheMethods = ['GET'];
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
// 추천 병원 조회 GET 요청인 경우에만 캐시 적용
if (this.isRequestGet(context)) {
const request = context.switchToHttp().getRequest();
const cacheKey = this.generateCacheKey(request);
console.log('cacheKey:', cacheKey);
// 캐시 데이터 확인
const cachedData = await this.cacheManager.get(cacheKey);
if (cachedData) {
console.log('캐시된 데이터를 사용합니다.');
return of(cachedData);
}
// 캐시된 데이터가 없는 경우 요청 처리
return next.handle().pipe(
tap((data) => {
// 데이터를 캐시에 저장
console.log('캐시에 데이터를 저장합니다.');
this.cacheManager.set(cacheKey, data);
}),
);
}
}
private generateCacheKey(request): string {
// 요청 URL과 쿼리 파라미터를 기반으로 고유한 캐시 키 생성
const reportId = request.params.report_id;
const radius = request.query.radius;
const maxCount = request.query.max_count;
return `${reportId}:${radius}:${maxCount}`;
}
private isRequestGet(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return this.cacheMethods.includes(req.method);
}
}
❓ 병원 조회 프론트엔드 화면에서 추천 병원 리스트를 불러올 때 최초 요청 이후 1분 내로 새로고침을 하는 경우 캐싱된 데이터를 불러오게 된다. 이때, 환자 이송 신청 및 철회를 하는 경우 화면이 새로고침되면서 환자가 이송을 신청한 병원에 대한 알림창과 이송신청 철회 버튼이 즉시 생기거나 사라져야하는데, 이송 신청 및 철회 전에 캐싱된 데이터를 사용하여 이를 즉각 반영하지 못하는 이슈가 발생하였다.
따라서, 이송 신청 및 철회를 하는 경우 해당 report_id로 인해 캐싱된 데이터들을 캐시 메모리에서 삭제해주었다. 이때, 동일한 report_id일 경우에도 radius와 max_count가 쿼리 스트링으로 반영되어 캐싱된 데이터가 1개 이상 존재할 수 있기 때문에 해당 report_id로 시작하는 key값을 가진 데이터들을 일괄 삭제해주었다.
@Injectable()
export class ClearCacheInterceptor implements NestInterceptor {
protected clearCacheMethods = ['POST', 'DELETE'];
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {=
if (this.isRequestPostOrDelete(context)) {
const request = context.switchToHttp().getRequest();
// 1. report_id를 해당 메서드에 넘겨준다.
await this.clearCacheKeysStartingWith(request.params.report_id);
}
return next.handle();
}
private async clearCacheKeysStartingWith(reportId: string): Promise<void> {
const cacheKeys = await this.getCacheKeysStartingWith(reportId);
// 4. 필터링한 key들을 돌면서 redis에서 삭제해준다.
await Promise.all(cacheKeys.map((key) => this.cacheManager.del(key)));
}
private async getCacheKeysStartingWith(prefix: string): Promise<string[]> {
// 2. redis에 담겨있는 모든 keys를 가져온다
const cacheKeys = await this.cacheManager.store.keys('*');
// 3. keys 중에서 report_id: 으로 시작하는 key들만 필터링해준다
return cacheKeys.filter((key) => key.startsWith(`${prefix}:`));
}
private isRequestPostOrDelete(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return this.clearCacheMethods.includes(req.method);
}
}