-
[Nest] CPU 상승 개선을 위한 Docker로 Redis 적용공부/Nest 2024. 10. 6. 18:44
CPU 상승 알림을 적용(블로그)하고 지속적으로 모니터링을 했는데, 특정 요청에서 CPU가 상승하는 원인을 파악하였습니다.
해당 요청은 기획적으로 무한 스크롤이 적용이 되면 해결이 되는 부분이지만, 해당 기능 적용 시기를 기획자에게 물어본 결과 아직은 기능적으로 적용할 생각이 없다고 하였고 이후에 특정한 시기에 적용할 예정이라고 말을 들었습니다.
그렇다고 기능이 적용되기 전까지 방치할 수 없다고 생각하여 해당 데이터 요청은 cache로 관리하는 것으로 변경하기로 하였습니다.

이미지를 보면 CPU 사용량이 50% 이상으로 보이는 부분들이 다수 존재합니다.
CPU 상승의 원인이 되는 요청은 해당 요청으로 CPU가 상승할 정도인가라는 생각이 들긴 했지만, 안정적인 형태가 아니여서 개선이 필요하였습니다.
비용적인 문제도 고려할 생각으로 Redis를 EC2 인스턴스 내부에 Docker로 Redis 컨테이너로 실행할 생각하였습다.
하지만, 제한된 자원을 사용하는 EC2에 Redis의 자원을 마음대로 사용하면 오히려 서비스가 사용하는 자원이 부족할 것이라고 생각하였습니다.
그래서, container를 실행하기 전에 local 자원을 제한적으로 사용하도록 제한을 두는 방식으로 진행하였습니다.
Redis 적용 후 Cloud Watch

실제로 적용하고 지속적인 모니터링을 했는데, CPU 상승률이 줄어든 것을 확인할 수 있었다.
Redis 적용 방법
1. Docker 설치(공식문서 링크)
# 기존 설치 Docker 제거 for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done # Docker 설치전 apt 환경 업데이트 # Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update # Docker 설치 sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 설치 후 hello-world 이미지로 확인 sudo docker run hello-world # docker ps 입력 docker ps # docker ps 입력 오류시(권한 문제) sudo groupadd docker sudo usermod -aG docker $USER newgrp docker공식문서를 기반으로 설치를 진행해 주시면 됩니다. 위에 작성한 방식으로 해주셔도 됩니다.
2. docker-compose 파일 작성
version: '3.1' services: redis: # redis 이미지는 docker hub에서 확인하시고 latest 버전으로 설치 image: redis:7.2.4-alpine container_name: test-redis # redis 기본 포트 6379 ports: - 6379:6379 # redis 실행 명령어로 포트(6379) 지정 및 auth(인증) 설정 - test1234가 password command: redis-server --port 6379 --requirepass test1234 # deploy로 local 자원을 어느정도 사용할지 제한할 수 있음 # cpu:{min:0.01,max:0.1}, memory:{min:128M,max:256M} deploy: resources: limits: cpus: '0.1' memory: 256M reservations: cpus: '0.01' memory: 128Mdocker-compose 작성법은 따로 작성하지 않도록 하겠습니다. deploy로 redis service의 자원을 제한할 수 있습니다.
원하시는 값으로 설정하시면 됩니다. 물론, 자원을 제한할 필요 없으신 분들은 deploy를 제거하고 사용하시면 됩니다.
3. docker compose up -d 명령어 입력하여 이미지 install 및 container 실행
4. test-redis 컨테이너 확인
- docker ps 명령어 입력하여 컨테이너 확인

