Node.js/NestJS

NestJS 전역 CacheManager로 Redis 사용하기

왈왈디 2024. 7. 6. 16:43
728x90

1. CacheManager 사용 이유

NestJS에서 캐싱을 위해 redis를 사용할 때

직접 RedisModule과 RedisService를 정의하여 사용할 수도 있지만,

@nestjs/cache-manager 와 cache-manager 라이브러리를 사용하면 훨씬 간단하다.

 

service를 정의해서 사용할 때는

redis 서버에 command를 보내는 메서드를 모두 직접 구현해 사용해야 한다.

 

라이브러리를 사용하면

get, set, reset, del 등 흔히 사용하는 커맨드들이 제공되어

바로 사용할 수 있다.

 

2. CacheManager 사용 방법

캐시 매니저 라이브러리 사용에 대해서는

NestJS 공식문서에서도 안내하고 있다. [공식 문서]

$ npm install @nestjs/cache-manager cache-manager

라이브러리를 설치한다.

//app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';

@Module({
  imports: [CacheModule.register()],
  controllers: [AppController],
})
export class AppModule {}

appModule에서 CacheModule을 import한다.

// user.service.ts
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

캐싱을 사용하고 싶은 service에서 CACH_MANAGER를 inject하여 cacheManager를 주입한다.

 

이렇게 하면 in-memory 캐싱을 사용할 수 있다.

const value = await this.cacheManager.get('key'); // 조회 & 할당
await this.cacheManager.set('key', 'value'); // defualt ttl 5초
await this.cacheManager.set('key', 'value', 1000); // 1000초 후 만료되는 키 설정
await this.cacheManager.set('key', 'value', 0); // 만료 없는 키 설정
await this.cacheManager.del('key'); // 삭제
await this.cacheManager.reset(); // 전체 삭제

 

3. Redis 연결

하지만 우리가 사용하고 싶은 것은 in-memory 캐싱이 아니라

Redis이다.

 

cacheManager가 redis로 연결되게 하기 위해서

AddModule에서 CachModule을 import할 때 storeredis로 설정해주어야 한다.

 

그러기 위해서 cache-manager-redis-storeredis 라이브러리를 설치해야 한다.

그런데 여기서 매우 중요한 주의사항이 있다.

 

두 라이브러리의 설치 버전을 반드시 정확히 해야한다.

NestJS 공식 문서에서도 호환성 이슈로

redis는 반드시 3.x.x 버전을 사용하라고 안내하고 있다.

NestJS - Caching

 

그런데 공식 문서에서 cache-manager-redis-store의 버전에 대해서는 논하지 않아서,

처음 시도했을 때 광장히 헤맸다.

 

최신 버전의 cache-manager-redis-store를 설치하면 

앱을 시작할 때 아래 에러가 발생한다.

TypeError: Cannot read properties of undefined (reading 'bind')

 

구글링을 통해 cache-manager-redis-store 버전은 반드시 2.0.0 으로 설치되어야 함을 발견했다.

아래와 같이 라이브러리를 설치하자.

npm i cache-manager-redis-store@2.0.0 redis@^3.0.0
// package.json
    "cache-manager-redis-store": "^2.0.0",
    "redis": "^3.1.2",

 

라이브러리를 설치했으면,

AppModule에서 CacheModule을 import 해줄 때

storeredis로 설정해줘야 한다.

// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CacheModule } from '@nestjs/cache-manager';
import * as redisStore from 'cache-manager-redis-store';
import { RedisClientOptions } from 'redis';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      cache: true,
    }),
    CacheModule.registerAsync<RedisClientOptions>({
      isGlobal: true,
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        store: redisStore,
        host: configService.getOrThrow<string>('REDIS_HOST'),
        port: configService.getOrThrow<number>('REDIS_PORT'),
        db: 0,
        ttl: 60,
      }),
    }),
  ]
})
export class AppModule {}

 

전역에서 사용하고자 isGlobaltrue로 설정했고,

configService를 사용하고자

registerAsyncuseFactory를 사용했다.

 

사용할 redis db지정과 deafult ttl 설정이 가능하다.

 

두 라이브러리가 typescript를 명시적으로 지원하지 않아

ide의 auto import가 지원되지 않으므로

직접 import문을 작성해 주어야 한다.

import * as redisStore from 'cache-manager-redis-store';
import { RedisClientOptions } from 'redis';

 

이대로 in-memory 캐싱과 동일하게

service에서 CACHE_MANAGER를 ineject 하여 사용하면

redis 캐싱을 사용할 수 있다.

// user.service.ts
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

 

4. 주의 사항

주의사항은 CacheModule을 AppModule에서 isGloabl: true 설정하고

config option을 세팅했다면

개별 모듈에서 CacheModuleimport 하지 말아야 한다는 것이다.

 

처음엔 위와 같이 CacheModule을 설정하고

api를 테스트해보았을 때,

db에 쿼리를 보내지 않기에 캐싱된 데이터를 사용하는 것은 확실한데

redis에 key가 저장되지 않아 매우 당황했었다.

 

cacheManager 호출부에서 console에 cacheManager.store를 찍어보니

in-memory 캐시가 사용된다는 것을 알 수 있었다.

{
  name: 'memory',
  usePromises: true,
  shouldCloneBeforeSet: true,
  ...
}

 

원인은 cacheManager를 사용하는 module에서

CacheModule을 import하고 있었다는 점이었다.

@Module({
  imports: [DiscoveryModule, CacheModule.register()],
})

 

이전에 ConfigModule을 사용하면서도 고생을 했던 이슈였는데

NestJS에서 Module을 import할 때 반환하는 module이 이원화되어 있어,

개별 모듈이 initialize하는 순간에는

AppModule에서 async로 설정한 config가 반영되지 않은 모듈이 import된다는 것이다.

(NestJS의 소스코드를 보면 확인할 수 있다. [nestjs/cache-manager 소스 코드])

 

async 설정이 반영된 모듈을 import하기 위해서는

모듈을 global 설정하여 개별 module에서 import없이 global module을 사용하거나,

dynamic module을 사용하여 모듈이 register되는 것을 await 한 후

개별 모듈들이 initialize하도록 해야 한다. 

 

나는 더 간단하게 global 모듈을 사용하고자 했다.

 

정상적으로 cacheManager가 redis와 연결되면

cacheManager.store name이 redis 인 것을 확인할 수 있다.

{
  name: 'redis',
  getClient: [Function: getClient],
  ...
  isCacheableValue: [Function (anonymous)]
}

 

redis-cli에서 key 조회하면 정상적으로 조회된다.

728x90