Node.js 인터뷰 질문 23

질문: Node.js에서 효과적인 에러 처리는 어떻게 구현하나요?

답변:

Node.js에서 효과적인 에러 처리는 애플리케이션의 안정성, 가용성 및 디버깅 용이성을 위해 매우 중요합니다. 적절한 에러 처리를 구현하면 예상치 못한 상황에서도 애플리케이션이 예측 가능하게 동작하며, 개발자와 사용자 모두에게 더 나은 경험을 제공합니다.

에러 유형 이해하기

Node.js에서는 다양한 유형의 에러가 발생할 수 있습니다:

  1. 표준 JavaScript 에러:

    • Error: 일반적인 에러
    • SyntaxError: 문법 오류
    • ReferenceError: 존재하지 않는 변수 참조
    • TypeError: 잘못된 타입 연산
    • RangeError: 범위를 벗어난 값 사용
  2. Node.js 특화 에러:

    • SystemError: 운영 체제 수준 에러 (파일 액세스, 네트워크 연결 등)
    • 각종 모듈별 특화 에러 (예: fs 모듈의 파일 접근 에러)

동기식 코드에서의 에러 처리

동기식 코드에서는 전통적인 try-catch 블록을 사용합니다:

try {
  // 에러가 발생할 수 있는 동기식 코드
  const data = fs.readFileSync("non-existent-file.txt");
  console.log(data);
} catch (err) {
  // 에러 처리
  console.error("파일 읽기 오류:", err.message);

  // 에러 유형에 따른 처리
  if (err.code === "ENOENT") {
    console.error("파일이 존재하지 않습니다.");
  }
}

비동기식 코드에서의 에러 처리

1. 콜백 패턴

Node.js의 전통적인 콜백 패턴에서는 첫 번째 인자로 에러 객체를 전달합니다 (Error-first 콜백):

fs.readFile("file.txt", (err, data) => {
  if (err) {
    // 에러 처리
    console.error("파일 읽기 오류:", err.message);
    return;
  }

  // 정상 처리
  console.log(data.toString());
});

2. Promise 기반 에러 처리

Promise를 사용한 비동기 코드에서는 .catch()를 사용하여 에러를 처리합니다:

fs.promises
  .readFile("file.txt")
  .then((data) => {
    // 정상 처리
    console.log(data.toString());
  })
  .catch((err) => {
    // 에러 처리
    console.error("파일 읽기 오류:", err.message);
  });

Promise 체인에서는 각 단계별로 에러를 처리할 수 있습니다:

fetchData()
  .then((data) => processData(data))
  .then((processedData) => saveData(processedData))
  .catch((err) => {
    if (err.name === "FetchError") {
      console.error("데이터 가져오기 오류:", err.message);
    } else if (err.name === "ProcessingError") {
      console.error("데이터 처리 오류:", err.message);
    } else {
      console.error("알 수 없는 오류:", err.message);
    }
  })
  .finally(() => {
    // 항상 실행되는 정리 코드
    console.log("작업 완료");
  });

3. Async/Await를 사용한 에러 처리

Async/Await 패턴을 사용하면 비동기 코드에서도 try-catch 블록을 사용할 수 있습니다:

async function readAndProcessFile() {
  try {
    const data = await fs.promises.readFile("file.txt");
    // 정상 처리
    const processedData = await processData(data);
    return processedData;
  } catch (err) {
    // 에러 처리
    console.error("파일 처리 오류:", err.message);
    throw new Error("파일 처리 중 오류가 발생했습니다: " + err.message);
  }
}

// 함수 사용
readAndProcessFile()
  .then((result) => console.log("처리 결과:", result))
  .catch((err) => console.error("최종 오류:", err.message));

커스텀 에러 클래스 만들기

애플리케이션에 특화된 에러 유형을 정의하면 에러 처리와 디버깅이 용이해집니다:

class DatabaseError extends Error {
  constructor(message, query) {
    super(message);
    this.name = 'DatabaseError';
    this.query = query;
    this.date = new Date();

    // 스택 트레이스 유지
    Error.captureStackTrace(this, DatabaseError);
  }
}

// 사용 예시
function queryDatabase(query) {
  try {
    // 데이터베이스 쿼리 실행
    if (/* 쿼리 실패 조건 */) {
      throw new DatabaseError('데이터베이스 쿼리 실패', query);
    }
  } catch (err) {
    console.error(`${err.name}: ${err.message}`);
    console.error(`문제가 발생한 쿼리: ${err.query}`);
    console.error(`발생 시간: ${err.date}`);
    throw err; // 필요에 따라 상위로 에러 전파
  }
}

에러 전파 및 중앙화된 에러 처리

일관된 에러 처리 전략을 구현하기 위해 에러를 중앙화하여 처리할 수 있습니다:

Express.js에서의 에러 처리 미들웨어

const express = require("express");
const app = express();

