메모리 누수의 일반적 원인
Node.js에서 메모리 누수는 가비지 컬렉터가 회수할 수 없는 객체가 지속적으로 쌓이는 현상입니다. V8 엔진의 힙 메모리가 계속 증가하다가 결국 OOM(Out of Memory)으로 프로세스가 종료됩니다.
메모리 누수의 5대 원인
| 원인 | 설명 | 빈도 |
|---|---|---|
| 전역 변수 축적 | 배열/맵에 데이터를 계속 추가하고 제거하지 않음 | 매우 높음 |
| 이벤트 리스너 누적 | 리스너 등록 후 해제하지 않음 | 높음 |
| 클로저 참조 | 클로저가 큰 객체를 참조하여 GC 방해 | 중간 |
| 타이머 미정리 | setInterval/setTimeout 미해제 | 중간 |
| 스트림 미종료 | 읽기/쓰기 스트림을 닫지 않음 | 낮음 |
프로그래밍 방식 Heap Snapshot
const v8 = require('v8');
const fs = require('fs');
function takeHeapSnapshot(label = 'snapshot') {
const filename = label + '-' + Date.now() + '.heapsnapshot';
const snapshotStream = v8.writeHeapSnapshot(filename);
console.log('Heap snapshot written to: ' + snapshotStream);
return snapshotStream;
}
// 메모리 모니터링 미들웨어
function memoryMonitor(req, res, next) {
const before = process.memoryUsage();
res.on('finish', () => {
const after = process.memoryUsage();
const heapDiff = (after.heapUsed - before.heapUsed) / 1024 / 1024;
if (heapDiff > 10) {
console.warn('[MEMORY] ' + req.url + ': +' + heapDiff.toFixed(2) + 'MB');
}
});
next();
}
// 주기적 메모리 로깅
setInterval(() => {
const mem = process.memoryUsage();
console.log(JSON.stringify({
rss: (mem.rss / 1024 / 1024).toFixed(1) + 'MB',
heapUsed: (mem.heapUsed / 1024 / 1024).toFixed(1) + 'MB',
heapTotal: (mem.heapTotal / 1024 / 1024).toFixed(1) + 'MB',
external: (mem.external / 1024 / 1024).toFixed(1) + 'MB',
}));
}, 30000);
일반적인 메모리 누수 패턴과 수정
// BAD: 전역 캐시 무한 증가
const cache = {};
app.get('/user/:id', async (req, res) => {
if (!cache[req.params.id]) {
cache[req.params.id] = await db.findUser(req.params.id);
}
res.json(cache[req.params.id]);
});
// GOOD: LRU 캐시로 크기 제한
const { LRUCache } = require('lru-cache');
const cache = new LRUCache({
max: 1000,
ttl: 1000 * 60 * 5,
maxSize: 50 * 1024 * 1024,
sizeCalculation: (value) => JSON.stringify(value).length,
});
// BAD: 이벤트 리스너 누적
app.get('/stream', (req, res) => {
emitter.on('data', (data) => res.write(data));
});
// GOOD: 연결 종료 시 리스너 해제
app.get('/stream', (req, res) => {
const handler = (data) => res.write(data);
emitter.on('data', handler);
req.on('close', () => {
emitter.off('data', handler);
});
});
Chrome DevTools로 분석
# Node.js를 inspect 모드로 실행
node --inspect server.js
# 또는 프로덕션에서 시그널로 디버거 활성화
kill -USR1 <pid>
디버깅 체크리스트
- Chrome DevTools의 Memory 탭에서 Heap Snapshot 3-point 비교법을 사용합니다
process.memoryUsage()의 heapUsed가 지속적으로 증가하면 누수를 의심합니다--max-old-space-size로 힙 크기를 제한하여 OOM을 일찍 감지합니다- 프로덕션에서는
clinic.js또는0x도구로 프로파일링합니다 - WeakRef와 FinalizationRegistry로 약한 참조를 활용합니다
댓글 0