[Redis] NestJs에 Redis 캐시를 적용해보자 (feat. cache-manager)

가상계좌 관련 결제 시스템의 결제 관련 필요 정보들을 저장하는 현재 로직 플로우로는 해결이 불가능한 품절/적립금 적립 관련 이슈가 있었는데 프론트엔드 전역변수로의 처리에 대해 이야기하다 Redis를 사용하면 가능할 거 같다라는 생각에 NestJS에 Redis 캐싱을 적용시키게 되었다.

 

Redis의 경우, AWS ElasticCache를 사용할 수 있지만, 기본적으로 메모리 용량 = 돈이기 때문에 docker 공식 redis 이미지를 기존 ec2 인스턴스 내에 docker 컨테이너로 standalone 모드로 띄워두는 방식을 사용하였다.

 

Redis 관련 의존성 추가

Redis 캐시를 사용하기 위한 의존성을 추가해준다.

yarn add cache-manager@4.1.0 cache-manager-redis-store@2.0.0

의존성 추가시 cache-manager 라이브러리 버전을 명시하는 이유는 cache-manager 최신 버전인 5.x.x 버전과 cache-manager-redis-store의 버전이 호환되지 않는 이슈로 store.set is not a function 타입 에러가 발생되기 때문이다.

 

DI 세팅

Redis 캐시 관련 환경 설정하려면 CacheModule을 register해서 import해야한다. CacheModule의 경우, @nestjs/common에 기본적으로 포함되어있다. ttl 옵션의 경우 4.x.x 버전은 초(s) 단위로 입력해야하고 5.x.x 버전은 밀리초(ms) 단위로 추가하면 된다.

import { CacheModule, Module } from '@nestjs/common';

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

import { RedisService } from './redis/redis.service';


const cacheModule = CacheModule.register({
  useFactory: async () => ({
    store: redisStore,
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT,
    ttl: 1000, // 캐시 default 유지시간: 1000s
  }),
});

@Module({
   imports: [cacheModule],
   providers: [RedisService],
   exports: [RedisService],
})
export CacheModule {}

 

cache-manager의 Cache 타입에서 get/set/reset/del 메소드를 RedisService 클래스를 생성하여 다른 서비스 로직에서 사용이 간편하게 매핑해둔다. 

import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';

@Injectable()
export class RedisService {
  constructor(@Inject(CACHE_MANAGER) private readonly redis: Cache) {}

  async get(key: string): Promise<any> {
    return await this.redis.get(key);
  }

  async set(key: string, value: any, option?: any) {
    await this.redis.set(key, value, option);
  }

  async reset() {
    await this.redis.reset();
  }

  async del(key: string) {
    await this.redis.del(key);
  }
}

 

RedisService 서비스 로직에 추가해보기

Redis 캐싱이 필요한 로직이 위치해있는 Service 클래스의 생성자에 import해서 사용하면 된다.

// 레디스 적용 예제

@Injectable()
export class PaymentService {
	constructor(
    	@InjectRepository(Payment)
    	private readonly repository: PaymentRepository,
        
        private readonly redisService: RedisService
    ){}
    
    async commitPayment(dto: ExampleDto) {
    	const orderNo = generateRandomToken(20);
        
        await this.redisService.set(orderNo, dto, {ttl: 60 * 5});
        /*
         * PG사 api 통신 로직
         */
    }
    
    async approve(orderNo: string) {
    	const cachedExampleDto: ExampleDto = await this.redisService.get(orderNo);
        /*
         * 결제 검증 로직
         */
    }
}

 

Redis에 저장하는 서로 다른 데이터의 key가 중복되면 안되므로 randomToken 20자리를 key, dto를 value로 저장하고, 검증 로직 단계에서 해당 key에 매칭되는 dto를 가져와서 로직을 진행하는 방식으로 리팩토링을 진행하였다.