[2026] Node.js 성능 최적화 | 클러스터링, 캐싱, 프로파일링
이 글의 핵심
Node.js 성능 최적화: 클러스터링, 캐싱, 프로파일링. 클러스터링 (Clustering)·캐싱 (Caching).
들어가며
성능 최적화의 중요성
Node는 한 프로세스 안에서 이벤트 루프로 I/O를 잘 감당하지만, CPU를 오래 쓰는 작업은 전체를 막을 수 있습니다. 그래서 클러스터·워커 스레드·캐시처럼 “누가 어떤 일을 나눠 갖는지”를 조정합니다. 최적화는 측정 없이 추측하지 말고, 프로파일러로 병목을 본 뒤 한 가지씩 바꾸는 것이 안전합니다. 성능이 중요한 이유:
- ✅ 사용자 경험: 빠른 응답 시간
- ✅ 비용 절감: 서버 리소스 효율화
- ✅ 확장성: 더 많은 사용자 처리
- ✅ SEO: 페이지 속도가 검색 순위에 영향 최적화 원칙:
- 측정 먼저: 추측하지 말고 측정
- 병목 지점 찾기: 가장 느린 부분 개선
- 점진적 개선: 한 번에 하나씩
- 트레이드오프 고려: 성능 vs 가독성
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
1. 클러스터링 (Clustering)
Cluster 모듈
다음은 javascript를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// cluster.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`마스터 프로세스 ${process.pid} 실행 중`);
// 워커 프로세스 생성
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 워커 종료 시 재시작
cluster.on('exit', (worker, code, signal) => {
console.log(`워커 ${worker.process.pid} 종료됨`);
console.log('새 워커 시작 중...');
cluster.fork();
});
} else {
// 워커 프로세스에서 서버 실행
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end(`워커 ${process.pid}가 처리함\n`);
});
server.listen(3000, () => {
console.log(`워커 ${process.pid} 시작됨`);
});
}
실행: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
node cluster.js
# 마스터 프로세스 12345 실행 중
# 워커 12346 시작됨
# 워커 12347 시작됨
# 워커 12348 시작됨
# 워커 12349 시작됨
PM2 클러스터 모드
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# CPU 코어 수만큼 프로세스 생성
pm2 start app.js -i max
# 특정 개수
pm2 start app.js -i 4
# 무중단 재시작
pm2 reload app
2. 캐싱 (Caching)
인메모리 캐싱
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 간단한 캐시
const cache = new Map();
function getCachedData(key, fetchFn, ttl = 60000) {
const cached = cache.get(key);
if (cached && Date.now() < cached.expiresAt) {
console.log('캐시 히트');
return cached.data;
}
console.log('캐시 미스');
const data = fetchFn();
cache.set(key, {
data,
expiresAt: Date.now() + ttl
});
return data;
}
// 사용
app.get('/api/users', async (req, res) => {
const users = getCachedData('users', async () => {
return await User.find();
}, 60000); // 1분 캐시
res.json({ users });
});
Redis 캐싱
npm install redis
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// cache.js
const redis = require('redis');
const client = redis.createClient({
host: 'localhost',
port: 6379
});
client.on('error', (err) => {
console.error('Redis 에러:', err);
});
client.connect();
// 캐시 설정
async function setCache(key, value, ttl = 3600) {
await client.setEx(key, ttl, JSON.stringify(value));
}
// 캐시 조회
async function getCache(key) {
const value = await client.get(key);
return value ? JSON.parse(value) : null;
}
// 캐시 삭제
async function deleteCache(key) {
await client.del(key);
}
module.exports = { setCache, getCache, deleteCache };
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 사용
// 변수 선언 및 초기화
const { getCache, setCache } = require('./cache');
app.get('/api/users/:id', async (req, res) => {
const { id } = req.params;
const cacheKey = `user:${id}`;
// 캐시 확인
let user = await getCache(cacheKey);
if (user) {
console.log('캐시 히트');
return res.json({ user, cached: true });
}
// 데이터베이스 조회
user = await User.findById(id);
if (!user) {
return res.status(404).json({ error: '사용자 없음' });
}
// 캐시 저장 (1시간)
await setCache(cacheKey, user, 3600);
res.json({ user, cached: false });
});
캐시 무효화
아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
app.put('/api/users/:id', async (req, res) => {
const { id } = req.params;
const user = await User.findByIdAndUpdate(id, req.body, { new: true });
// 캐시 무효화
await deleteCache(`user:${id}`);
res.json({ user });
});
3. 데이터베이스 최적화
인덱스
아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// MongoDB
userSchema.index({ email: 1 }); // 단일 인덱스
userSchema.index({ name: 1, age: -1 }); // 복합 인덱스
userSchema.index({ email: 1 }, { unique: true });
// 인덱스 확인
const indexes = await User.collection.getIndexes();
console.log(indexes);
쿼리 최적화
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 느린 쿼리
const posts = await Post.find();
for (const post of posts) {
const author = await User.findById(post.author); // N+1 문제
}
// ✅ Populate 사용
const posts = await Post.find().populate('author');
// ✅ 필요한 필드만 선택
const posts = await Post.find()
.select('title content author')
.populate('author', 'name email');
// ✅ Lean (Mongoose 객체 → Plain Object)
const posts = await Post.find().lean(); // 더 빠름
커넥션 풀
아래 코드는 javascript를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// MongoDB
mongoose.connect('mongodb://localhost:27017/mydb', {
maxPoolSize: 10, // 최대 연결 수
minPoolSize: 2
});
// PostgreSQL
const pool = new Pool({
max: 20,
min: 5,
idleTimeoutMillis: 30000
});
4. 비동기 최적화
병렬 처리
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 순차 실행 (느림)
async function sequential() {
const users = await User.find();
const posts = await Post.find();
const comments = await Comment.find();
return { users, posts, comments };
}
// 총 시간: T1 + T2 + T3
// ✅ 병렬 실행 (빠름)
async function parallel() {
const [users, posts, comments] = await Promise.all([
User.find(),
Post.find(),
Comment.find()
]);
return { users, posts, comments };
}
// 총 시간: max(T1, T2, T3)
동시 실행 제한
아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// p-limit 사용
const pLimit = require('p-limit');
async function processFiles(files) {
const limit = pLimit(5); // 최대 5개 동시 실행
const results = await Promise.all(
files.map(file => limit(() => processFile(file)))
);
return results;
}
5. 메모리 관리
메모리 사용량 확인
아래 코드는 javascript를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
function logMemoryUsage() {
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)} MB`, // 총 메모리
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`, // 할당된 힙
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`, // 사용 중인 힙
external: `${Math.round(used.external / 1024 / 1024)} MB` // C++ 객체
});
}
setInterval(logMemoryUsage, 60000); // 1분마다
메모리 누수 방지
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 메모리 누수
const cache = new Map();
app.get('/api/data/:id', async (req, res) => {
const data = await fetchData(req.params.id);
cache.set(req.params.id, data); // 계속 쌓임!
res.json(data);
});
// ✅ LRU 캐시 사용
const LRU = require('lru-cache');
const cache = new LRU({
max: 500, // 최대 500개
maxAge: 1000 * 60 * 60 // 1시간
});
app.get('/api/data/:id', async (req, res) => {
let data = cache.get(req.params.id);
if (!data) {
data = await fetchData(req.params.id);
cache.set(req.params.id, data);
}
res.json(data);
});
스트림 사용
아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 전체 파일을 메모리에 로드
// 실행 예제
app.get('/download', async (req, res) => {
const data = await fs.promises.readFile('large-file.pdf');
res.send(data);
});
// ✅ 스트림 사용
app.get('/download', (req, res) => {
const stream = fs.createReadStream('large-file.pdf');
stream.pipe(res);
});
일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.
6. 프로파일링
Node.js 내장 프로파일러
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# CPU 프로파일
node --prof app.js
# 프로파일 분석
node --prof-process isolate-0x*.log > processed.txt
Chrome DevTools
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 디버그 모드로 실행
node --inspect app.js
# 또는 중단점과 함께
node --inspect-brk app.js
브라우저에서 chrome://inspect 접속 후 프로파일링.
clinic.js
아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
npm install -g clinic
# CPU 프로파일
clinic doctor -- node app.js
# 이벤트 루프 지연
clinic bubbleprof -- node app.js
# 메모리 누수
clinic heapprofiler -- node app.js
7. 벤치마킹
Apache Bench
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 설치
sudo apt install apache2-utils
# 테스트
ab -n 1000 -c 10 http://localhost:3000/
# -n: 총 요청 수
# -c: 동시 연결 수
autocannon
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
npm install -g autocannon
# 테스트
autocannon -c 10 -d 10 http://localhost:3000/
# -c: 동시 연결 수
# -d: 지속 시간 (초)
벤치마크 코드
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// benchmark.js
const autocannon = require('autocannon');
async function runBenchmark() {
const result = await autocannon({
url: 'http://localhost:3000',
connections: 10,
duration: 10,
pipelining: 1
});
console.log('요청/초:', result.requests.mean);
console.log('지연시간:', result.latency.mean, 'ms');
console.log('처리량:', result.throughput.mean, 'bytes/sec');
}
runBenchmark();
8. 실전 최적화 예제
예제 1: API 응답 캐싱
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const express = require('express');
const redis = require('redis');
const app = express();
const client = redis.createClient();
await client.connect();
// 캐시 미들웨어
function cacheMiddleware(ttl = 3600) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cached = await client.get(key);
if (cached) {
console.log('캐시 히트');
return res.json(JSON.parse(cached));
}
// 원래 res.json을 래핑
const originalJson = res.json.bind(res);
res.json = (data) => {
client.setEx(key, ttl, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (err) {
next();
}
};
}
// 사용
app.get('/api/users', cacheMiddleware(60), async (req, res) => {
const users = await User.find();
res.json({ users });
});
예제 2: 데이터베이스 쿼리 최적화
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 비효율적
async function getPostsWithAuthors() {
const posts = await Post.find();
for (const post of posts) {
post.author = await User.findById(post.author); // N+1
}
return posts;
}
// ✅ 최적화
async function getPostsWithAuthorsOptimized() {
return await Post.find()
.populate('author', 'name email')
.select('title content author createdAt')
.lean() // Plain Object로 변환 (빠름)
.limit(20);
}
예제 3: 이미지 최적화
npm install sharp
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const sharp = require('sharp');
const fs = require('fs');
async function optimizeImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(800, 600, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: 80 })
.toFile(outputPath);
const inputSize = fs.statSync(inputPath).size;
const outputSize = fs.statSync(outputPath).size;
console.log(`압축률: ${((1 - outputSize / inputSize) * 100).toFixed(2)}%`);
}
// Express에서 사용
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('image'), async (req, res) => {
const inputPath = req.file.path;
const outputPath = `optimized/${req.file.filename}.jpg`;
await optimizeImage(inputPath, outputPath);
res.json({ path: outputPath });
});
9. 압축
gzip 압축
npm install compression
다음은 javascript를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const compression = require('compression');
// 모든 응답 압축
app.use(compression());
// 조건부 압축
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
level: 6 // 압축 레벨 (0-9)
}));
효과:
- HTML: 70-90% 감소
- JSON: 60-80% 감소
- CSS/JS: 50-70% 감소
10. 자주 발생하는 문제
문제 1: 이벤트 루프 블로킹
다음은 javascript를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ CPU 집약적 작업 (블로킹)
app.get('/heavy', (req, res) => {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
res.json({ sum });
});
// ✅ Worker Threads 사용
const { Worker } = require('worker_threads');
app.get('/heavy', (req, res) => {
const worker = new Worker('./heavy-task.js');
worker.on('message', (result) => {
res.json({ sum: result });
});
worker.on('error', (err) => {
res.status(500).json({ error: err.message });
});
});
아래 코드는 javascript를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// heavy-task.js
const { parentPort } = require('worker_threads');
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
parentPort.postMessage(sum);
문제 2: 메모리 누수
원인: 전역 변수, 이벤트 리스너 미제거, 캐시 무한 증가 다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 메모리 누수
const EventEmitter = require('events');
const emitter = new EventEmitter();
app.get('/api/data', (req, res) => {
emitter.on('data', (data) => { // 리스너가 계속 쌓임!
res.json(data);
});
});
// ✅ 리스너 제거
app.get('/api/data', (req, res) => {
const handler = (data) => {
res.json(data);
emitter.off('data', handler); // 제거
};
emitter.on('data', handler);
});
11. 실전 팁
성능 모니터링
아래 코드는 javascript를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 응답 시간 측정
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
if (duration > 1000) {
console.warn(`느린 요청: ${req.method} ${req.url} - ${duration}ms`);
}
});
next();
});
에러 처리 최적화
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 동기 에러 처리 (try-catch 오버헤드)
app.get('/api/users', async (req, res) => {
try {
const users = await User.find();
res.json({ users });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ✅ 에러 핸들러로 위임
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/api/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json({ users });
}));
// 전역 에러 핸들러
app.use((err, req, res, next) => {
logger.error(err.message, { stack: err.stack });
res.status(500).json({ error: 'Internal Server Error' });
});
Keep-Alive
아래 코드는 javascript를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const http = require('http');
const server = http.createServer((req, res) => {
res.end('Hello');
});
// Keep-Alive 설정
server.keepAliveTimeout = 65000; // 65초
server.headersTimeout = 66000; // 66초
server.listen(3000);
정리
핵심 요약
- 클러스터링: 멀티 코어 활용, PM2 클러스터 모드
- 캐싱: Redis, 인메모리 캐시, TTL 설정
- 데이터베이스: 인덱스, 쿼리 최적화, 커넥션 풀
- 비동기: 병렬 처리, 동시 실행 제한
- 메모리: 스트림 사용, LRU 캐시, 메모리 모니터링
- 압축: gzip, 이미지 최적화
성능 최적화 우선순위
- 데이터베이스 쿼리: 가장 큰 병목
- 캐싱: 빠른 효과
- 비동기 최적화: 병렬 처리
- 압축: 네트워크 비용 절감
- 클러스터링: 멀티 코어 활용
측정 도구
| 도구 | 용도 |
|---|---|
--prof | CPU 프로파일링 |
| Chrome DevTools | 메모리, CPU 분석 |
| clinic.js | 종합 진단 |
| autocannon | HTTP 벤치마크 |
| PM2 | 프로세스 모니터링 |
다음 단계
- Node.js 보안 심화
- Node.js 마이크로서비스
추천 학습 자료
도구: