일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- Kafka
- node.js
- BOJ
- eventLoop
- 백준
- 25635
- Bitwise AND
- 파라매틱서치
- node-cron
- 전역에러처리
- 25186
- firebase functions deploy limit
- 알고리즘
- promise.race
- microtask
- 코드리뷰를꼼꼼히하자
- 23289
- nextTick
- firebase functions
- 20309
- 23560
- 귀납적증명
- macrotask
- Java
- 1781
- PS
- hash
- Docer
- graceful shutdown
- ad-hoc
- Today
- Total
웰제오의 개발 블로그
Node.js 에서 전역 에러 처리하기 본문
프로그래밍에 있어서 예외 ( 에러 ) 처리는 필수적이다.
일반적으로는 try/catch
문으로 처리하곤 하지만 이는 코드의 가독성을 떨어트리고,
매번 로직을 작성할 때 마다 발생할 수 있는 예외상황에 대한 코드를 작성해야 하고, 공통되는 상황에 있어서도 매번 같은 코드를 작성해야하는 불편함 또한 존재한다.
어쩔때는 catch 문 작성을 깜빡하는 실수를 하기도 한다..
이러한 단점들로 인해 많은 언어, 프레임워크 에서는 전역적인 에러처리를 지원해준다
Spring 을 사용해본 사람이라면 @RestControllerAdvice
라는 어노테이션을 들어봤을 것 이다
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(FooException.class)
public void handleFooException(FooException error) {
// ...
}
@ExceptionHandler(BarException.class)
public void handleBarException(BarException error) {
// ...
}
// ...
}
인턴 때 Spring 을 사용하면서 try / catch 문으로 일일이 예외처리하는 나를 보고 사수님이 알려주셨던 내용이고, 각 예외의 종류별 전역적인 처리를 가능하게 해준다
Typescript + Node.js 환경에서 작업을 하게 되면서 어김없이 예외처리 코드 작성을 마주하게 되었고,
어떻게 하면 Spring 의 RestControllerAdvice 와 같이 깔끔하게 예외를 처리할 수 있을지 고민하게 되었다.
과연 Node.js 에서는 전역적인 에러 처리가 가능할지, 나는 이를 어떻게 구현했고, 이의 장단점에 대해 간단하게 정리해보려고 한다
EventEmitter
Node.js 의 EventEmitter 클래스는 node 의 events 모듈에서 export 되는 클래스로, Javascript 에는 없는 이벤트 관련 개체를 Node.js 가 구현한 것 이다.
간단히 사용법을 살펴보면
const { EventEmitter } = require("events");
const hub = new EventEmitter();
hub.on("EVENT", (arg) => console.log(`EVENT has emitted with ${arg}`));
hub.emit("EVENT", "Hello");
/**
EVENT has emitted with Hello
*/
다음과 같이 on
메소드를 사용해 발생할 이벤트에 대한 함수를 등록하고,
emit
메소드를 통해 이벤트를 호출한다
이를 응용해 에러처리를 담당하는 EventEmitter 개체를 export 해, 예외 상황에 대한 event 를 처리하는 함수를 등록해놓으면, catch 에서의 예외처리 로직을 어느정도 공통적으로 관리할 수 있다
// 📄 errorHub.js
const { EventEmitter } = require("events");
const errorHub = new EventEmitter();
const errorHandler = (error) => {
const { type } = error;
switch(type) {
case "foo": //...
case "bar": //...
default: //...
}
}
errorHub.on("error", errorHandler);
export default errorHub;
==============================================================
// 📄 index.js
const errorHub = require("./errorHub.js");
//...
if(isFooErrorSituation) {
errorHub.emit("error", {type: "foo", errorBody: ...})
}
try {
//...
} catch(e) {
errorHub.emit("error", {type: "bar", errorBody: ...})
}
catch 에 처리로직이 들어가지 않아 한층 깔끔해졌지만, 여전히 try / catch 문은 남아있다
process 개체를 활용해 try / catch 문까지 없애보자
process
Node.js 사용자들에게는 process.env
에 익숙한 process 개체이다
Node.js 공식문서를 확인해보면
The process object is an instance of EventEmitter
와 같이, process 개체는 EventEmitter 의 구현체 라고 나와있다.
즉, EventEmitter 개체를 전역 모듈로 export 하지 않아도 코드 어디서나 process 개체를 통해 이벤트 활용이 가능한 것 이다.
pre-defined 된 event 들 중에는 uncaughtException
과 unhandledRejection
가 있는데 이 둘을 활용하면 try / cath 문 없는 전역 에러 처리가 가능하다
process.on("uncaughtException", (error) => console.log(`Error occured with ${error}`));
process.on("unhandledRejection", (error) => console.log(`Error occured with ${error} inside promise`))
console.log("process started");
Promise.reject("REJECTED");
throw new Error("CUSTOM EXPECTION");
/**
process started
Error occured with Error: CUSTOM EXPECTION
Error occured with REJECTED inside promise
*/
위의 예시와 같이 uncaughtException 은 동기 에러가, unhandledRejection 은 비동기 에러가 발생했을 때 emit 됨을 알 수 있다
이를 응용해 보다 깔끔한 전역적인 에러의 처리가 가능하다
function errorHandler(error) {
if(error instanceof FooException) {
// ...
}
if(error instanceof BarException) {
// ...
}
}
process.on("uncaughtException", errorHandler);
process.on("unhandledRejection", errorHandler)
단점
처음에 이 방법을 알아내곤 이게 모든걸 해결해주리라 믿고 이를 엄청 남용했었다.
제대로 이해하지 못하고 사용했던 탓인지 크리티컬한 이슈를 마주했었고 그때 원인을 파악하느라 꽤나 고생했었다
다음의 코드를 보고 실행결과를 예측해보자,
function errorHandler(error) {
console.log(`Error occured with ${error}`);
}
process.on("uncaughtException", errorHandler);
process.on("unhandledRejection", errorHandler)
function foo() {
throw new Error("error");
}
function functionThatMustNotExecutedInErrorState() {
console.log("Global error handler won't be executed synchronously!!")
}
Promise.resolve(functionThatMustNotExecutedInErrorState());
foo();
/**
Global error handler won't be executed synchronously!!
Error occured with Error: error
*/
process 개체를 활용한 전역 에러 처리는 기존의 try / catch 문과는 다르게 에러 처리가 동기적으로 이루어지지 않는다는 큰 문제가 있다
이는 EventEmitter 개체의 event 발생시의 콜백함수로 등록된 작업들은 micro task queue 에 큐잉되기 때문인데
만약 에러 발생 이전에 nextTick queue 에 혹은 micro task queue 에 큐잉된 작업이 있다면, 이들이 먼저 실행된 후 에러처리 로직이 실행된다
만약 미리 큐잉된 작업이 에러 발생여부에 영향을 받는 작업이라면, 정말 예기치 못한 상황이 벌어질 수 있다
따라서 위와 같은 방법을 통한 전역에러 처리는 비동기적으로 이루어져도 괜찮은 에러들을 담당하게 하고,
다른 작업에 영향을 줄 수 있어 동기적으로 처리되어야 하는 에러들은 try / catch 문에서 처리되는것이 좋다
'개발' 카테고리의 다른 글
[Node.js] Thread Hang 을 야기할 수 있는 작업의 핸들링 (Promise.race, Worker Thread) (1) | 2022.10.27 |
---|---|
배포 프로세스 최적화를 통한 GCP functions deploy limit 해결 (0) | 2022.10.22 |
AWS ECS graceful shutdown 설정 및 트러블 슈팅 (1) | 2022.10.21 |
Node.js 환경에서의 반복작업 수행 (0) | 2022.10.19 |
kafka-consumer-groups.sh NullPointerException 트러블 슈팅 (2) | 2022.10.04 |