에러 처리가 중요한 이유
서버에서 에러를 잘 처리하는 것은 엄청 중요하다. 수십 ~ 수만명이 동시다발적으로 접속해서 사용하는 어플리케이션의 서버가 될 수 있기 때문에, 적절히 에러를 처리하지 못했을 경우에 수만명이 서버를 이용하지 못하는 심각한 상황이 생길 수 있다.
에러 처리를 잘한다는게 무슨 말일까 ?
- 클라이언트가 요청한 request를 제대로 처리하지 못했다면, 적절한 에러 메세지를 보내주어서 클라이언트에게 충분한 에러에 대한 내용을 전달한다.
- 시스템 내부적으로 큰 문제가 발생하더라도 서버가 중지되지 않도록, 문제 상황에서 빠르게 복구될 수 있도록 예외처리를 잘 하는것
동기 / 비동기 에러 처리
코드 마지막에 최후의 보루로써 에러 핸들러를 두긴 하지만, 어디서 에러가 발생했는지 더 상세히 로깅하고 클라이언트에게 충분한 에러 내용 전달을 위해 각각의 미들웨어에서 에러처리를 하는게 좋다.
import express from 'express'; import fs from 'fs'; import fsAsync from 'fs/promises'; const app = express(); app.use(express.json()); app.get('/file1', (req, res) => { // 1. 비동기함수 에러 처리 방법 fs.readFile('/file1.txt', (err, data) => { if (err) { res.status(404).send('File Not Found'); // 비동기함수는 첫번째 인자로 들어온 err 에 대한 처리를 해줘야한다. 처리 하지 않으면 클라이언트나 서버에 에러에 대한 로그를 잡을 수 없다. } }); // // 2. 동기함수 에러 처리 방법 // try { // const data = fs.readFileSync('/file1.txt'); // 동기함수는 try{} catch(){} 로 감싸서 자세한 에러 내용을 클라이언트에게 알려주거나 혹은, 감싸지 않아도 마지막 에러 핸들러에서 에러 처리 가능 // } catch (error) { // res.status(404).send('File not found'); // } }); // 버전 5 이하에서는: require('express-async-errors'); // Express 5 부터는 이렇게 app.use((error, req, res, next) => { console.error(error); res.status(500).json({ message: 'Something went wrong' }); }); app.listen(8080);
readFileSync()는 파일을 다 읽어야지 다음줄로 넘어가는 동기 함수이다. 이 경우에는 try{}catch(){}로 감싸서 자세한 에러 사항을 잡아내거나, 혹은 try-catch로 감싸지 않아도 마지막 안전망(에러 핸들러)에 포착된다.
하지만 readFile(‘/file1.txt’,(err,data)=>{})은 파일을 다 읽고 다음 줄로 넘어가는게 아니라, 실행시켜두고 파일이 다 읽어지면 등록한 콜백함수를 실행시켜주는 비동기 함수이다. ‘/file1.txt’ 가 다 읽어지면, 등록한 콜백함수의 첫번째 인자는 에러 발생시 인자로, 데이터를 두번째 인자로 넘겨 콜백함수를 실행해준다.
미들웨어 내의 비동기함수에서 err에 대한 핸들링을 해주지않으면 최후의 보루인 에러 핸들러에도 에러가 잡히지 않는다. 왜냐하면 readFile()을 호출하는 것 자체만으로는 에러가 발생하지않았고, 콜백함수 첫번째 인자(err)에 에러가 전달되었기 때문이다.
그래서 비동기 함수의 경우에는 콜백함수 내에서 에러에 대한 적절한 처리를 해주어야한다!
Promise 함수 에러 핸들링
app.get('/file2', (req, res) => { fsAsync .readFile('/file2.txt') // .catch((error) => { res.status(404).send('file2 not found!'); }); //.catch(next); // 최종 에러 핸들러가 있다면, error를 next로 전달해도 됨 }); app.use((error, req, res, next) => { console.error(error); res.status(500).json({ message: 'Something went wrong' }); });
Promise는 콜백함수를 등록하지는 않지만, .then((data)=>{}).catch((error)=>{}) 와 같이 정상적으로 잘 동작했다면 data를 받아서 then을 실행하고 에러가 나면catch 의 인자로 error 를 받아 에러를 처리한다.
Promise 코드를 호출한다고 해서, 그 호출하는 코드에 문제가 있는 것은 아니기 때문에 에러가 발생하지 않고, 추후 비동기로 함수가 실행되는중 에러가 발생하면 catch의 인자로써 들어가기때문에 에러가 잡히지 않는다. 비동기함수는 try-catch 로 에러를 잡을 수 없다. (함수 실행 자체에는 문제가 없고 추후, 내부에서 에러가 발생하기 때문)
그래서 Promise 함수의 경우에는 .then().catch()의 catch 내에서 에러 핸들링을 해줘야한다. 에러를 next(error); 로 전달해도 되고, res를 직접 보내도 된다.
async / await 에러 핸들링
app.get('/file3', async (req, res) => { try { const data = await fsAsync.readFile('/file2.txt'); } catch { res.sendStatus(404); } }); app.use((error, req, res, next) => { console.error(error); res.status(500).json({ message: 'Something went wrong' }); }); app.listen(8080);
const data = await fsAsync.readFile('/file2.txt');
위 코드는 file2.txt 를 읽어오는걸 기다렸다가 data 변수에 담는 것으로 동기적으로 실행된다.
async로 함수를 감싸주면, 함수 내부에서는 await 라는 키워드를 이용하여, 순차적(동기적)으로 실행되는 것처럼 처리할 수 있지만, 함수 자체는 비동기인 Promise로 감싸진다. 그렇기 때문에 async 콜백함수 내부에서 에러가 발생하면 Promise 내에서 에러가 발생하는것과 동일하기때문에 catch를 이용해서 잡아야한다.
비동기 에러 처리하는 방법 – 최신 버전
import express from 'express'; import fsAsync from 'fs/promises'; const app = express(); app.get('/', (req, res, next) => { return fsAsync.readFile('/file2.txt').catch(next); }); app.use((error, req, res, next) => { console.error(error); res.status(500).json({ message: 'Something went wrong' }); next(); }); //github.com/expressjs/express/issues/2259#issuecomment-433586394 //github.com/blakeembrey/async-middleware app.listen(8080);
비동기 함수에서 catch를 사용하지 않더라도, 마지막 안전망(에러 처리 미들웨어)에 에러를 떨어트릴 수 있는 방법이 있다.
express 버전 5 미만은, ‘express-async-error’ 모듈을 사용하여, promiss를 리턴하기만 하면 안전망에서 에러를 잡을 수 있다.
express 버전 5부터는 promiss를 리턴해도 별 다른 에러처리를 하지 않는다면, 제일 마지막 안전망에서 에러를 처리할 수 있다.
중요한 포인트 ✏️
- 각각의 미들웨어에서 에러가 발생했을때 적절한 에러메세지를 사용자에게 보내줘야 한다.
- 동기 / 비동기 에러 처리하는 방법이 다르다
- 동기 함수의 경우 실수로 에러처리를 하지않더라도 마지막 안전망(에러 처리 미들웨어)가 에러를 포착할 수 있지만, Promise 혹은 Async(비동기적)을 쓸때 에러가 발생한다면, 외부에서는 에러를 감지할 수 있는 방법이 없다.