에러핸들링
에러 핸들링의 목적과 구분
에러 핸들링
에러 발생 전 사전 처리, 발생한 에러를 잡아내고 처리하는 일련의 과정
1. 에러 핸들링 목적
서비스 사용자의 흐름이 막히지 않도록 유도하여 이탈 방지
이용 간 발생 가능한 에러 사전 예측하고, 에러 발생 시 발생 알림 및 정상 구동에 대한 안내 필요.
안정화에 대한 믿음을 줌으로써 서비스 이탈 방지
2. 에러 핸들링 구분
2-1. 에러 핸들링(Error Handling)
컴퓨터가 발생시키는 에러 - 문법 오류, 통신 장애로 인한 에러 등 런타임과정에서 발생하는 에러들
2-2. 예외 핸들링(Exception Handling)
개발자가 의도적으로 발생시키는 예외상황 - 런타임 실행은 문제 없지만 서비스 자체 규칙에 위배된 상황들을 알림(이메일, 비밀번호 등)
- Ex. 이메일에 @ 포함여부/ 비밀번호 10자리 이상 조건 위배 시 에러 발생시키기
const { email, password } = req.body
if (!email.includes('@')) {
return new Error('EMAIL_INVALID')
}
if (password.length < 10) {
return new Error('PASSWORD_INVALID')
}
throw와 try-catch
1. throw
예외 상황에 에러를 발생시키는 방법
어떤 예외 상황, 어떤 에러인지 설명하기 위한 메시지 필수
throw new Error('EMAIL_INVALID')
예외 발생 시, 진행하던 작업을 중단시키고 위 에러를 호출한 상위 모듈로 제어를 넘김.
2. try-catch
try 블록에는 에러가 발생할 가능성이 있는 코드
catch 블록에는 에러가 발생했을 때를 대비한 행동
- try 블록에서 발생한 에러를 throw하면 catch 블록으로 넘어감
- 그렇기 때문에, try 블록 내부에 throw가 있거나/ throw 코드를 내장하고 있는 다른 함수를 호출함
- catch 블록에서 에러를 throw 하면 상위 모듈로 에러 전달
try {
const { email } = req.body
if (!email.includes('@')) {
const error = new Error('EMAIL_INVALID')
error.statusCode = 400
throw error
}
} catch (err) {
console.log(err) // 개발자 확인을 위한 콘솔
return res.status(201).json({ message : "USER_CREATED" });
}
에러 핸들링과 미들웨어
1. 미들웨어
모듈화하여 공통된 작업을 수행할 수 있는 역할
Express의 미들웨어는 컨트롤러와 컨트롤러를 이어주는 또 다른 컨트롤러의 한 종류.
즉, 컨트롤러 사이에 위치하여, 컨트롤러 진입 전, 공통적으로 해야하는 작업들을 처리하는 기능
- 중복되는 기능들이라고해서 전부 미들웨어로 생성하는 것은 아님
- 대표적 미들웨어 - CORS 정책 설정 미들웨어/ 서버에서 일어나는 상황을 기록(로그, log)하는 logging 미들웨어
- 에러 핸들링 또한 미들웨어 사용 권장
2. 에러 핸들링에서 미들웨어의 중요성
각 계층에서 발생하는 에러를 한 군데로 모아서 처리하면 유지보수 측면에서 유리함.
발생 한 에러를 response로 반환하지 않고 throw 하면(던지면) 에러 처리 미들웨어 한군데에서 catch(잡아서) 처리
해당 미들웨어에 로그 기능까지 추가한다면 기록까지 가능
const { email } = req.body
if (!email.includes('@')) {
throw new Error('EMAIL_INVALID')
}
에러 핸들링 적용
1. throw로 에러 던지기
에러가 발생한 모듈을 호출한 곳이나 상위 계층에서 에러 캐치 가능
1-1. 동기함수에서 에러 던지기
- someParam이 특정 값을 가지고 있지 않다면 에러 던짐
- 에러를 던지면(throw) 함수 밖으로 나옴(에러 발생 시 함수를 진행할 필요가 없는 경우)
// func.js
function someFunc(someParam) {
if (!someParam) {
throw new Error('someError');
}
return someParam;
}
module.exports = { someFunc }
1-2. 비동기함수에서 에러 던지기
- 동기적인 경우와 동일하나, 던져진 에러를 잡아내는 곳에서 비동기적 에러 처리가 필요함. (동기적 처리시 Promise Rejection 발생)
- await 를 사용하여 try - catch
- promise - catch의 기능 활용
- Express 에서는 async wrapping 모듈로 처리도 가능
// func.js
// 동기적 함수 someFunc에 이어서 작성
async function someAsyncFunc(someParam) {
if (!someParam) {
throw new Error('someError');
}
return someParam;
}
module.exports = { someFunc, someAsyncFunc }
2. 비동기 방식 try - catch 에러 핸들링
2-1. await 방식
- 상위 모듈 caller( ) 를 async 함수로 만들어 줌.
- 하위 모듈 { someAsyncFunc } 에 await 를 걸어주며 비동기 동작
// caller.js
const { someAsyncFunc } = require('./func');
async function caller() {
console.log('첫번째 콘솔');
try {
await someAsyncFunc();
}
catch(error) {
console.log(error); // Error: someError
}
console.log('두번째 콘솔');
}
caller();
// 최종적으로 콘솔에 보이는 것
첫번째 콘솔
Error: someError
두번째 콘솔
2-2. promise - catch 방식
- 상위 모듈 caller( ) 를 동기적 함수로 유지
- 항상 promise 를 리턴하는 async function에서 catch를 사용
- 동기적인 작업들이 먼저 출력이 되고 그 뒤에 비동기 작업들이 출력
// caller.js
const { someAsyncFunc } = require('./func');
function caller() {
console.log('첫번째 콘솔');
someAsyncFunc().catch((error) => {
console.log(error);
// Error: someError
});
console.log('두번째 콘솔');
}
caller();
// 최종적으로 콘솔에 보이는 것
첫번째 콘솔
두번째 콘솔
Error: someError
3. Express 미들웨어로 에러 핸들링
- someQuery에 매개 변수가 없이 api 를 호출한다면 undefiend로 정해진 값으로 인해 someFunc은 에러를 던짐
- res.json()은 실행되지 않고 Express 의 기본적인 에러 처리방법으로 처리됨
// app.js
const express = require('express');
const { someFunc, someAsyncFunc } = require('./func');
const app = express();
app.get('/someFunc', (req, res) => {
const { someQuery } = req.query;
const someValue = someFunc(someQuery);
res.json({ result: someValue });
});
app.get('/someAsyncFunc', async (req, res) => {
const { someQuery } = req.query;
const someValue = await someAsyncFunc(someQuery);
res.json({ result: someValue });
});
app.listen(3000);
3-1. 에러핸들링 미들웨어 추가
- 라우터에서 던지는 에러를 하나로 통일하여 받을 수 있음.
- 사용자 받는 에러를 파악하여 정확한 디버깅 가능
- 일관적인 인터페이스를 유지 가능
// app.js
const express = require('express');
const { someFunc, someAsyncFunc } = require('./func');
const app = express();
app.get('/someFunc', (req, res) => {
const { someQuery } = req.query;
const someValue = someFunc(someQuery);
res.json({ result: someValue });
});
app.get('/someAsyncFunc', (req, res) => {
const { someQuery } = req.query;
const someValue = someAsyncFunc(someQuery);
res.json({ result: someValue });
});
// error handling 미들웨어
app.use((err, req, res, next) => {
if (err.message === 'someError') {
res.status(400).json({ message: "someQuery notfound." });
return;
}
res.status(500).json({ message: "internal server error" });
});
app.listen(3000);
3-2. async wrapping 적용을 통한 비동기 모듈 에러 잡기
- asyncWrap을 컨트롤러에 씌워 주어 비동기 컨트롤러에서 생기는 에러 잡음
- 해당 에러는 ‘next’를 통해 에러 핸들링 미들웨어로 넘어감
// async-wrap.js
function asyncWrap(asyncController) {
return async (req, res, next) => {
try {
await asyncController(req, res)
}
catch(error) {
next(error);
}
};
}
module.exports = asyncWrap;
---------------------------------------------------------------
// app.js
const asyncWrap = require('./async-wrap');
app.get('/someAsyncFunc', asyncWrap(async (req, res) => {
const { someQuery } = req.query;
const someValue = await someAsyncFunc(someQuery);
res.json({ result: someValue });
}));
레이어드 패턴 에러핸들링 적용
1. Controller Error
- KEY_ERROR - Request로 들어와야하는 정보 가운데 누락된 key가 있을 때 (요청 자체가 문제가 있는 상황)
- request body라는 객체 안에 email 이라는 특정 키가 없는 경우
- 빈값이 입력된 경우와 다름 (빈 값의 경우는 에러 발생 안 함)
console.log(req.body) // { password : 'myPassword' }
const { email, password } = req.body
console.log(email) // undefined
console.log(password) // 'myPassword'
if (!email) {
throw new Error('KEY_ERROR')
err.statusCode = 400;
throw err
} // 에러 발생 시점
2. Service Error
- 비즈니스 로직과 규칙을 설정하는 곳
- 개발자가 직접 의도한 예외와 관련된 에러가 발생
- 사용자의 요청의 문제가 아니라 내부 처리 과정 중 문제라면 500번대 Status Code를 보내는 것이 일반적
//service/userService.js
const userDao = require('../models/userDao')
// 정규 표현식을 이용한 비밀번호 유효성 검증
const signUp = async (name, email, password, profileImage) => {
const pwValidation = new RegExp(
'^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,20})'
);
if (!pwValidation.test(password)) {
const err = new Error('PASSWORD_IS_NOT_VALID');
err.statusCode = 409;
throw err;
}
const createUser = await userDao.createUser(
name,
email,
password,
profileImage
);
return createUser;
};
module.exports = {
signUp
}
3. Dao Error
- 데이터베이스의 문제로 에러가 발생한다면 500번대 status code
//models/userDao.js
const createUser = async ( name, email, password, profileImage ) => {
try {
return await myDataSource.query(
`INSERT INTO users(
name,
email,
profile_image,
password
) VALUES (?, ?, ?, ?);
`,
[ name, email, password, profileImage ]
);
}
catch (err) {
const err = new Error('INVALID_DATA_INPUT');
err.statusCode = 500;
throw err;
}
};
module.exports = {
createUser
}