Nest.js event-emitter에서 Error가 발생하면 서버가 터져

beygee·2023년 6월 15일
6

nest.js-error.handling

목록 보기
1/1
post-thumbnail

Overview.

nest.js 서버에서 이벤트 아키텍처를 이용할 일이 있어서 @nestjs/event-emitter 라이브러리를 사용하고 있었습니다.
프로덕션 환경에서 eventEmitter로 발행한 이벤트를 수신하여 비즈니스 로직을 처리하는 과정에서 런타임 에러가 발생했는데, 이게 서버 중단 현상을 초래했었습니다. 😅

예를 들어 아래와 같은 event.fired 이벤트 수신 과정에 문제가 생겼다고 가정해봅시다.

@OnEvent('event.fired', { async: true })
public async handleEvent(event: Event) {
  throw new Error('Event Error Fired!')
}

그럼 아래 로그와 같이 런타임 에러가 발생하여 이후 서버는 아무 요청도 받을 수 없게 됩니다.

nest.js 패키지에서 이런 에러 핸들링이 되지 않았던 걸까요?

예외 처리

nest.js exception.filter docs에 따르면,
요청, 응답과 같은 Execution Context 생애주기에서 발생하는 에러는 ExceptionFilter에서 에러를 캐치하여 예외를 처리할 수 있습니다.

하지만 EventEmitter와 같이 Context를 벗어난 곳에서는 따로 예외를 처리해주지 않습니다.
옆 동네 @nestjs/schedule 같은 경우는 어떨까요?

Cron의 경우

@Cron("*/5 * * * * *")
public async handleCron() {
  throw new Error("Cronjob Error!")
}

위 코드를 돌려보면 아래와 같은 로그가 출력됩니다.

한번에 서버가 종료되지 않고, 기본 nest.js loggerConsoleLoggerError 구문이 출력된 것을 볼 수 있습니다.
CronjobeventEmitter 둘 다 Execution Context 외부의 실행환경인데 왜 하나는 예외 처리가 되어있고 다른 하나는 서버가 중단될까요?
Cronjob의 경우는 누군가 이미 이슈를 제기하였고, 이에 따라 이 변경점으로 수정이 되었습니다.

하지만 EventEmitter의 경우는 대응되고 있지 않았습니다.
그래서 PR을 보내기 전 OnEvent 데코레이터를 급하게 수정할 필요가 있었습니다.
데코레이터 사용 경험을 그대로 살리면서 안전하게 서버가 중단되지 않도록 해야했습니다.

데코레이터 수정

// on-safe-event.decorator.ts

import { applyDecorators, Logger } from '@nestjs/common'
import { OnEvent, OnEventType } from '@nestjs/event-emitter'
import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces'

function _OnSafeEvent() {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value

    const metaKeys = Reflect.getOwnMetadataKeys(descriptor.value)
    const metas = metaKeys.map((key) => [key, Reflect.getMetadata(key, descriptor.value)])

    descriptor.value = async function (...args: any[]) {
      try {
        await originalMethod.call(this, ...args)
      } catch (err) {
        Logger.error(err, err.stack, 'OnSafeEvent')
      }
    }
    metas.forEach(([k, v]) => Reflect.defineMetadata(k, v, descriptor.value))
  }
}

export function OnSafeEvent(event: OnEventType, options?: OnEventOptions | undefined) {
  return applyDecorators(OnEvent(event, options), _OnSafeEvent())
}

함수 데코레이터를 만들어 내부에서 try-catch로 감싸 예외처리를 해주고, 에러 발생 시 Logger로 출력하도록 합니다.
이후에 기존의 nestjs/event-emitterOnEvent와 데코레이터를 합성시켜주면 간단하게 구현할 수 있습니다.

적용 후

이후에 OnEvent를 사용하던 곳에 OnSafeEvent로 변경을 해주면 됩니다.

@OnSafeEvent('event.fired', { async: true })
async handleEvent(event: Event) {
  throw new Error('Event Error Fired!')
}

@Timeout(3000)
async handleTimeout() {
  this.eventEmitter.emit('event.fired')
}

@Timeout(4000)
async handleTimeout2() {
  this.eventEmitter.emit('event.fired')
}

그러면 다음과 같이 에러를 만나도 중단되지 않고 Logger로 에러가 처리되는 모습을 볼 수 있습니다.

이로써 EventHandler 환경에서도 예상치 못한 에러가 발생했을 경우도 서버에 치명적인 손상을 주지 않으면서 대처가 가능하게 되었습니다.
물론 nest.js CQRS 환경에서도 이와 비슷한 이슈가 발생한다고는 합니다 🦦
현 상황에서는 각각의 아키텍처에 맞게 에러 테스트를 해보면서 서버를 견고하게 만들 필요가 있어보입니다.

그렇다면 process.nextTick이나 setTimeout과 같이 Node.js Event Loop의 Timers phasemicrotask queue에서 처리되는 비동기 콜백함수 내부에서 발생하는 에러는 어떻게 처리될까요?

이 내용은 다음 포스트에서 이어서 확인해보겠습니다.

References.

profile
Solidarite Co. CTO. Korea Univ Comp. Sci

2개의 댓글

comment-user-thumbnail
2024년 8월 21일

안녕하세요 작성자님, 좋은글 감사합니다.
23년 8월 부로 https://github.com/nestjs/event-emitter/pull/936/commits/e322cb820f528640545e2e5685bdb141bfdc9142 해당 PR 에 의해 suppressErrors 옵션이 추가되어 (Default: true) 이벤트 수신자에서 에러 발생 시에도 서버 종료가 되지 않는것으로 확인됩니다~!

1개의 답글