Node.js 인터뷰 질문 92
질문: Node.js 애플리케이션의 성능을 최적화하는 다양한 방법과 전략에 대해 설명해주세요.
답변:
Node.js 애플리케이션의 성능 최적화는 여러 측면에서 접근할 수 있습니다. 주요 최적화 전략과 구현 방법을 살펴보겠습니다.
1. 메모리 최적화
메모리 사용량을 최적화하고 메모리 누수를 방지합니다.
// memory-optimization.js
// 1. 스트림 사용하여 대용량 데이터 처리
const fs = require("fs");
const csv = require("csv-parser");
function processLargeCSV(filePath) {
const results = [];
return new Promise((resolve, reject) => {
fs.createReadStream(filePath)
.pipe(csv())
.on("data", (data) => {
// 데이터를 메모리에 누적하지 않고 처리
processRecord(data);
})
.on("end", () => {
resolve();
})
.on("error", reject);
});
}
// 2. 객체 풀링으로 메모리 재사용
class ObjectPool {
constructor(createFn, initialSize = 10) {
this.createFn = createFn;
this.pool = Array(initialSize)
.fill(null)
.map(() => createFn());
}
acquire() {
return this.pool.pop() || this.createFn();
}
release(obj) {
if (this.pool.length < 100) {
// 최대 풀 크기
this.pool.push(obj);
}
}
}
// 버퍼 풀 사용 예시
const bufferPool = new ObjectPool(() => Buffer.alloc(1024));
function processData(data) {
const buffer = bufferPool.acquire();
try {
// 버퍼 사용
buffer.write(data);
// 처리 로직
} finally {
bufferPool.release(buffer);
}
}
// 3. 메모리 사용량 모니터링
function monitorMemory() {
const used = process.memoryUsage();
console.log({
heapTotal: `${Math.round((used.heapTotal / 1024 / 1024) * 100) / 100} MB`,
heapUsed: `${Math.round((used.heapUsed / 1024 / 1024) * 100) / 100} MB`,
external: `${Math.round((used.external / 1024 / 1024) * 100) / 100} MB`,
rss: `${Math.round((used.rss / 1024 / 1024) * 100) / 100} MB`,
});
}
2. CPU 최적화
CPU 사용량을 최적화하고 병렬 처리를 활용합니다.
// cpu-optimization.js
const cluster = require("cluster");
const numCPUs = require("os").cpus().length;
if (cluster.isMaster) {
// 마스터 프로세스
console.log(`마스터 프로세스 ${process.pid} 실행`);
// CPU 코어 수만큼 워커 생성
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`워커 ${worker.process.pid} 종료`);
// 워커 재시작
cluster.fork();
});
} else {
// 워커 프로세스
const express = require("express");
const app = express();
app.get("/compute", (req, res) => {
// CPU 집약적인 작업
const result = computeIntensive();
res.json({ result });
});
app.listen(3000, () => {
console.log(`워커 ${process.pid}가 3000번 포트에서 실행 중`);
});
}
// 워커 스레드를 사용한 CPU 집약적 작업 처리
const { Worker } = require("worker_threads");
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker("./worker.js", {
workerData: data,
});
worker.on("message", resolve);
worker.on("error", reject);
worker.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`워커가 ${code} 코드로 종료됨`));
}
});
});
}
3. 데이터베이스 최적화
데이터베이스 쿼리와 연결을 최적화합니다.
// database-optimization.js
const { Pool } = require("pg");
// 커넥션 풀 설정
const pool = new Pool({
max: 20, // 최대 연결 수
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// 쿼리 캐싱
const NodeCache = require("node-cache");
const queryCache = new NodeCache({ stdTTL: 600 }); // 10분 캐시
async function getCachedData(key, queryFn) {
let data = queryCache.get(key);
if (data === undefined) {
data = await queryFn();
queryCache.set(key, data);
}
return data;
}
// 배치 처리
async function batchInsert(records, batchSize = 1000) {
const batches = [];
for (let i = 0; i < records.length; i += batchSize) {
batches.push(records.slice(i, i + batchSize));
}
for (const batch of batches) {
await pool.query(
"INSERT INTO table_name (column1, column2) VALUES " +
batch.map((r) => `(${r.value1}, ${r.value2})`).join(",")
);
}
}
// 인덱스 활용
const userSchema = new mongoose.Schema({
email: { type: String, index: true },
createdAt: { type: Date, index: true },
});
// 쿼리 최적화
const optimizedQuery = {
// 필요한 필드만 선택
select: "name email",
// 인덱스 활용
sort: { createdAt: -1 },
// 페이지네이션
limit: 10,
skip: 0,
};
4. 캐싱 전략
다양한 레벨의 캐싱을 구현합니다.
// caching-strategies.js
const Redis = require("ioredis");
const redis = new Redis();
// 다층 캐싱
class MultiLevelCache {
constructor() {
this.localCache = new Map();
this.redis = redis;
}
async get(key) {
// 1. 로컬 캐시 확인
if (this.localCache.has(key)) {
return this.localCache.get(key);
}
// 2. Redis 캐시 확인
const value = await this.redis.get(key);
if (value) {
// 로컬 캐시에도 저장
this.localCache.set(key, JSON.parse(value));
return JSON.parse(value);
}
return null;
}
async set(key, value, ttl = 3600) {
// Redis에 저장
await this.redis.setex(key, ttl, JSON.stringify(value));
// 로컬 캐시에도 저장
this.localCache.set(key, value);
}
}
// HTTP 응답 캐싱
const express = require("express");
const app = express();
// 캐시 미들웨어
function cacheMiddleware(duration) {
return (req, res, next) => {
const key = req.originalUrl;
redis.get(key, (err, data) => {
if (data) {
return res.json(JSON.parse(data));
}
// 원본 send 메서드 저장
const sendResponse = res.json.bind(res);
// send 메서드 래핑
res.json = (body) => {
redis.setex(key, duration, JSON.stringify(body));
sendResponse(body);
};
next();
});
};
}
// 라우트에 캐시 적용
app.get("/api/data", cacheMiddleware(300), async (req, res) => {
const data = await fetchData();
res.json(data);
});
5. 네트워크 최적화
네트워크 요청과 응답을 최적화합니다.
// network-optimization.js
const compression = require("compression");
const express = require("express");
const app = express();
// 응답 압축
app.use(compression());
// HTTP/2 설정
const spdy = require("spdy");
const fs = require("fs");
const options = {
key: fs.readFileSync("server.key"),
cert: fs.readFileSync("server.crt"),
};
spdy.createServer(options, app).listen(3000);
// 요청 배치 처리
class RequestBatcher {
constructor(batchSize = 10, timeWindow = 100) {
this.queue = [];
this.batchSize = batchSize;
this.timeWindow = timeWindow;
this.timer = null;
}
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
if (this.queue.length >= this.batchSize) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.timeWindow);
}
});
}
async flush() {
clearTimeout(this.timer);
this.timer = null;
const batch = this.queue.splice(0, this.batchSize);
if (batch.length === 0) return;
try {
const results = await this.processBatch(
batch.map((item) => item.request)
);
batch.forEach((item, index) => {
item.resolve(results[index]);
});
} catch (error) {
batch.forEach((item) => {
item.reject(error);
});
}
}
async processBatch(requests) {
// 배치 처리 로직 구현
return Promise.all(requests);
}
}
6. 코드 최적화
코드 레벨에서의 최적화를 수행합니다.
// code-optimization.js
// 1. 루프 최적화
function optimizedLoop(array) {
const length = array.length;
for (let i = 0; i < length; i++) {
// length를 캐시하여 매 반복마다의 속성 접근 방지
}
}
// 2. 문자열 연결 최적화
function buildString(items) {
return items.join(""); // + 연산자 대신 join 사용
}
// 3. 함수 최적화
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
};
// 4. 비동기 작업 최적화
async function optimizedAsync(items) {
// 동시성 제어
const concurrency = 3;
const results = [];
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency);
const batchResults = await Promise.all(
batch.map((item) => processItem(item))
);
results.push(...batchResults);
}
return results;
}
7. 모니터링과 프로파일링
성능 모니터링과 프로파일링을 구현합니다.
// monitoring.js
const prometheus = require("prom-client");
// 메트릭 설정
const httpRequestDurationMicroseconds = new prometheus.Histogram({
name: "http_request_duration_ms",
help: "Duration of HTTP requests in ms",
labelNames: ["method", "route", "code"],
buckets: [0.1, 5, 15, 50, 100, 500],
});
// 성능 모니터링 미들웨어
function performanceMonitoring(req, res, next) {
const start = process.hrtime();
res.on("finish", () => {
const duration = process.hrtime(start);
const durationMs = duration[0] * 1000 + duration[1] / 1000000;
httpRequestDurationMicroseconds
.labels(req.method, req.route.path, res.statusCode)
.observe(durationMs);
});
next();
}
// CPU 프로파일링
const profiler = require("v8-profiler-next");
function startProfiling(duration = 30000) {
const title = `CPU-${Date.now()}`;
profiler.startProfiling(title);
setTimeout(() => {
const profile = profiler.stopProfiling(title);
profile
.export()
.pipe(fs.createWriteStream(`${title}.cpuprofile`))
.on("finish", () => profile.delete());
}, duration);
}
요약
Node.js 애플리케이션 성능 최적화의 주요 전략:
메모리 최적화
- 스트림 활용
- 객체 풀링
- 메모리 모니터링
CPU 최적화
- 클러스터 모듈 활용
- 워커 스레드 사용
- 작업 분산
데이터베이스 최적화
- 커넥션 풀링
- 쿼리 캐싱
- 인덱스 활용
캐싱 전략
- 다층 캐싱
- 분산 캐싱
- 응답 캐싱
네트워크 최적화
- 응답 압축
- HTTP/2 활용
- 요청 배치 처리
코드 최적화
- 루프 최적화
- 메모이제이션
- 비동기 처리 최적화
모니터링과 프로파일링
- 성능 메트릭 수집
- CPU 프로파일링
- 메모리 프로파일링