NestJS 성능 최적화가 필요한 시점
NestJS 애플리케이션은 기능이 늘어날수록 응답 시간이 증가하고 메모리 사용량이 올라갑니다. Compression, Payload 제한, 직렬화 최적화, 메모리 관리를 체계적으로 적용하면 동일 하드웨어에서 2~5배 처리량 향상을 기대할 수 있습니다.
Compression 적용
응답 본문을 Gzip으로 압축하면 네트워크 전송량을 60~80% 줄입니다.
// main.ts
import compression from 'compression';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(compression({
threshold: 1024, // 1KB 이하는 압축 안 함
level: 6, // 압축 레벨 (1~9, 6이 균형점)
filter: (req, res) => {
// SSE 스트림은 압축 제외
if (req.headers['accept'] === 'text/event-stream') {
return false;
}
return compression.filter(req, res);
},
}));
await app.listen(3000);
}
참고: Nginx 뒤에 NestJS가 있다면 Nginx에서 압축하는 것이 더 효율적입니다. NestJS에서의 압축은 Nginx 없이 직접 노출되는 경우에 사용합니다.
Payload 크기 제한
대용량 요청 본문은 메모리를 과다 소비하고 DoS 공격 벡터가 됩니다.
// main.ts - Express 기반
import { json, urlencoded } from 'express';
app.use(json({ limit: '1mb' })); // JSON 본문 1MB 제한
app.use(urlencoded({ extended: true, limit: '1mb' }));
// Fastify 기반
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ bodyLimit: 1048576 }), // 1MB
);
// 파일 업로드는 별도 제한
@Post('upload')
@UseInterceptors(FileInterceptor('file', {
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
files: 5, // 최대 5개
},
}))
uploadFile(@UploadedFile() file: Express.Multer.File) {
return { size: file.size };
}
직렬화 성능 최적화
NestJS Serialization 응답 제어에서 다룬 ClassSerializerInterceptor는 편리하지만, class-transformer의 리플렉션 비용이 큽니다. 고성능이 필요하면 수동 매핑이 더 빠릅니다.
// ❌ class-transformer (느림: 리플렉션 + 인스턴스 생성)
@UseInterceptors(ClassSerializerInterceptor)
@Get('users')
findAll() {
return this.userService.findAll(); // 엔티티 → DTO 자동 변환
}
// ✅ 수동 매핑 (빠름: 단순 객체 생성)
@Get('users')
findAll() {
const users = this.userService.findAll();
return users.map(u => ({
id: u.id,
name: u.name,
email: u.email,
// password 제외
}));
}
| 방식 | 상대 속도 | 적합한 경우 |
|---|---|---|
| class-transformer | 1x (기준) | CRUD API, 개발 속도 우선 |
| 수동 매핑 | 5~10x | 대량 데이터, 고성능 API |
| JSON.stringify 직접 | 3~5x | 극단적 성능 요구 |
캐싱 전략
NestJS Cache Manager 심화의 기본 캐싱 외에, 응답 레벨 캐싱을 Interceptor로 구현합니다.
@Injectable()
export class HttpCacheInterceptor implements NestInterceptor {
constructor(
@Inject(CACHE_MANAGER) private cache: Cache,
) {}
async intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
// GET 요청만 캐싱
if (request.method !== 'GET') {
return next.handle();
}
const key = `http:${request.url}`;
const cached = await this.cache.get(key);
if (cached) {
return of(cached);
}
return next.handle().pipe(
tap(response => this.cache.set(key, response, 30_000)), // 30초 TTL
);
}
}
// 컨트롤러에서 적용
@UseInterceptors(HttpCacheInterceptor)
@Get('products')
findAll() {
return this.productService.findAll();
}
메모리 관리와 GC 튜닝
Node.js의 기본 힙 메모리는 약 1.5GB입니다. 대규모 서비스에서는 조정이 필요합니다.
# 힙 메모리 조정
node --max-old-space-size=2048 dist/main.js # 2GB
# Docker 환경
FROM node:20-alpine
ENV NODE_OPTIONS="--max-old-space-size=1536"
CMD ["node", "dist/main.js"]
// 메모리 사용량 모니터링 엔드포인트
@Controller('health')
export class HealthController {
@Get('memory')
getMemory() {
const mem = process.memoryUsage();
return {
heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(1)}MB`,
heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(1)}MB`,
rss: `${(mem.rss / 1024 / 1024).toFixed(1)}MB`,
external: `${(mem.external / 1024 / 1024).toFixed(1)}MB`,
};
}
}
메모리 누수 주범
| 원인 | 증상 | 해결 |
|---|---|---|
| 글로벌 배열/Map 누적 | RSS 지속 증가 | TTL 기반 캐시(LRU) 사용 |
| 이벤트 리스너 미해제 | MaxListeners 경고 | onModuleDestroy에서 해제 |
| 대용량 응답 버퍼링 | GC pause 증가 | 스트리밍 응답 사용 |
| 클로저 참조 유지 | 힙 사이즈 증가 | WeakRef/FinalizationRegistry |
데이터베이스 쿼리 최적화
// ❌ N+1 문제
const orders = await orderRepository.find();
for (const order of orders) {
const items = await order.items; // 주문마다 쿼리 발생
}
// ✅ Relations 로딩
const orders = await orderRepository.find({
relations: ['items', 'items.product'],
});
// ✅ QueryBuilder로 필요한 컬럼만
const orders = await orderRepository
.createQueryBuilder('o')
.select(['o.id', 'o.total', 'o.createdAt'])
.leftJoin('o.items', 'item')
.addSelect(['item.id', 'item.quantity'])
.where('o.userId = :userId', { userId })
.getMany();
응답 시간 측정 Interceptor
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
private readonly logger = new Logger('Performance');
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const start = performance.now();
return next.handle().pipe(
tap(() => {
const duration = performance.now() - start;
if (duration > 1000) { // 1초 이상이면 경고
this.logger.warn(
`SLOW ${request.method} ${request.url} - ${duration.toFixed(0)}ms`,
);
}
}),
);
}
}
정리
NestJS 성능 최적화는 Compression으로 전송량을 줄이고, Payload 제한으로 과부하를 방지하며, 직렬화 방식 선택과 캐싱으로 처리 속도를 높이는 것이 핵심입니다. 메모리 누수를 조기에 감지하고, 슬로우 쿼리를 모니터링하면 안정적인 운영이 가능합니다. 성능 문제는 대부분 DB 쿼리와 직렬화에서 발생하므로 이 두 영역을 먼저 점검하세요.