Dev/General

에러핸들링

싯벨트 2022. 9. 12. 09:59
728x90

에러 핸들링의 목적과 구분

에러 핸들링
에러 발생 전 사전 처리, 발생한 에러를 잡아내고 처리하는 일련의 과정 

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 에러 핸들링

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
}

참고자료

- https://expressjs.com/en/guide/error-handling.html