Node.js 인터뷰 질문 91

질문: Node.js에서 효과적인 에러 처리 전략과 구현 방법에 대해 설명해주세요.

답변:

Node.js에서 에러 처리는 애플리케이션의 안정성과 신뢰성을 보장하는 중요한 부분입니다. 다양한 에러 처리 전략과 구현 방법을 살펴보겠습니다.

1. 커스텀 에러 클래스

애플리케이션 특화된 에러를 정의하고 처리합니다.

// errors.js
class AppError extends Error {
  constructor(message, statusCode = 500, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
    this.name = "ValidationError";
  }
}

class NotFoundError extends AppError {
  constructor(message) {
    super(message, 404);
    this.name = "NotFoundError";
  }
}

class UnauthorizedError extends AppError {
  constructor(message) {
    super(message, 401);
    this.name = "UnauthorizedError";
  }
}

module.exports = {
  AppError,
  ValidationError,
  NotFoundError,
  UnauthorizedError,
};

// 사용 예시
const { ValidationError } = require("./errors");

function validateUser(user) {
  if (!user.email) {
    throw new ValidationError("이메일은 필수입니다");
  }
  if (!user.password) {
    throw new ValidationError("비밀번호는 필수입니다");
  }
}

2. 비동기 에러 처리

Promise와 async/await를 사용한 에러 처리:

// async-error-handling.js
const fs = require("fs").promises;

// Promise 체인에서의 에러 처리
function readFilePromise(path) {
  return fs
    .readFile(path, "utf8")
    .then((data) => JSON.parse(data))
    .catch((error) => {
      if (error.code === "ENOENT") {
        throw new NotFoundError("파일을 찾을 수 없습니다");
      }
      if (error instanceof SyntaxError) {
        throw new ValidationError("유효하지 않은 JSON 형식입니다");
      }
      throw error;
    });
}

// async/await에서의 에러 처리
async function readFileAsync(path) {
  try {
    const data = await fs.readFile(path, "utf8");
    return JSON.parse(data);
  } catch (error) {
    if (error.code === "ENOENT") {
      throw new NotFoundError("파일을 찾을 수 없습니다");
    }
    if (error instanceof SyntaxError) {
      throw new ValidationError("유효하지 않은 JSON 형식입니다");
    }
    throw error;
  }
}

// 여러 비동기 작업의 에러 처리
async function processMultipleFiles(paths) {
  try {
    const results = await Promise.all(paths.map((path) => readFileAsync(path)));
    return results;
  } catch (error) {
    console.error("파일 처리 중 오류 발생:", error);
    throw error;
  }
}

3. Express.js 에러 처리 미들웨어

Express 애플리케이션에서의 중앙 집중식 에러 처리:

// error-middleware.js
const { AppError } = require("./errors");

// 에러 로깅 미들웨어
function errorLogger(error, req, res, next) {
  console.error("에러 발생:", {
    name: error.name,
    message: error.message,
    stack: error.stack,
    path: req.path,
    method: req.method,
    body: req.body,
    timestamp: new Date(),
  });
  next(error);
}

// 운영 환경 에러 응답 미들웨어
function errorHandler(error, req, res, next) {
  const statusCode = error.statusCode || 500;
  const message = error.isOperational
    ? error.message
    : "서버 내부 오류가 발생했습니다";

  res.status(statusCode).json({
    status: "error",
    message,
    ...(process.env.NODE_ENV === "development" && {
      stack: error.stack,
      details: error,
    }),
  });
}

// 404 에러 처리 미들웨어
function notFoundHandler(req, res, next) {
  next(new NotFoundError(`${req.originalUrl} 경로를 찾을 수 없습니다`));
}

// Express 앱에 적용
const app = express();

app.use(express.json());

// 라우트 핸들러
app.get("/api/users/:id", async (req, res, next) => {
  try {
    const user = await userService.findById(req.params.id);
    if (!user) {
      throw new NotFoundError("사용자를 찾을 수 없습니다");
    }
    res.json(user);
  } catch (error) {
    next(error);
  }
});

// 에러 처리 미들웨어 등록 (순서 중요)
app.use(notFoundHandler);
app.use(errorLogger);
app.use(errorHandler);

4. 비동기 작업 재시도 메커니즘

일시적인 오류에 대한 재시도 로직:

// retry.js
async function retry(fn, options = {}) {
  const {
    maxAttempts = 3,
    delay = 1000,
    backoff = 2,
    shouldRetry = (error) => true,
  } = options;

  let attempts = 0;
  let currentDelay = delay;

  while (attempts < maxAttempts) {
    try {
      return await fn();
    } catch (error) {
      attempts++;

      if (attempts === maxAttempts || !shouldRetry(error)) {
        throw error;
      }

      console.log(`재시도 중... (${attempts}/${maxAttempts})`);
      await new Promise((resolve) => setTimeout(resolve, currentDelay));
      currentDelay *= backoff;
    }
  }
}

