웰제오의 개발 블로그

Node.js 에서 전역 에러 처리하기 본문

개발

Node.js 에서 전역 에러 처리하기

웰치스제로오렌지 2022. 9. 24. 23:30

프로그래밍에 있어서 예외 ( 에러 ) 처리는 필수적이다.

일반적으로는 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 들 중에는 uncaughtExceptionunhandledRejection 가 있는데 이 둘을 활용하면 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 문에서 처리되는것이 좋다

Comments