- 컨테이너에 직접 접속하여 확인 (docker exec -it {컨테이너 ID} redis-cli

- redis 명령어를 사용해보려고 하면 NOAUTH Authentication required라는 error가 발생하는데, 이는 인증받지 않은 사용자라는 말입니다.
- 컨테이너 생성 시 requirepass 뒤에 작성했던 값(저는 test1234)을 auth test1234 입력하면 OK라는 문구와 함께 redis 명령어를 컨테이너 내부에 접속해서 사용할 수 있습니다.

5. NestJS에 Redis 설정 추가
- 해당 방법은 많은 사람들이 작성한 블로그 글이 있습니다. 저도 여러 가지를 보고 제가 생각한 방식으로 풀었습니다.
- 공식문서에서 제공하는 CacheModule도 있으니 참고해 주시기 바라겠습니다.
- 제가 생각한 방식은 Redis 모듈을 생성하여 다른 Service에서 Injection 해서 사용하는 방식으로 결정하였습니다.
- 라이브러리 설치
npm install @nestjs/cache-manager cache-manager npm i --save cache-manager-ioredis-yet- 코드 작성(CQRS 패턴으로 작성된 코드입니다.)
// redis.module.ts import { CacheModule } from '@nestjs/cache-manager'; import { redisStore } from 'cache-manager-ioredis-yet'; import { Module } from '@nestjs/common'; import { BaseRedisQueryImplement } from './infrastucture/query/base-redis.query-implement'; import { BaseRedisRepositoryImplement } from './infrastucture/repository/base-redis.repository-implement'; import { InjectionToken } from './application/inject-token'; import { RedisController } from './interface/redis.controller'; import { CqrsModule } from '@nestjs/cqrs'; import { DeleteRedisKeyHandler } from './application/command/delete-redis-key/delete-redis-key.handler'; @Module({ imports: [ CqrsModule, CacheModule.registerAsync({ useFactory: async () => { const store = await redisStore({ host: localhost, port: 6379, password: test1234, }); return { store }; }, }), ], controllers: [RedisController], providers: [ { provide: InjectionToken.REDIS_QUERY, useClass: BaseRedisQueryImplement, }, { provide: InjectionToken.REDIS_REPOSITORY, useClass: BaseRedisRepositoryImplement, }, DeleteRedisKeyHandler, ], exports: [ InjectionToken.REDIS_QUERY, InjectionToken.REDIS_REPOSITORY, ], }) export class RedisModule {}- test 모듈에 Injection해서 사용할 수 있도록 사용
//test.module.ts import { Module } from '@nestjs/common'; import { RedisModule } from 'src/redis/redis.module'; import { TestController } from './interface/test.controller'; import { FindTestQueryHandler } from './application/query/find-test/find-test.handler'; import { InjectionToken } from './application/inject-token'; import { TestQueryImplement } from './infrastructure/query/test.query-implement'; import { CqrsModule } from '@nestjs/cqrs'; import { TestRepositoryImplement } from './infrastructure/repository/test.repository-implement'; import { RedisModule } from 'src/redis/redis.module'; @Module({ imports: [CqrsModule, RedisModule], controllers: [TestController], providers: [ { provide: InjectionToken.TEST_QUERY, useClass: TestQueryImplement, }, { provide: InjectionToken.TEST_REPOSITORY, useClass: TestRepositoryImplement, }, FindTestQueryHandler, ], exports: [], }) export class TestModule {}- test.service.ts 작성
// test.service.ts import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; import { FindTestQuery } from './find-test.query'; import { TestQuery } from '../test.query'; import { Inject } from '@nestjs/common'; import { InjectionToken } from '../../inject-token'; import { InjectionToken as RedisToken } from 'src/redis/application/inject-token'; import { BaseRedisQuery } from 'src/redis/application/query/base-redis.query'; import { BaseRedisRepository } from 'src/redis/application/command/base-redis.repository'; import { FindTestResponse } from 'src/test/interface/dto/find-test.dto'; @QueryHandler(FindTestQuery) export class FindTestQueryHandler implements IQueryHandler<FindTestQuery> { @Inject(InjectionToken.Test_QUERY) private readonly testQuery: TestQuery; @Inject(RedisToken.REDIS_QUERY) private readonly baseRedisQuery: BaseRedisQuery<FindTestResponse>; @Inject(RedisToken.REDIS_REPOSITORY) private readonly baseRedisRepository: BaseRedisRepository<FindTestResponse>; async execute() { const cache = await this.baseRedisQuery.find('test'); if (cache) { return cache; } const test = await this.testQuery.find(); this.baseRedisRepository.create(`test`, test); return test; } }- Redis Query와 Redis Query implement 파일 생성
// base-redis.query.ts export interface BaseRedisQuery<T> { find: (key: string) => Promise<T[] | null>; // 공통으로 사용하려고 type을 T로 설정 findById: (key: string) => Promise<T | null>; }// base-redis.query-implement.ts import { Injectable } from '@nestjs/common'; import { BaseRedisQuery } from '../../application/query/base-redis.query'; import { Cache } from '@nestjs/cache-manager'; @Injectable() export class BaseRedisQueryImplement<T> implements BaseRedisQuery<T> { constructor(private readonly cacheManager: Cache) {} async find(key: string): Promise<T[] | null> { const cache = await this.cacheManager.get<T[]>(key); return cache ?? null; } async findById(key: string): Promise<T | null> { const cache = await this.cacheManager.get<T>(key); return cache ?? null; } }- Redis Repository와 Redis Repository Implement 파일 생성
// base-redis.repository.ts export interface BaseRedisRepository<T> { create: (key: string, data: T | T[], ttl: number) => Promise<void>; // ttl은 캐시 유효시간(ms로 인식) delete: (key: string) => Promise<void>; findByKey: (key: string) => Promise<T>; }// base-redis.repository-implement.ts import { Injectable } from '@nestjs/common'; import { BaseRedisRepository } from '../../application/command/base-redis.repository'; import { Cache } from '@nestjs/cache-manager'; import { SetTTL } from 'src/common/decorator/redis-ttl-transfor.decorator'; @Injectable() export class BaseRedisRepositoryImplement<T> implements BaseRedisRepository<T | null> { constructor(private readonly cacheManager: Cache) {} @SetTTL() // ttl 시간을 ms -> m으로 변경하는 decorator 생성 async create(key: string, data: T | T[], ttl: number): Promise<void> { await this.cacheManager.set(key, data, ttl); } async delete(key: string): Promise<void> { await this.cacheManager.del(key); } async findByKey(key: string): Promise<T> { return await this.cacheManager.get(key); } }- SetTTL은 decorator로 구성하여 ms 시간을 m으로 변환하도록 하였습니다.(해당 부분은 추후에 다시 작성하도록 하겠습니다.)
해당 코드는 제가 서비스에 적용한 부분과는 다른 부분이 많습니다. 흐름을 파악하는 부분으로 사용하셨으면 합니다.
데코레이터 부분은 추후에 다시 작성하도록 하겠습니다.
/***24-10-28 추가***/

이전에 install 했던 cache-manager-ioredis-yet가 deprecated가 됐고, 문서에서도 다른 라이브러리 사용하라고 이야기하고 있어서 해당 라이브러리를 수정하였습니다.
Keyv 라이브러리로 변경하였습니다. (링크)
나머지 코드는 동일하며, module에서 설정하는 부분만 변경하였습니다.
import { ConfigService } from '@nestjs/config'; import Keyv from 'keyv'; import KeyvRedis from '@keyv/redis'; @Module({ imports: [ CqrsModule, CacheModule.registerAsync({ inject: [ConfigService], useFactory: (configService: ConfigService) => { const redisConfig = configService.get<RedisConfig>('redis'); return new Keyv({ store: new KeyvRedis( `redis://default:${redisConfig.password}@${redisConfig.host}:${redisConfig.port}`, ), }); }, }), ], ....
해당 코드는 CQRS 패턴을 이용하여 Redis를 적용하는 코드로 구성하였습니다. 기본 구조를 가지고 사용하시는 분에게는 조금 다른 부분으로 생각하실 수 있지만, 코드를 조금 파악하시다 보면 이해하실 수 있으실 것이라고 생각합니다.
특정 문제나 궁금한 점을 검색하며 찾아보고 작성한 글입니다. 혹시라도 부정확한 정보를 전달드릴 수 있습니다. 틀린 부분이 있으면 댓글을 남겨주세요.
Ref.
https://www.npmjs.com/package/keyv
keyv
Simple key-value storage with support for multiple backends. Latest version: 5.1.2, last published: 7 days ago. Start using keyv in your project by running `npm i keyv`. There are 1444 other projects in the npm registry using keyv.
www.npmjs.com
https://www.tomray.dev/nestjs-caching-redis
Ultimate Guide: NestJS Caching With Redis [2023]
A detailed tutorial with NestJS examples on how to implement in-memory and Redis caching using the NestJS Cache Manager and Cache Interceptor
www.tomray.dev
https://kscodebase.tistory.com/686
NestJS에서의 Redis 적용
내가 적용한 시점에는 일일 다운로드 3만 건이었고, 현재는 5만건인 라이브러리다. 서비스에서도 사용하고 있는데 사용성이 무척 좋아서 공유한다. Installation # with npm npm install @liaoliaots/nestjs-redis
kscodebase.tistory.com
nestjs_cache-manager로 redis 사용하기
NestJS 어플리케이션에 Redis를 적용해보겠습니다. 캐싱이란? 캐시는 중복되거나 자주 사용하는 데이터를 저장해뒀다가 필요 시 바로 사용하는 것 Redis란? open source, in-memory data structure store as a db, ca
meir.tistory.com
NestJS/Typescript : 캐시 메모리와 레디스(Redis) — laetipark
캐시 메모리와 레디스(Redis)에 대한 정리와 nestjs/cache-manager 모듈을 통해 캐시 쓰기와 읽기를 구현해보았다.
blex.me
https://medium.com/@mut1aq/using-redis-in-nestjs-8ca1a009670f
Using Redis in NestJS
Learning the art of building scalable and high-performance applications is an art every developer aspires to achieve
medium.com
'공부 > Nest' 카테고리의 다른 글
[NestJS] EC2에서 Puppetter를 활용한 PDF 파일 다운로드 (0) 2025.10.25 ReferenceError: crypto is not defined (0) 2025.05.24 [Nest] TypeORM의 DELETE, REMOVE 차이 (0) 2024.06.02 [NestJS] 랜덤 문자열을 생성하여 양방향 암호화 진행하기 (0) 2024.04.23 [NestJS] 기본 구조 (0) 2023.09.09