글로벌 에러 미들웨어 설계와 구현
Express 애플리케이션에서 중앙집중식 에러 처리를 위한 글로벌 에러 미들웨어 설계와 구현, 에러 로깅과 Node 예외 처리 패턴을 포함해 실무에서 바로 적용 가능한 구성과 예제를 담은 설명서
목차
개요
애플리케이션이 커지면 에러가 흩어지기 쉽다. 이때 글로벌 에러 미들웨어는 일관된 응답과 중앙 로깅을 제공한다. 본문은 Express 글로벌 에러 핸들러 설계 원칙과 Node 예외 처리 패턴, 에러 로깅 Node.js 구성 방법을 단계별로 설명한다.
왜 글로벌 에러 미들웨어가 필요한가
콘트롤러별로 에러 처리를 하다 보면 관리는 어려워진다. 글로벌 에러 미들웨어는 다음을 보장한다.
- 일관된 사용자 응답 포맷
- 중앙집중식 로깅과 모니터링 연계
- 보안상 민감한 정보 노출 제어
- 비동기 코드에서 누락된 예외 포착
설계 원칙
1. 책임 분리
비즈니스 로직은 에러를 던지기만 하고, 미들웨어가 에러를 처리한다. 로거는 별도 모듈로 분리해 재사용성을 높인다.
2. 에러 분류
에러는 크게 클라이언트 오류(400대), 인증/권한 오류(401/403), 서버 오류(500)로 구분한다. 커스텀 에러 클래스를 만들어 타입 정보를 보관하면 처리 로직 단순화에 도움이 된다.
3. 안전한 응답
운영 환경에서는 스택 트레이스나 내부 정보를 응답에 포함하지 않는다. 대신 에러 코드를 통해 문제를 추적하도록 한다.
구현 단계
1) 커스텀 에러 클래스
상태 코드와 메시지를 담는 간단한 에러 클래스를 만든다.
class AppError extends Error {
constructor(message, status = 500, isOperational = true) {
super(message);
this.status = status;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
2) 글로벌 에러 미들웨어
Express에서는 에러 핸들러에 네 개의 파라미터(err, req, res, next)가 필요하다. 여기서 로깅과 응답 포맷을 처리한다.
const logger = require('./logger');
const AppError = require('./AppError');
function globalErrorHandler(err, req, res, next) {
const status = err.status || 500;
const isOperational = err.isOperational ?? false;
// 에러 로깅
logger.error({ message: err.message, stack: err.stack, status });
// 안전한 응답 구성
const response = {
status: status >= 500 ? 'error' : 'fail',
message: isOperational ? err.message : '서버에 오류가 발생했습니다.'
};
res.status(status).json(response);
}
module.exports = globalErrorHandler;
3) 로거 통합
에러 로깅은 모니터링과 알림의 출발점이다. winston, pino 같은 라이브러리를 사용하면 파일이나 외부 시스템으로 전송하기 쉽다.
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'errors.log', level: 'error' })
]
});
module.exports = logger;
비동기 코드의 예외 처리
비동기 함수에서 에러를 놓치지 않으려면 미들웨어나 헬퍼로 래핑한다. Promise를 반환하는 라우트는 catch로 next(err)를 호출해야 글로벌 핸들러로 전달된다.
const catchAsync = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// 사용 예
// router.get('/', catchAsync(async (req, res) => { ... }));
응답 포맷 설계 예시
일관된 JSON 스펙을 정의하면 클라이언트가 처리하기 쉽다. 예를 들어:
- status: 'success' | 'fail' | 'error'
- message: 사용자용 설명
- code: 내부 에러 코드(선택)
- details: 개발용 추가 정보(운영에서는 제외)
배포와 모니터링 고려사항
운영 환경에서는 에러 알림 채널을 구성한다. 에러 로그를 수집해 트렌드를 분석하고, 반복되는 오류는 우선순위를 정해 수정한다. 또한 민감 정보는 로그에서 마스킹한다.
마무리
글로벌 에러 미들웨어는 안정적인 서비스 운영의 핵심 구성 요소다. Express 글로벌 에러 핸들러와 Node 예외 처리 패턴을 적용하면 에러 대응 속도를 높이고, 에러 로깅 Node.js 구성으로 문제 추적이 쉬워진다. 설계는 단순하게, 구현은 일관되게 유지하는 것이 관건이다.