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 애플리케이션 성능 최적화의 주요 전략:

  1. 메모리 최적화

    • 스트림 활용
    • 객체 풀링
    • 메모리 모니터링
  2. CPU 최적화

    • 클러스터 모듈 활용
    • 워커 스레드 사용
    • 작업 분산
  3. 데이터베이스 최적화

    • 커넥션 풀링
    • 쿼리 캐싱
    • 인덱스 활용
  4. 캐싱 전략

    • 다층 캐싱
    • 분산 캐싱
    • 응답 캐싱
  5. 네트워크 최적화

    • 응답 압축
    • HTTP/2 활용
    • 요청 배치 처리
  6. 코드 최적화

    • 루프 최적화
    • 메모이제이션
    • 비동기 처리 최적화
  7. 모니터링과 프로파일링

    • 성능 메트릭 수집
    • CPU 프로파일링
    • 메모리 프로파일링

results matching ""

    No results matching ""