// 사용 예시
async function fetchData(url) {
  return retry(
    async () => {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP 오류: ${response.status}`);
      }
      return response.json();
    },
    {
      maxAttempts: 3,
      delay: 1000,
      backoff: 2,
      shouldRetry: (error) => {
        // 특정 HTTP 상태 코드에 대해서만 재시도
        return error.message.includes("429") || error.message.includes("503");
      },
    }
  );
}

5. 프로세스 레벨 에러 처리

Node.js 프로세스의 예기치 않은 종료 처리:

// process-errors.js
process.on("uncaughtException", (error) => {
  console.error("처리되지 않은 예외:", error);
  // 에러 로깅
  logError(error);

  // 진행 중인 요청 정상 종료를 위한 유예 시간
  setTimeout(() => {
    process.exit(1);
  }, 1000);
});

process.on("unhandledRejection", (reason, promise) => {
  console.error("처리되지 않은 Promise 거부:", reason);
  // 에러를 uncaughtException 핸들러로 전달
  throw reason;
});

// 종료 시그널 처리
process.on("SIGTERM", async () => {
  console.log("SIGTERM 시그널 수신");
  await gracefulShutdown();
});

process.on("SIGINT", async () => {
  console.log("SIGINT 시그널 수신");
  await gracefulShutdown();
});

async function gracefulShutdown() {
  try {
    // 새로운 요청 거부
    server.close();

    // 진행 중인 요청 완료 대기
    await Promise.all([
      // 데이터베이스 연결 종료
      disconnectDatabase(),
      // 메시지 큐 연결 종료
      disconnectMessageQueue(),
      // 캐시 연결 종료
      disconnectCache(),
    ]);

    process.exit(0);
  } catch (error) {
    console.error("정상 종료 실패:", error);
    process.exit(1);
  }
}

6. 트랜잭션 롤백

데이터베이스 작업 실패 시 롤백 처리:

// transaction.js
class TransactionManager {
  constructor(db) {
    this.db = db;
  }

  async executeTransaction(callback) {
    const connection = await this.db.getConnection();

    try {
      await connection.beginTransaction();

      const result = await callback(connection);

      await connection.commit();
      return result;
    } catch (error) {
      await connection.rollback();
      throw error;
    } finally {
      connection.release();
    }
  }
}

// 사용 예시
const transactionManager = new TransactionManager(db);

async function transferMoney(fromAccount, toAccount, amount) {
  return transactionManager.executeTransaction(async (connection) => {
    // 출금 계좌 잔액 확인
    const [fromAccountData] = await connection.query(
      "SELECT balance FROM accounts WHERE id = ? FOR UPDATE",
      [fromAccount]
    );

    if (fromAccountData.balance < amount) {
      throw new ValidationError("잔액이 부족합니다");
    }

    // 출금
    await connection.query(
      "UPDATE accounts SET balance = balance - ? WHERE id = ?",
      [amount, fromAccount]
    );

    // 입금
    await connection.query(
      "UPDATE accounts SET balance = balance + ? WHERE id = ?",
      [amount, toAccount]
    );

    return { success: true };
  });
}

7. 에러 모니터링과 알림

운영 환경에서의 에러 모니터링:

// error-monitoring.js
const winston = require("winston");
const Sentry = require("@sentry/node");

// Sentry 설정
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
});

// 로거 설정
const logger = winston.createLogger({
  level: "error",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: "error.log" }),
    new winston.transports.Console({
      format: winston.format.simple(),
    }),
  ],
});

class ErrorMonitor {
  static async captureError(error, context = {}) {
    // 에러 로깅
    logger.error("에러 발생:", {
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack,
      },
      context,
      timestamp: new Date(),
    });

    // Sentry에 에러 전송
    Sentry.withScope((scope) => {
      Object.entries(context).forEach(([key, value]) => {
        scope.setExtra(key, value);
      });
      Sentry.captureException(error);
    });

    // 심각한 에러인 경우 알림 발송
    if (error.statusCode >= 500) {
      await this.sendAlert(error, context);
    }
  }

  static async sendAlert(error, context) {
    // 슬랙, 이메일 등으로 알림 발송
    const message = `
      심각한 에러 발생!
      시간: ${new Date()}
      에러: ${error.message}
      상태: ${error.statusCode}
      컨텍스트: ${JSON.stringify(context)}
    `;

    // 알림 발송 로직
    await sendSlackNotification(message);
    await sendEmailAlert(message);
  }
}

// 사용 예시
app.use(async (error, req, res, next) => {
  await ErrorMonitor.captureError(error, {
    path: req.path,
    method: req.method,
    userId: req.user?.id,
  });
  next(error);
});

요약

Node.js 애플리케이션의 효과적인 에러 처리를 위한 주요 전략:

  1. 커스텀 에러 클래스

    • 명확한 에러 타입 정의
    • 상황별 적절한 에러 생성
    • 일관된 에러 처리
  2. 비동기 에러 처리

    • Promise 체인 활용
    • async/await 패턴
    • 적절한 에러 전파
  3. 미들웨어 기반 처리

    • 중앙 집중식 에러 처리
    • 환경별 응답 포맷
    • 에러 로깅
  4. 재시도 메커니즘

    • 일시적 오류 처리
    • 백오프 전략
    • 조건부 재시도
  5. 프로세스 레벨 처리

    • 예기치 않은 종료 처리
    • 정상 종료 보장
    • 리소스 정리
  6. 트랜잭션 관리

    • 데이터 일관성 보장
    • 자동 롤백
    • 리소스 해제
  7. 모니터링과 알림

    • 실시간 에러 추적
    • 로깅 시스템 구축
    • 알림 메커니즘

results matching ""

    No results matching ""