2026년 NestJS와 Redis: 캐싱, 세션 관리, 면접 대비 가이드
NestJS에서 Redis를 활용한 캐싱과 세션 관리 구현 방법을 알아봅니다. @nestjs/cache-manager 설정, 캐시 전략, 세션 영속화, Node.js 캐싱 관련 기술 면접 빈출 질문을 종합적으로 다룹니다.

NestJS와 Redis 통합은 데이터베이스 호출을 인메모리 조회로 대체하여 애플리케이션 성능을 획기적으로 향상시킵니다. 캐싱용 @nestjs/cache-manager와 세션 영속화용 express-session + connect-redis 조합은 백엔드 애플리케이션에서 가장 일반적인 두 가지 Redis 사용 사례를 다룹니다.
NestJS에서 Redis 캐싱은 자주 액세스하는 데이터를 메모리에 저장하여 데이터베이스 부하를 줄입니다. Redis를 사용한 세션 관리는 여러 인스턴스 간에 세션 상태를 공유하여 수평적 확장을 가능하게 합니다.
@nestjs/cache-manager와 Redis 설정
NestJS 10부터 cache-manager 모듈은 별도의 패키지로 분리되었습니다. 설치에는 NestJS 래퍼와 기반 캐시 스토어가 모두 필요합니다. cache-manager v5에서 Redis 통합을 위해 @keyv/redis 어댑터가 권장됩니다.
# Install required packages
npm install @nestjs/cache-manager cache-manager @keyv/redis모듈 등록에서 Redis 연결과 기본 TTL을 설정합니다. isGlobal 옵션을 사용하면 다시 임포트하지 않고도 모든 모듈에서 캐시를 사용할 수 있습니다.
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';
@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
useFactory: () => ({
stores: [
new KeyvRedis(process.env.REDIS_URL || 'redis://localhost:6379'),
],
ttl: 60000, // Default TTL in milliseconds
}),
}),
],
})
export class AppModule {}stores 배열은 다중 계층 캐싱을 위해 여러 캐시 스토어를 허용합니다. 프로덕션 환경에서는 일반적으로 Redis를 기본 스토어로 사용하고, 개발용으로 선택적 인메모리 폴백을 설정합니다.
캐시 주입과 서비스 레벨 캐싱
CACHE_MANAGER 토큰은 서비스 레벨 작업을 위해 캐시에 직접 액세스하는 방법을 제공합니다. 이 접근 방식은 비즈니스 로직이 캐시 무효화를 결정할 때 자동 HTTP 캐싱보다 더 세밀한 제어가 가능합니다.
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { UsersRepository } from './users.repository';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@Inject(CACHE_MANAGER) private cache: Cache,
private usersRepository: UsersRepository,
) {}
async findById(id: string): Promise<User | null> {
// Check cache first
const cacheKey = `user:${id}`;
const cached = await this.cache.get<User>(cacheKey);
if (cached) {
return cached;
}
// Cache miss: fetch from database
const user = await this.usersRepository.findById(id);
if (user) {
await this.cache.set(cacheKey, user, 300000); // 5 minutes TTL
}
return user;
}
async update(id: string, data: Partial<User>): Promise<User> {
const user = await this.usersRepository.update(id, data);
// Invalidate cache on update
await this.cache.del(`user:${id}`);
return user;
}
}user:{id} 캐시 키 패턴으로 타겟 무효화가 가능합니다. user:*와 같은 더 복잡한 패턴에는 기반 클라이언트를 통한 Redis 전용 명령어가 필요합니다.
CacheInterceptor를 활용한 자동 HTTP 응답 캐싱
내장 CacheInterceptor는 HTTP GET 응답을 자동으로 캐싱합니다. 컨트롤러 레벨에서 적용하면 모든 GET 엔드포인트가 캐싱되고, 메서드 레벨 적용은 세밀한 제어를 제공합니다.
import { Controller, Get, UseInterceptors, Param } from '@nestjs/common';
import { CacheInterceptor, CacheTTL, CacheKey } from '@nestjs/cache-manager';
import { ProductsService } from './products.service';
@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
constructor(private productsService: ProductsService) {}
@Get()
@CacheTTL(120000) // Override default TTL: 2 minutes
async findAll() {
return this.productsService.findAll();
}
@Get(':id')
@CacheKey('product-detail') // Custom cache key prefix
async findOne(@Param('id') id: string) {
return this.productsService.findById(id);
}
}인터셉터는 기본적으로 요청 URL에서 캐시 키를 생성합니다. 쿼리 파라미터가 캐싱에 영향을 미치지 않아야 하는 라우트에서는 커스텀 @CacheKey() 데코레이터로 이 동작을 재정의합니다. NestJS의 인터셉터와 가드는 기술 면접에서 자주 다뤄지는 개념입니다.
기본 캐시 키는 쿼리 파라미터를 포함한 전체 URL을 사용합니다. /products?page=1과 /products?page=2에 대한 두 요청은 별도의 캐시 엔트리를 생성합니다. 페이지네이션이 중복 캐시 응답을 생성하지 않도록 하려면 @CacheKey()로 재정의하십시오.
Redis와 express-session을 사용한 세션 관리
NestJS는 기본적으로 Express에서 실행되므로 express-session이 세션 관리의 표준 선택입니다. connect-redis 어댑터는 세션 데이터를 메모리 대신 Redis에 저장하여 서버 재시작 후에도 세션을 영속화합니다.
# Install session packages
npm install express-session connect-redis redis
npm install -D @types/express-session세션 설정은 main.ts에서 애플리케이션 시작 전에 이루어집니다. Redis 클라이언트는 CacheManager 설정과 독립적으로 연결됩니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';
import { createClient } from 'redis';
import RedisStore from 'connect-redis';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Initialize Redis client
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
await redisClient.connect();
// Configure session middleware
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET || 'change-this-in-production',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}),
);
await app.listen(3000);
}
bootstrap();saveUninitialized: false 옵션은 빈 세션의 저장을 방지하여 Redis 메모리 사용량을 줄입니다. 프로덕션 환경에서는 secure: true 쿠키 설정에 HTTPS가 필요합니다.
Node.js / NestJS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
컨트롤러와 가드에서 세션 액세스
세션 데이터는 요청 객체를 통해 액세스할 수 있습니다. 타입이 지정된 세션 인터페이스는 개발자 경험을 향상시키고 컴파일 타임에 오류를 감지합니다.
export interface SessionData {
userId?: string;
email?: string;
roles?: string[];
loginAt?: Date;
}
declare module 'express-session' {
interface SessionData {
userId?: string;
email?: string;
roles?: string[];
loginAt?: Date;
}
}확장된 세션 타입으로 자동 완성에서 세션 프로퍼티를 사용할 수 있습니다.
import { Controller, Post, Body, Req, HttpCode } from '@nestjs/common';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@HttpCode(200)
async login(@Body() loginDto: LoginDto, @Req() req: Request) {
const user = await this.authService.validateUser(
loginDto.email,
loginDto.password,
);
// Store user data in session
req.session.userId = user.id;
req.session.email = user.email;
req.session.roles = user.roles;
req.session.loginAt = new Date();
return { message: 'Logged in successfully' };
}
@Post('logout')
@HttpCode(200)
async logout(@Req() req: Request) {
return new Promise((resolve, reject) => {
req.session.destroy((err) => {
if (err) reject(err);
resolve({ message: 'Logged out successfully' });
});
});
}
}가드는 세션 유효성을 확인하여 라우트를 보호합니다. 이 패턴은 NestJS의 인가와 RBAC 시스템과 원활하게 통합됩니다.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class SessionGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
return !!request.session?.userId;
}
}NestJS 애플리케이션의 캐시 무효화 전략
캐시 무효화는 캐싱에서 더 어려운 문제입니다. 다양한 시나리오에 적용되는 세 가지 전략이 있습니다.
시간 기반 만료(TTL) 는 데이터 변경이 예측 가능하거나 약간의 오래된 상태가 허용되는 경우에 적합합니다. 상품 카탈로그, 설정 값, 공개 콘텐츠가 이 패턴에 적합합니다.
이벤트 기반 무효화는 기반 데이터가 변경될 때 캐시 엔트리를 클리어합니다. 이는 업데이트 및 삭제 작업에서 명시적인 cache.del() 호출이 필요합니다.
패턴 기반 무효화는 관련된 여러 키를 제거합니다. Redis는 SCAN과 DEL 명령어로 패턴 삭제를 지원하지만, cache-manager는 이를 추상화합니다.
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
@Injectable()
export class CacheInvalidationService {
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
// Single key invalidation
async invalidateUser(userId: string): Promise<void> {
await this.cache.del(`user:${userId}`);
}
// Multiple related keys
async invalidateUserRelated(userId: string): Promise<void> {
const keys = [
`user:${userId}`,
`user:${userId}:profile`,
`user:${userId}:preferences`,
];
await Promise.all(keys.map((key) => this.cache.del(key)));
}
// Clear entire cache (use sparingly)
async clearAll(): Promise<void> {
await this.cache.reset();
}
}reset() 메서드는 모든 캐시 데이터를 클리어합니다. 배포나 데이터 마이그레이션 시에는 유용하지만, 프로덕션 환경에서 실수로 호출하면 위험합니다.
Redis 연결 풀링과 클러스터 지원
프로덕션 배포에서는 동시 요청을 효율적으로 처리하기 위해 연결 풀링이 필요합니다. ioredis 라이브러리는 클러스터 지원과 연결 풀링을 기본 제공합니다.
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis, { Cluster } from 'ioredis';
@Injectable()
export class RedisConfig {
constructor(private configService: ConfigService) {}
createClient(): Redis | Cluster {
const clusterNodes = this.configService.get<string>('REDIS_CLUSTER_NODES');
if (clusterNodes) {
// Cluster mode
const nodes = clusterNodes.split(',').map((node) => {
const [host, port] = node.split(':');
return { host, port: parseInt(port, 10) };
});
return new Redis.Cluster(nodes, {
redisOptions: {
password: this.configService.get('REDIS_PASSWORD'),
},
});
}
// Standalone mode
return new Redis({
host: this.configService.get('REDIS_HOST', 'localhost'),
port: this.configService.get('REDIS_PORT', 6379),
password: this.configService.get('REDIS_PASSWORD'),
maxRetriesPerRequest: 3,
});
}
}TypeORM이나 Prisma를 사용하는 애플리케이션에서는 데이터베이스와 캐시 연결을 각각 자체 연결 풀로 별도 설정해야 합니다.
NestJS Redis 면접 질문 모음
Node.js 캐싱에 관한 기술 면접에서는 개념적 이해와 실제 구현이 모두 다뤄집니다. 다음 질문들이 자주 출제됩니다.
인메모리 캐싱 대신 Redis를 사용하는 이유는?
인메모리 캐싱은 재시작 후에 유지되지 않으며 여러 인스턴스 간에 공유할 수 없습니다. Redis는 수평적으로 확장된 애플리케이션을 위해 영속성, 복제, 공유 상태를 제공합니다. 트레이드오프는 로컬 메모리 액세스와 비교한 네트워크 지연입니다.
cache-manager와 직접 Redis 사용의 차이점은?
cache-manager 라이브러리는 다양한 캐시 스토어에 걸쳐 통합된 API를 제공합니다. 인메모리에서 Redis로 전환하려면 서비스 코드 수정 없이 스토어 설정만 변경하면 됩니다. 직접 Redis 사용은 pub/sub, 정렬된 집합, Lua 스크립팅 등 더 많은 Redis 전용 기능을 제공합니다.
캐시 스탬피드란 무엇이며 어떻게 방지하는가?
캐시 스탬피드는 인기 있는 캐시 엔트리가 만료되고 많은 동시 요청이 동시에 데이터베이스에 액세스할 때 발생합니다. 방지 전략은 다음과 같습니다:
- 락 기반 갱신: 하나의 요청이 캐시를 갱신하고 나머지는 대기
- 확률적 조기 만료: TTL 만료 전 무작위 갱신
- 백그라운드 갱신: 별도 프로세스가 만료 임박 엔트리 갱신
CacheInterceptor와 서비스 레벨 캐싱은 언제 사용해야 하는가?
CacheInterceptor는 응답이 URL에만 의존하는 상태 비저장 GET 엔드포인트에 적합합니다. 서비스 레벨 캐싱은 데이터 변경 시 캐시 무효화가 필요하거나 비GET 작업에서 캐싱이 필요한 시나리오를 처리합니다.
Redis 세션에서 세션 고정 공격을 어떻게 방지하는가?
인증 후 세션 ID를 재생성합니다. 공격자가 인증 전 세션 ID를 사용하는 것을 방지하기 위해 사용자 자격 증명을 저장하기 전에 req.session.regenerate()를 호출합니다.
// Secure login with session regeneration
async login(@Body() loginDto: LoginDto, @Req() req: Request) {
const user = await this.authService.validateUser(
loginDto.email,
loginDto.password,
);
return new Promise((resolve, reject) => {
req.session.regenerate((err) => {
if (err) reject(err);
req.session.userId = user.id;
req.session.email = user.email;
resolve({ message: 'Logged in successfully' });
});
});
}시니어 후보자는 캐싱 전략 간의 트레이드오프를 설명합니다. 주니어는 "캐싱은 성능을 향상시킨다"고 말할 수 있습니다. 시니어는 고도로 개인화된 데이터나 빠르게 변하는 데이터처럼 캐싱이 이점 없이 복잡성만 추가하는 경우를 설명합니다.
성능 모니터링과 Redis 메트릭
프로덕션 애플리케이션에는 캐시 성능에 대한 가시성이 필요합니다. 주요 메트릭은 다음과 같습니다:
- 적중률: 캐시에서 제공된 요청의 비율. 80% 미만은 TTL 또는 키 전략 문제를 시사합니다.
- 메모리 사용량: Redis
INFO memory명령어로 현재 및 피크 메모리를 확인합니다. - 연결 수: 연결이 너무 많으면 풀링이 누락된 것입니다.
- 제거 수: 0이 아닌 제거는 캐시가 가득 차서 엔트리를 삭제하고 있음을 의미합니다.
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisHealthService {
constructor(private redis: Redis) {}
async getMetrics() {
const info = await this.redis.info('stats');
const memory = await this.redis.info('memory');
const stats = this.parseInfo(info);
const mem = this.parseInfo(memory);
const hits = parseInt(stats.keyspace_hits, 10);
const misses = parseInt(stats.keyspace_misses, 10);
const hitRate = hits / (hits + misses) || 0;
return {
hitRate: (hitRate * 100).toFixed(2) + '%',
usedMemory: mem.used_memory_human,
connectedClients: stats.connected_clients,
evictedKeys: stats.evicted_keys,
};
}
private parseInfo(info: string): Record<string, string> {
return info.split('\n').reduce((acc, line) => {
const [key, value] = line.split(':');
if (key && value) acc[key.trim()] = value.trim();
return acc;
}, {} as Record<string, string>);
}
}이 메트릭을 헬스 체크 엔드포인트로 노출하거나 Prometheus와 통합하여 프로덕션 모니터링을 수행합니다.
프로덕션 NestJS 배포에서의 Redis 캐싱과 세션
NestJS를 Redis와 함께 배포할 때는 여러 운영 고려 사항에 주의해야 합니다:
- 연결 문자열 관리: Redis URL은 코드가 아닌 환경 변수에 저장합니다. 비밀번호에는 시크릿 관리를 사용합니다.
- 그레이스풀 셧다운: 애플리케이션 종료 시 Redis 연결을 닫아 연결 누수를 방지합니다.
- 재시도 로직: Redis 연결 실패에 대해 지수 백오프를 설정합니다.
- 별도 Redis 인스턴스: 캐시와 세션용으로 별도의 Redis 인스턴스를 고려합니다. 캐시 제거가 세션 데이터에 영향을 주지 않도록 합니다.
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisCleanup implements OnModuleDestroy {
constructor(private redis: Redis) {}
async onModuleDestroy() {
await this.redis.quit();
}
}마이크로서비스 아키텍처에서 Redis는 NestJS의 내장 트랜스포트 레이어를 통해 메시지 브로커로도 작동하여 캐싱, 세션, 서비스 간 통신을 통합합니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
Node.js / NestJS 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 9월 10일 업데이트
태그
공유
관련 기사

NestJS 면접: Guards, Interceptors, 모듈형 아키텍처
Guards, Interceptors, 모듈형 아키텍처에 관한 NestJS 기술 면접의 빈출 질문을 구체적인 TypeScript 코드 예제와 기술적 설명과 함께 다룹니다.

NestJS + Prisma: Node.js를 위한 모던 백엔드 스택
NestJS와 Prisma로 모던한 백엔드 API를 구축하기 위한 완전한 가이드입니다. 설정, 모델, 서비스, 트랜잭션 및 모범 사례를 설명합니다.

Node.js 백엔드 면접 질문: 완벽 가이드 2026
Node.js 백엔드 면접에서 가장 자주 나오는 25가지 질문. Event loop, async/await, streams, 클러스터링, 성능을 상세한 답변과 함께 설명합니다.