// 일반 라우트 및 미들웨어
app.get("/data", async (req, res, next) => {
  try {
    // 비즈니스 로직
    const data = await fetchData();
    res.json(data);
  } catch (err) {
    // 에러를 다음 미들웨어로 전달
    next(err);
  }
});

// 에러 처리 미들웨어 (모든 라우트 후에 정의)
app.use((err, req, res, next) => {
  console.error("애플리케이션 에러:", err);

  // 에러 유형에 따른 응답
  if (err.name === "ValidationError") {
    return res.status(400).json({
      error: "유효성 검증 실패",
      details: err.details,
    });
  }

  if (err.name === "DatabaseError") {
    return res.status(503).json({
      error: "데이터베이스 오류",
      message: "서비스를 일시적으로 사용할 수 없습니다.",
    });
  }

  // 일반적인 서버 에러
  res.status(500).json({
    error: "서버 오류",
    message:
      process.env.NODE_ENV === "production"
        ? "서비스 처리 중 문제가 발생했습니다."
        : err.message,
  });
});

스트림과 이벤트 에미터에서의 에러 처리

스트림이나 이벤트 에미터를 사용할 때는 'error' 이벤트 리스너를 추가해야 합니다:

const readable = getReadableStream();

// 스트림 에러 처리
readable.on("error", (err) => {
  console.error("스트림 에러:", err.message);
});

// 이벤트 에미터 에러 처리
const myEmitter = new EventEmitter();

myEmitter.on("error", (err) => {
  console.error("이벤트 에미터 에러:", err.message);
});

처리되지 않은 예외 처리

프로세스 전체에 영향을 미칠 수 있는 처리되지 않은 예외를 캐치하기 위한 메커니즘:

// 처리되지 않은 예외 캐치
process.on("uncaughtException", (err) => {
  console.error("처리되지 않은 예외:", err);

  // 로그 기록, 알림 발송 등의 작업 수행

  // 프로세스 종료 (권장)
  process.exit(1);
});

// 처리되지 않은 Promise 거부 캐치
process.on("unhandledRejection", (reason, promise) => {
  console.error("처리되지 않은 Promise 거부:", reason);
});

주의: uncaughtException 이벤트는 마지막 수단으로 사용해야 합니다. 이 이벤트가 발생한 후에는 애플리케이션의 상태가 불안정할 수 있으므로, 가능한 한 정상적으로 종료하는 것이 좋습니다.

에러 로깅과 모니터링

효과적인 에러 처리를 위해 로깅 및 모니터링 시스템을 구축하는 것이 중요합니다:

const winston = require("winston");

// 로거 설정
const logger = winston.createLogger({
  level: "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    // 콘솔 출력
    new winston.transports.Console(),
    // 파일 저장
    new winston.transports.File({ filename: "error.log", level: "error" }),
    new winston.transports.File({ filename: "combined.log" }),
  ],
});

// 에러 발생 시 로깅
try {
  someFunction();
} catch (err) {
  logger.error("에러 발생", {
    error: {
      message: err.message,
      stack: err.stack,
      name: err.name,
    },
    additionalInfo: "관련 컨텍스트 정보",
  });
}

실제 프로덕션 환경에서는 Sentry, Datadog, New Relic과 같은 외부 모니터링 서비스를 활용하여 에러를 실시간으로 모니터링하고 알림을 받는 것이 좋습니다.

에러 처리 모범 사례

  1. 항상 에러를 처리하라: 모든 비동기 작업과 외부 서비스 호출에는 에러 처리 코드를 포함해야 합니다.

  2. 에러 메시지는 명확하게: 에러 메시지는 문제의 원인과 가능한 해결책을 제시할 수 있도록 상세하게 작성합니다.

  3. 사용자에게 적절한 정보만 제공: 프로덕션 환경에서는 사용자에게 상세한 에러 정보를 노출하지 않습니다.

  4. 발생 가능한 에러 유형을 문서화: API와 함수에서 발생할 수 있는 에러 유형을 문서화하여 개발자가 적절히 대응할 수 있도록 합니다.

  5. 단계적 에러 처리: 각 계층(데이터 액세스, 비즈니스 로직, API)에서 적절한 수준의 에러 처리를 구현합니다.

  6. 에러 회복 메커니즘: 가능한 경우 에러로부터 회복할 수 있는 메커니즘을 구현합니다(예: 재시도 로직).

  7. 테스트 중 에러 시나리오 포함: 단위 테스트와 통합 테스트에 다양한 에러 시나리오를 포함시킵니다.

효과적인 에러 처리는 단순히 예외를 catch하는 것을 넘어, 애플리케이션의 안정성을 보장하고 문제를 빠르게 진단하고 해결할 수 있는 종합적인 전략이 필요합니다. 잘 설계된 에러 처리 시스템은 개발자 경험과 사용자 경험 모두를 향상시키는 핵심 요소입니다.

results matching ""

    No results matching ""