NestJS에서 Redis를 연동하려고 할 때 가장 고민되는 점은 무엇인가요?
"공식 문서대로 @nestjs/cache-manager를 쓰자니 INCR 같은 원자적(Atomic) 연산이 안 되고, 그렇다고 ioredis만 쓰자니 단순 캐싱 로직까지 일일이 짜는 게 너무 번거롭다"는 점일 겁니다.
게다가 무턱대고 두 라이브러리를 다 쓰면 Redis 커넥션이 두 배로 늘어나는 리소스 낭비까지 발생하죠.
오늘은 이 모든 문제를 한 번에 해결하는 '실무형 하이브리드 아키텍처'를 소개합니다.
단 하나의 Redis 연결을 재사용하여, 편리한 캐싱과 강력한 성능 제어를 동시에 잡는 방법을 단계별로 알아봅시다.
1. 왜 이 조합이어야 할까요?
우리는 세 가지 라이브러리를 조합해서 사용할 겁니다.
- @nestjs/cache-manager: NestJS의 표준 캐싱 인터페이스입니다. 비즈니스 로직 침범 없이 데코레이터나 get/set으로 캐싱을 쉽게 구현합니다.
- ioredis: Node.js 진영에서 가장 신뢰받는 Redis 클라이언트입니다. INCR, ZADD, Pipeline 등 Redis의 모든 기능을 제약 없이 사용할 수 있습니다.
- cache-manager-redis-yet: cache-manager v5 버전의 호환성 문제를 해결하고, 내부적으로 ioredis를 사용하여 성능을 보장하는 최신 어댑터입니다.
핵심은 "ioredis로 맺은 단 하나의 강력한 연결을 cache-manager에게 빌려준다"는 것입니다.
2. 프로젝트 설정 및 설치
먼저 필요한 패키지들을 설치합니다.
# NestJS 캐시 모듈, Redis 어댑터, ioredis 클라이언트 설치
npm install @nestjs/cache-manager cache-manager cache-manager-redis-yet ioredis
npm install -D @types/cache-manager
3. Redis 모듈 구성 (커넥션 생성의 주체)
먼저 ioredis를 사용하여 실제 Redis 연결을 담당할 모듈을 만듭니다.
이 모듈은 애플리케이션 전역에서 쓰이므로 @Global()로 설정합니다.
// src/redis/redis.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
// 유지보수를 위해 주입 토큰을 상수로 관리
export const REDIS_CLIENT = 'REDIS_CLIENT';
@Global()
@Module({
imports: [ConfigModule], // 환경변수 사용을 위해 import
providers: [
{
provide: REDIS_CLIENT,
useFactory: (configService: ConfigService) => {
return new Redis({
host: configService.get<string>('REDIS_HOST'),
port: configService.get<number>('REDIS_PORT'),
password: configService.get<string>('REDIS_PASSWORD'),
// 리소스 관리 및 안정성 옵션 (실무 권장)
connectTimeout: 10000,
maxRetriesPerRequest: 3,
});
},
inject: [ConfigService],
},
],
exports: [REDIS_CLIENT], // 다른 모듈(CacheModule 등)에서 쓸 수 있게 export
})
export class RedisModule {}
4. AppModule 설정 (리소스 최적화의 핵심)
이 부분이 가장 중요합니다.
CacheModule을 설정할 때 새로운 연결을 만드는 게 아니라, 방금 만든 RedisModule의 클라이언트를 주입(Inject) 받아 재사용합니다.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-yet';
import { RedisModule, REDIS_CLIENT } from './redis/redis.module';
import Redis from 'ioredis';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
// 1. RedisModule 로드 (여기서 실제 연결이 생성됨)
RedisModule,
// 2. CacheModule 설정 (생성된 연결을 재사용)
CacheModule.registerAsync({
isGlobal: true,
imports: [RedisModule],
useFactory: async (redisClient: Redis) => {
return {
store: await redisStore({
// 핵심: 호스트/포트를 다시 적지 않고, 이미 연결된 인스턴스를 넘깁니다.
// 라이브러리 버전에 따라 redisInstance 또는 ioRedis 옵션을 사용합니다.
redisInstance: redisClient,
ttl: 5000, // 기본 만료 시간 (밀리초 단위! v5 주의)
}),
};
},
inject: [REDIS_CLIENT], // RedisModule에서 만든 클라이언트를 주입받음
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
왜 이렇게 하나요? 이렇게 구성하면 애플리케이션은 Redis와 단 하나의 TCP 연결만 맺습니다. 트래픽이 늘어나거나 오토스케일링이 될 때 Redis 서버의 커넥션 부하를 절반으로 줄일 수 있는 아주 중요한 최적화 패턴입니다.
5. 실무 적용: Service에서 하이브리드로 사용하기
이제 서비스 레이어에서는 상황에 따라 두 가지 도구를 적재적소에 꺼내 쓰면 됩니다.
- CACHE_MANAGER: 단순 데이터 조회/캐싱
- REDIS_CLIENT: 동시성 제어, 복잡한 자료구조, Atomic 연산
// src/app.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { REDIS_CLIENT } from './redis/redis.module';
import Redis from 'ioredis';
@Injectable()
export class AppService {
constructor(
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
@Inject(REDIS_CLIENT) private readonly redisClient: Redis,
) {}
/**
* [Case 1] 단순 조회: CacheManager 사용
* 편의성이 중요할 때 (상품 목록, 공지사항 등)
*/
async getSimpleData(key: string) {
// 1. 캐시 조회 (코드 간결)
const cachedData = await this.cacheManager.get(key);
if (cachedData) return cachedData;
// 2. 데이터 페칭 및 캐싱
const data = 'Some DB Data';
await this.cacheManager.set(key, data, 10000); // 10초
return data;
}
/**
* [Case 2] 정밀 제어: ioredis 사용
* 데이터 무결성이 생명일 때 (선착순 이벤트, 조회수, 재고 차감)
*/
async increaseViewCount(postId: number) {
const key = `post:view:${postId}`;
// INCR 명령어 사용: Race Condition(경쟁 상태) 원천 차단
// cache-manager로는 구현하기 까다로운 부분입니다.
const currentViews = await this.redisClient.incr(key);
return { postId, views: currentViews };
}
/**
* [Case 3] 고급 자료구조: ioredis 사용
* 실시간 랭킹 (Sorted Set)
*/
async updateRanking(userId: number, score: number) {
// ZADD: 랭킹 점수 추가
await this.redisClient.zadd('game:ranking', score, userId.toString());
}
}
6. 결론 및 마무리
이제 여러분의 NestJS 프로젝트는:
- cache-manager의 편리함으로 생산성을 높였고,
- ioredis의 강력함으로 복잡한 요구사항(동시성 등)을 해결할 수 있으며,
- 단일 커넥션 패턴으로 리소스까지 아끼는 튼튼한 구조를 갖췄습니다.
처음에는 설정이 조금 복잡해 보일 수 있지만, 한 번 구성해 두면 프로젝트 규모가 커져도 흔들리지 않는 단단한 기반이 되어줄 것입니다.
'Backend > NestJs' 카테고리의 다른 글
| NestJS XSS 방어: sanitize-html과 DTO Transform으로 자동화하기 (0) | 2026.03.22 |
|---|---|
| [NestJS] 사이드 프로젝트 Q&A, 디스코드 웹훅(Webhook)으로 실시간 알림 받기 (0) | 2026.01.26 |
| [NestJS] 이메일 시스템 설계: Nodemailer + Handlebars 레이아웃/파셜 완벽 적용기 (0) | 2026.01.12 |
| [NestJS] 이메일 발송 기능 구현: Nodemailer와 Gmail SMTP 연동 (0) | 2026.01.06 |
| [NestJS] 인증의 완성: Redis와 HttpOnly Cookie로 보안 철벽 치기 (0) | 2026.01.02 |