Error Handling 적용하기

Gaeun·2023년 1월 3일
0

wecode TIL

목록 보기
15/24

1. throw로 에러 던져보기

개발자가 작성하는 모듈에서 발생가능한 에러 상황에서 던지게 되며 상위 계층이나 호출하는 곳에서 모듈의 에러를 감지할 수 있다. 아래는 에러를 던지는 기본적인 방법이다.

1-1. 동기함수에서 에러 던지기

// func.js
function someFunc(someParam) {
  if (!someParam) {
    throw new Error('someError');
  }

  // ...someFunc의 로직

  return someParam;
}

module.exports = { someFunc }

위 예시 코드에서 매개 변수 someParam이 특정한 값을 가지고 있지 않다면 에러를 던진다. someFunc이 someParam 값이 없을 때, 더 이상 함수를 진행할 필요가 없을 때 처리할 때 사용된다.

1-2. 비동기함수에서 에러 던지기

// func.js

...someFunc

async function someAsyncFunc(someParam) {
  if (!someParam) {
    throw new Error('someError');
  }

  // ...someAsyncFunc의 로직

  return someParam;
}

module.exports = { someFunc, someAsyncFunc }

위 내용은 비동기 함수 내에서 throw 하는 코드이다. 에러를 던질 때에는 위 동기방식의 함수와 큰 차이는 없다. 다만, 비동기 함수의 throw는 Promise Rejection을 발생시키기 때문에 에러를 잡아내는 곳에서는 다른 방식을 이용해야한다.

그리고 비동기 작업은 동기적인 에러 핸들링 방식으로 처리 할 수 없다. 만일 비동기 에러를 처리하지 않으면 unhandled promise rejection 으로 인해 프로그램 자체를 종료시키기 때문에 꼭 비동기적인 방법을 이용하여서 해당 에러를 사전에 핸들링 해주어야 한다.

이를 위해서 활용가능한 두 가지 방법은 바로 (1) await 를 사용하여 try - catch 로 에러 핸들링을 하는 것과 (2) promise - catch의 기능을 이용하여 에러 핸들링을 하는 방법이 있습니다. Express 에서는 async wrapping 모듈로 비동기 함수를 처리할 수 있다.

2. try-catch 구문으로 에러 핸들링


라이브러리 혹은 개발자가 작성한 모듈에서 throw가 발생했다면 상위 모듈에서 해당 에러를 잡아낼 수 있다. 에러를 잡아낸 후 return 및 throw를 하지 않으면 로직은 계속 진행되며 멈추지 않는다.

2-1. 동기 방식일 때

// caller.js

const { someFunc } = require('./func');

function caller() {
    const someValueWithParam = someFunc(1);
    console.log("someValue:", someValue1);
    // someValue: 1
	
    const someValueWithoutParam = someFunc();
    // Error: someError
    // 에러가 발생하였으므로 더 이상 실행되지 않는다.
    console.log('someValue', someValueWithoutParam);
}
caller();

// 최종적으로 콘솔에 보이는 것
someValue: 1

위 예시 코드는 someFunc을 import 하여 매개 인자에 값을 넣었을 때와 넣지 않았을 때 차이를 보여주는 코드이다. 매개 인자에 값을 넣게 되면 someFunc의 로직에 의해 정상적인 값을 반환해주지만 매개 인자에 값을 넣지 않는다면 someFunc은 throw를 하게 된다. 그 때, 에러가 발생하였기 때문에 더 이상 로직이 실행되지 않고 함수가 종료된다. 이제 try - catch 구문을 사용해보자.

// caller.js

const { someFunc } = require('./func');

function caller() {
    const someValueWithParam = someFunc(1);
    console.log("someValue:", someValueWithParam);
    // someValue: 1
	
    try {
        const someValueWithoutParam = someFunc();
	// 에러가 발생하였으므로 더 이상 실행되지 않는다.
	console.log('someValue', someValueWithoutParam);
    }
    catch(error) {
        console.log(error);
        // Error: someError
    }

    console.log('여기는 실행됩니다.');
}
caller();

// 최종적으로 콘솔에 보이는 것
someValue: 1
Error: someError
여기는 실행됩니다.

위 예시 코드는 try-catch를 써서 에러가 발생하면 해당 에러를 잡아두도록 프로그래밍 되어 있다. try 안에 에러를 검출해 낼 코드를 작성하고 catch에 에러를 검출했을 시에 관한 작업을 작성한다. 이후 try - catch 문을 제외한 부분은 다시 정상적으로 실행이 됩니다.

