본문 바로가기
BackEnd

Error Handing 적용하기

by SoriKim 2023. 11. 19.
반응형

1. Error Handing 적용하는 방법 

이전 시간에는 Error Handing이 무엇인지 알아보았습니다. 이번 시간에는 Error Handing 적용하는 방법에 대해 살펴보겠습니다. 

 

1️⃣ throw로 에러 던져보기 

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

 

1) 동기함수에서 에러 던지기

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

  // ...someFunc의 로직

  return someParam;
}

module.exports = { someFunc }

 

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

 

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-cathch 구문으로 에러 핸들링

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

 

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) 비동기 방식일 때

다음은 비동기 함수를 호출하는 방식으로 에러 핸들링은 동기 방식과는 다릅니다.

// 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를 사용하는 방법입니다. 우선 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 방식을 사용합니다. 

 

// 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는 위 'somAsyncFunc()' 함수의 리턴값 async function은 늘 promist를 리턴하듯이 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', async (req, res) => {
  const { someQuery } = req.query;

  const someValue = await someAsyncFunc(someQuery);

  res.json({ result: someValue });
});

app.listen(3000);

해당 코드는 someFunc을 호출하기 위한 라우터로 someQuery라는 쿼리를 받아와 someFunc을 호출해 결과를 사용자에게 보여주는 역할을 합니다. 기본적으로 Express는 자동으로 에러를 처리하며 만약 someQuery란에 아무런 매개변수도 넣지 않고 api 호출 시 someQuery는 undefined로 결과값이 정해지고 이로 인해 someFunc는 에러를 던지게 됩니다. 에러를 던지게 된다면 하단에 작성된 res.json()은 실행되지 않고 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 handing 미들웨어에서 보는 것과 같이 이제 사용자에게 어떤 에러가 갈지 예측할 수 있으며 일관적인 인터페이스를 유지할 수 있게 만들어 줍니다. 그러나 이 방법을 사용한 경우 여전히 맹점이 존재하는데, 바로 비동기 모듈 에러는 잡지 못한다는 점입니다. 이런 점은 또 다른 모듈인 asyn 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'를 통해 에러 핸들링 미들웨어로 넘어가게 됩니다. 

 

반응형

'BackEnd' 카테고리의 다른 글

인증 & 인가에 대해서  (0) 2023.11.23
각 Layer 별 Error Handling에 대해  (0) 2023.11.23
Error Handing에 대해서  (0) 2023.11.19
[API] Middleware에 대해서  (0) 2023.11.15
[API] Path Parameter & Query Parameter에 대해서  (0) 2023.11.15

댓글