NestJS Caching 응용

projaguar·2022년 12월 17일
0
post-thumbnail

Intro

프로젝트를 진행하면서, 서버의 케시 적용은 오픈 일정에 맞추기 위해 우선 순위에서 밀려 항상 뒷전으로 밀리는 경우가 많았고, 적용하지 못하고 오픈하는 경우가 다반사 였습니다. 프로젝트 후반에 적용 하려면 테스트 중 변수도 많고, 잘 작동하지 않게되면 캐시적용 포기하고 오픈하게 됩니다.
서버 캐시는 노력대비 비용 절감 효과가 매우 크기 때문에, 필요한 서버에는 꼭 적용하고, 그리고 서버 설정 초반에 구성 하는것을 추천 드립니다.

  • 캐시는 다양한 방법으로 설정 할 수 있습니다만, 이번 샘플은 제가 가장 많이 사용 하는 방식 -global module설정으로- 으로 설명 하였습니다.
  • 캐시를 사용하지 않는 데이터를 조회가 가능하도록 파라메터로 no cache 데이터 조회 기능을 추가하였습니다.
  • 특정 path에는 캐시가 적용 되지 않도록 예외 path 처리를 하였습니다. (path parameter도 감안한 처리를 하였습니다.)
  • class-validator, class-transformer로 query parameter의 boolean 값을 처리하였습니다.

프로젝트 Setup

프로젝트 셋업 부분은 복잡하지 않기 때문에, 설정 절차와 제가 사용하는 설정 부분 중심으로 기술하겠습니다. 좀더 다양한 설정은configuration 설정부분과 caching 설정부분을 참고하세요.

주의
Nest의 caching 설정 문서에는 ttl 설정이 sec(초) 단위인 것처럼 기술 되어있는데, cache manager에서는 밀리세컨드(milliseconds)로 처리되는것 같습니다.

1. 프로젝트 생성

$ npx @nestjs/cli new nest-cache
$ cd new nest-cache

2. 환경변수 및 캐시 관련 패키시 설치

$ yarn add @nestjs/config
$ yarn add cache-manager
$ yarn add class-validator class-transformer
#
# .env
#

# (로컬)실행 포트 설정
PORT=3000

# 캐시 TTL 설정
CACHE_TTL=30000

3. 캐시 예외 처리를 위한 interceptor 작성

캐싱이 적용되지 않는 path를 지정하거나, query parameter에 noCache=true 요청이 오면, 캐싱하지 않도록 처리 합니다.

//
// http-cache.interceptor.ts
//

import { CacheInterceptor, ExecutionContext, Injectable } from '@nestjs/common';

const excludePaths = [
  // 캐시가 적용되지 않아야 할 path 목록 ()
  /(\/sample2\/)(.*)/i,
];

@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
  trackBy(context: ExecutionContext): string | undefined {
    const request = context.switchToHttp().getRequest();
    const { query } = request;
    const { httpAdapter } = this.httpAdapterHost;

    // Get Request가 아닌 request 처리
    const isGetRequest = httpAdapter.getRequestMethod(request) === 'GET';
    if (!isGetRequest) {
      return undefined;
    }

    // noCache=true query parameter 처리
    const noCache = query.noCache && query.noCache.toLowerCase() === 'true';
    if (noCache) {
      return undefined;
    }

    // 설정된 캐시 예외 path 처리
    const requestPath = httpAdapter.getRequestUrl(request).split('?')[0];
    const exclude = excludePaths.find((path) => {
      return path.test(requestPath);
    });
    if (exclude) {
      return undefined;
    }

    return httpAdapter.getRequestUrl(request);
  }
}

4. 모듈 설정

//
// app.module.ts
//

import { CacheModule, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HttpCacheInterceptor } from './http-cache.interceptor';

@Module({
  imports: [
    // config module 설정
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    // cache module 설정
    CacheModule.register({
      isGlobal: true,
      ttl: +(process.env.CACHE_TTL ?? 3000),
    }),
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: HttpCacheInterceptor,
    },
  ],
})
export class AppModule {}

app.module에 global 캐시 설정으로, 전체 NestJS app에서 캐시 기능을 사용할 수 있습니다.
필요에 따라 app.module에 global 설정을 적용하지 않고 하위 모듈에 부분적으로 캐시를 적용할 수 있습니다.
'providers' 부분은, 기본적으로 제공되는 CacheInterceptor를 사용하지 않고, CacheInterceptor를 상속받아 커스터마이징 한 HttpCacheInterceptor를 적용하도록 하였습니다.
위의 설정은 앱 전반에 걸쳐 공통으로 캐시 인터셉터를 사용할때 설정 입니다.
캐시 ttl은 환경변수로 적용하여 개발 테스트시와 서비스 적용시 필요한 값이 적용 될 수 있도록 하였습니다.
캐시 설정 관련 추가 옵션들의 설명은 공식문서를 참고하세요.

5. 캐시를 적용할 controller 설정

//
// app.controller.ts
//

import {
  CACHE_MANAGER,
  Controller,
  Get,
  Inject,
  Param,
  Query,
  UseInterceptors,
} from '@nestjs/common';
import { Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';
import { AppService } from './app.service';
import { HttpCacheInterceptor } from './http-cache.interceptor';
import { Cache } from 'cache-manager';

// 샘플 query dto
export class QueryDto {
  @IsBoolean()
  @Transform(({ value }) => value.toLowerCase() === 'true')
  readonly noCache: boolean;
}

@Controller()
@UseInterceptors(HttpCacheInterceptor) // 캐시 인터셉터 설정
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject(CACHE_MANAGER)
    private cacheManager: Cache,
  ) {}

  @Get()
  getHello(): string {
    console.log('hello called');
    return this.appService.getHello();
  }

  @Get('sample1')
  getSample1(): string {
    console.log('sample1 called');
    return this.appService.getSample1();
  }

  @Get('sample2/:key')
  async getSample2(
    @Param() key: string,
    @Query() query: QueryDto,
  ): Promise<any> {
    console.log('sample2 called');

    // no cache 일 경우
    if (query.noCache) {
      return this.appService.getSample2();
    }

    // cache manager에서 데이터 조회
    let value = await this.cacheManager.get(key);
    if (!value) {
      value = this.appService.getSample2();
      this.cacheManager.set(key, value);
    }

    //
    // 다른 처리들 ,,,
    //

    return value;
  }
}

'/sample2/:key' 요청은 HttpCacheInterceptor에서 캐시 예외 path로 지정하였기 때문에, 클라이언트의 모든 요청에대해 함수가 실행 됩니다. 모든 요청을 받아 직접 query parmater를 처리하고, cache-manager를 사용하여 캐싱 처리를 합니다. 캐싱은 필요 하지만, 요청에대한 후속처리 (통계처리 같은..)가 필요할때 사용하는 코드 샘플 입니다.

Conclusion

케싱 데이터가 크지 않다면 max 설정 정도로 과도한 메모리 사용의 방지 처리와 함께 사용해도 좋지만, 데이터가 크고 많다면 Redis 와 연동하여 사용하는것을 추천합니다. 설정 방법은 공식문서를 참고하세요.
또한 cache-manager를 이용하는 상세한 방법은 cache-manager 패키지 공식 사이트를 참고 하세요.

Reference

https://docs.nestjs.com/techniques/caching
https://github.com/nestjs/nest/blob/master/sample/20-cache/src/common/http-cache.interceptor.ts
https://dev.to/secmohammed/nestjs-caching-globally-neatly-1e17

profile
아직도 개발하고 있는 개발자 입니다

0개의 댓글