[TIL] 캐시 처리 & 무효화 (interceptor)

김시원·2023년 6월 22일
0

TIL

목록 보기
50/50
post-custom-banner

캐시 처리 (service layer → cache interceptor)

❓ 기존 코드에서는 service layer에서 캐싱된 데이터가 있는지 확인하고, 있으면 해당 캐싱 데이터를 return해주고 없으면 비지니스 로직 수행 후 해당 return값을 캐싱해주었다.
그러나, 이는 service layer에서 캐시를 처리하는 부분이 처리하는 비지니스 로직과 함께 들어있기 때문에 유지보수면에서든, 코드 이해도면에서든 비효율적이므로, 캐시를 따로 처리해주는 cache interceptor를 만들어 controller에서 @UseInterceptors()를 통해 사용하였다.

  • CacheInterceptor 코드
    @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값을 가진 데이터들을 일괄 삭제해주었다.

  • ClearCacheInterceptor 코드
    @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);
      }
    }
post-custom-banner

0개의 댓글