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 애플리케이션의 효과적인 에러 처리를 위한 주요 전략:
커스텀 에러 클래스
- 명확한 에러 타입 정의
- 상황별 적절한 에러 생성
- 일관된 에러 처리
비동기 에러 처리
- Promise 체인 활용
- async/await 패턴
- 적절한 에러 전파
미들웨어 기반 처리
- 중앙 집중식 에러 처리
- 환경별 응답 포맷
- 에러 로깅
재시도 메커니즘
- 일시적 오류 처리
- 백오프 전략
- 조건부 재시도
프로세스 레벨 처리
- 예기치 않은 종료 처리
- 정상 종료 보장
- 리소스 정리
트랜잭션 관리
- 데이터 일관성 보장
- 자동 롤백
- 리소스 해제
모니터링과 알림
- 실시간 에러 추적
- 로깅 시스템 구축
- 알림 메커니즘