7️⃣ 에러 처리 및 디버깅: 안정적인 애플리케이션 만들기 🛡️
지금까지 우리는 주로 기능이 정상적으로 동작하는 "해피 패스(happy path)"에 집중했습니다. 하지만 실제 애플리케이션에서는 사용자의 잘못된 입력, 데이터베이스 연결 실패, 예기치 못한 버그 등 수많은 에러가 발생할 수 있습니다.
잘 만들어진 애플리케이션은 이러한 에러가 발생했을 때 서버가 그대로 멈춰버리는 대신, 에러를 우아하게 처리하고 사용자에게 적절한 피드백을 주며, 개발자에게는 문제 해결에 필요한 충분한 정보를 남겨야 합니다.
이번 챕터에서는 Node.js와 Express 환경에서 에러를 체계적으로 처리하는 전략과, 문제가 발생했을 때 원인을 효율적으로 찾아내는 디버깅 기법, 그리고 애플리케이션의 상태를 추적하는 로깅의 중요성에 대해 다룹니다.
Node.js 에러 처리 전략 🚧
Node.js 환경의 에러는 크게 동기적 에러와 비동기적 에러로 나눌 수 있으며, 각각 다른 처리 방식이 필요합니다.
1. 기본 에러 처리 방식 복습
우리는 이미 여러 형태의 에러 처리를 접했습니다.
- 동기적 에러: 일반적인 try...catch 구문을 사용합니다.
-
JavaScript
try { throw new Error('Something went wrong!'); } catch (error) { console.error(error); } - 비동기적 에러 (Async/Await): async 함수 내에서 발생하는 에러 역시 try...catch로 잡을 수 있습니다. 이는 프로미스가 rejected 되었을 때 await가 예외를 던지기 때문입니다.
-
JavaScript
async function doSomethingAsync() { try { const result = await somePromiseThatMightFail(); } catch (error) { console.error('비동기 작업 실패:', error); } }
2. Express의 중앙 집중식 에러 처리
모든 라우트 핸들러마다 try...catch 블록을 작성하여 res.status(...).json(...) 코드를 반복하는 것은 매우 비효율적입니다. Express는 중앙 집중식 에러 처리 미들웨어라는 우아한 해결책을 제공합니다.
작동 원리:
- 라우트 핸들러나 일반 미들웨어에서 에러가 발생했을 때, catch 블록에서 res 객체로 직접 응답을 보내는 대신 next(error) 를 호출합니다.
- next() 함수에 인자(주로 에러 객체)가 전달되면, Express는 다른 모든 일반 미들웨어와 라우트 핸들러를 건너뛰고, 맨 마지막에 등록된 오류 처리 미들웨어로 제어를 즉시 넘깁니다.
- 오류 처리 미들웨어는 (err, req, res, next) 라는 특별한 4개의 인자를 가지며, 이곳에서 모든 에러를 한꺼번에 처리하여 일관된 형태의 에러 응답을 클라이언트에게 보낼 수 있습니다.
구현 예시:
1단계: 라우트 핸들러 수정
try...catch 블록에서 에러를 next()로 넘기도록 수정합니다.
// GET /api/users/:id
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findByPk(req.params.id);
if (user) {
res.json(user);
} else {
// 직접 에러를 생성하여 다음으로 넘길 수도 있습니다.
const error = new Error('User not found');
error.status = 404;
throw error;
}
} catch (err) {
// 잡힌 에러를 중앙 에러 핸들러로 전달
next(err);
}
});
2단계: 중앙 에러 처리 미들웨어 정의
app.js 파일의 가장 마지막 부분 (다른 모든 app.use()와 라우트 핸들러 뒤)에 오류 처리 미들웨어를 추가합니다.
// ... 다른 모든 app.use와 라우트 코드들 ...
// 404 Not Found 에러 처리 핸들러 (일치하는 라우트가 없을 때)
app.use((req, res, next) => {
const error = new Error('Not Found');
error.status = 404;
next(error);
});
// 중앙 집중식 에러 처리 핸들러 (err 인자를 가짐)
app.use((err, req, res, next) => {
// 응답에 보낼 에러 상태 코드 설정 (에러 객체에 status가 없으면 500)
res.status(err.status || 500);
// 일관된 에러 응답 객체 전송
res.json({
error: {
message: err.message
// 개발 환경에서는 에러 스택도 함께 보내주면 디버깅에 유용합니다.
// stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
}
});
});
app.listen(3000, () => console.log('Server started'));
이 방식을 사용하면 에러 응답의 형식을 한 곳에서 관리할 수 있고, 각 라우트 핸들러의 코드는 비즈니스 로직에만 집중하여 훨씬 깔끔해집니다.
Java 개발자를 위한 비유
Express의 중앙 집중식 에러 처리 미들웨어는 Spring의 @ControllerAdvice (또는 @RestControllerAdvice)와 @ExceptionHandler 어노테이션을 사용하는 것과 기능적으로 동일합니다. @ControllerAdvice 클래스가 애플리케이션 전역의 컨트롤러에서 발생하는 예외를 가로채서 처리하는 것처럼, Express의 오류 처리 미들웨어는 모든 라우트에서 next(err)로 전달된 에러를 가로채서 처리합니다.
3. 잡히지 않은 예외 처리
Express 에러 핸들러는 요청-응답 주기 내에서 next(err)로 전달된 에러만 잡을 수 있습니다. 만약 이 범위를 벗어난 곳에서 에러가 발생하면(예: Promise 체인에서 .catch가 없거나, 비동기 콜백 내의 동기적 에러) Node.js 프로세스가 다운될 수 있습니다.
이러한 최후의 에러들을 처리하기 위해 process 객체의 이벤트를 사용합니다.
// 잡히지 않은 동기적 예외 처리
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
// 프로덕션 환경에서는 에러를 로깅하고, 프로세스를 정상적으로 종료하는 것이 좋습니다.
// 애플리케이션이 불안정한 상태에 있을 수 있기 때문입니다.
process.exit(1);
});
// 처리되지 않은 프로미스 거부(rejection) 처리
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
이러한 핸들러는 애플리케이션의 "최후의 안전망" 역할을 합니다. 여기서 에러가 발생했다는 것은 애플리케이션이 예측 불가능한 상태에 빠졌음을 의미하므로, 에러를 기록한 뒤 프로세스를 종료하고 PM2와 같은 프로세스 관리 도구가 서버를 자동으로 재시작하도록 하는 것이 일반적인 운영 패턴입니다.
체계적인 에러 처리 전략은 안정적인 서비스를 위한 필수 요건입니다. 에러를 어떻게 잡고, 어떻게 일관되게 처리하며, 최후의 순간까지 어떻게 대비할지 이해하는 것은 매우 중요합니다.
'프로그래밍 > NODEJS 강좌 BY GEMINI' 카테고리의 다른 글
| 디버깅 도구 활용 및 로깅의 중요성 🐞📝 (3) | 2025.07.22 |
|---|---|
| NoSQL 데이터베이스 연동 (MongoDB 예시) 📄 (5) | 2025.07.09 |
| 6️⃣ 데이터베이스 연동: 정보의 저장과 관리 💾 (0) | 2025.07.03 |
| RESTful API 설계 및 구현 🏗️ (1) | 2025.07.02 |
| 정적 파일 제공하기 (CSS, JavaScript, 이미지) 🖼️ (0) | 2025.06.23 |