댓글로 한 분께서 23년 8월 부로 https://github.com/nestjs/event-emitter/pull/936/commits/e322cb820f528640545e2e5685bdb141bfdc9142 해당 PR 에 의해 suppressErrors 옵션이 추가되어 (Default: true) 이벤트 수신자에서 에러 발생 시에도 서버 종료가 되지 않는것으로 확인됨을 알려주셨습니다.
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
logger
인 ConsoleLogger
의 Error
구문이 출력된 것을 볼 수 있습니다.
Cronjob
과 eventEmitter
둘 다 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-emitter
의 OnEvent
와 데코레이터를 합성시켜주면 간단하게 구현할 수 있습니다.
적용 후
이후에 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 phase
나 microtask queue
에서 처리되는 비동기 콜백함수 내부에서 발생하는 에러를 적절히 처리해야하는 상황도 생길 수 있을 것 같습니다.