try-catch 이외에 try-catch-finally로도 작성할 수 있다. finally 블록에 있는 구문은 try 혹은 catch 이후에 무조건적으로 한번은 실행되는 영역이다.

2-2. 비동식 방식일 때

// caller.js

const { someAsyncFunc } = require('./func');

function caller() {
    try {
        someAsyncFunc();
    }
    catch(error) {
        console.log(error);
    }
}
caller();

// 최종적으로 콘솔에 보이는 것
Unhandled Promise Rejection: Error: someError

위 예시 코드에서 비동기 함수 에러는 잡히지 않는다.

최종적으로 unhandled 에러 즉, 처리되지 않는 에러로 잡히게 되는데, 그 이유는 이벤트 루프, 태스크 큐와 관련된 내용과 연관되어 있으며 추후 작성할 예정이다. 이 처리되지 않은 에러를 핸들링 하기 위해서 특수한 장치를 두어야 잡히게 되는데 두 가지 방법이 있다. 첫번째는 await를 사용하는 방법, 두번째는 promise - catch를 사용하는 방법이다.

2-2-1. 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
두번째 콘솔

await를 사용하면 동기방식에서 사용했던 방법대로 try - catch 구문을 사용할 수 있다. 다만 하위 모듈 { someAsyncFunc } 에 await 를 걸어주기 위해 상위 모듈 ‘caller( )’ 또한 async 함수로 만들어 주어야 하는 현상이 생긴다. 만약 caller() 함수를 비동기로 만들고 싶지 않으면 promise, catch 방식을 사용한다.

2-2-2. promise - catch 방식

// caller.js
const { someAsyncFunc } = require('./func');

function caller() {
    console.log('첫번째 콘솔');
    someAsyncFunc().catch((error) => {
        console.log(error);
        // Error: someError
    });
    console.log('두번째 콘솔');
}
caller();

// 최종적으로 콘솔에 보이는 것
첫번째 콘솔
두번째 콘솔
Error: someError

promise-catch는 위 ‘someAsyncFunc()’ 함수의 리턴값이 그러 하듯이 (async function은 늘 promise 를 리턴한다) promise를 리턴 받는 상황에서 사용할 수 있다.

이는 await 를 사용하지 않아 caller를 동기 함수로 유지할 수 있다. 하지만 비동기 함수는 비동기적으로 로직 처리 및 에러 처리를 하기 때문에 동기적으로 작동하지 않는다. 그래서 위 콘솔과 같이 동기적인 작업들이 먼저 출력이 되고 그 뒤에 비동기 작업들이 출력된다.

3. Express 미들웨어로 에러 핸들링

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', (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);

미들웨어를 추가하여서 이제 라우터에서 던지는 에러를 하나로 통일하여 받을 수 있게 되었다.

위 코드 예시에서 가장 마지막에 적혀있는 error handling 미들웨어에서 보는 것과 같이 이제 사용자에게 어떤 에러가 갈지 예측할 수 있으며 이는 일관적인 인터페이스를 유지할 수 있게 만들어 준다. 하지만 이 방법으로 했을 경우 여전히 맹점이 있는데, 바로 비동기 모듈 에러는 잡지 못 한다는 점이다. 이는 또 다른 모듈인 async wrapping을 작성 및 적용하여 해결 할 수 있게 된다.

// 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 });
}));

이제 asyncWrap을 컨트롤러에 씌워 주게 된다면 비동기 컨트롤러에서 생기는 에러를 잡을 수 있게 된다. asyncWrap은 컨트롤러를 받아서 비동기 에러를 처리하는 새로운 컨트롤러를 만드는 모듈이다. 해당 에러는 ‘next’를 통해 에러 핸들링 미들웨어로 넘어가게 된다.

4. Summary

  • Node.js 백엔드 개발 환경에서 처리 할 수 있는 에러 핸들링에는 크게 세 가지 방법 (Throw, Try & Catch, Middleware) 이 존재한다.
  • Throw와 Try-Catch를 이용한 방식은 각각 동기, 비동기를 기준으로 문법을 달리 표현할 수 있다.
  • Middleware를 이용한 방식은 다양한 에러를 개발자가 의도한 특정 에러로 수렴하여 전달 할 수 있다는 장점이 있다. 비동기 에러 핸들링을 Middleware를 이용하여 처리 하려면 ‘async-wrap’ 과 같은 별도의 컨트롤러를 적용해야 한다.
  • 각각의 에러 핸들링 방식은 특정 한계점이 있으며, 서로 상호 보완하는 모습을 보인다.
profile
🌱 새싹 개발자의 고군분투 코딩 일기

0개의 댓글