본문 바로가기
프로젝트/saebom

NestJS 예외 필터가 Discord.js에서 동작하지 않는 이유와 해결책

by 오오오니 2025. 3. 26.
💐

NestJS 예외 필터가 Discord.js에서 동작하지 않는 이유와 해결책

비어 있음
2025년 2월 24일 오전 2:40
완료

🌱 Discord 봇 개발 중 마주친 에러 처리 이슈

평소에 에러 처리 할때 사용하던 Exception Filter가 동작하지 않음.

왜 Exception Filter가 동작하지 않을까?

요약하면 디스코드봇은 http통신이 아니기 때문입니다.
Exception Filter는 HTTP 요청-응답 주기를 지원하게 설계되었는데 디스코드 봇은 이벤트 리스너 방식으로 동작하기 때문!

왜 HTTP 통신이 아니면 안되는데?

HTTP 통신이 아니면 안되는 이유를 알려면 먼저 실행 컨텍스트에 대해서 알아야합니다.
위의 링크에 있는 예제 코드입니다.
인수로 가지는 ArgumentsHost 에 대해 알아보겠습니다.
ArgumentsHost
💡
HTTP, WebSocket, RPC에서 예외가 발생했는지 알려주고, 그 요청/응답 객체에 접근할 수 있게 해주는 도구
ArgumentsHost는 실행 컨텍스트에 대한 정보를 제공하는 인터페이스입니다.
HTTP, WebSocket, RPC 컨텍스트에 대한 정보를 추상화해서 제공하는 역할을 합니다.
http에서 컨텍스트를 가져오려면 switchToHttp() WebSocket 컨텍스트로 전환하려면 switchToWs(): RPC컨텍스트로 전환하려면 switchToRpc() 를 사용할 수 있습니다.
그리고 실행 컨텍스트에서 요청, 응답 객체 등을 추출할 수 있습니다.
복사
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; import { Request, Response } from 'express'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); const status = exception.getStatus(); response .status(status) .json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, }); } }
디스코드 봇은 HTTP, WebSocket, RPC 에 다 해당되지 않습니다.
그래서 ExceptionFilter 를 사용할 수 없는 것이죠

그럼 디스코드봇은 뭘로 통신하는 걸까?

→ 웹소켓
🙋‍♀️ 그러면 WebSocket 에 해당하는거 아니야?
NestJS는 WebSocket 통신을 위한 프레임워크를 제공하는데 디스코드 봇은 자체적인 WebSocket 클라이언트를 사용합니다.
Discord.js는 디스코드 API와 통신하기 위해 WebSocket 위에 추가 기능을 구현했습니다. 그래서 NestJS의 WebSocket 게이트웨이와 호환되지 않습니다.
🙋‍♀️ 일반적인 WebSocket 통신은 뭐고 Discord.js의 WebSocket통신 방식은 뭔데?
간단히 설명하자면 Discord.js는 기본 웹소켓 기능 위에 추가적인 레이어가 있어서 WebSocket 게이트웨이와 호환되지 않아
정리하자면, “NestJS의 Exception Filter는 ExecutionContext 를 기반으로 동작해서 HTTP, RPC, WebSocket 통신에서 적용할 수 있지만 Discord.js는 이 중 해당 되는 곳이 없어서 안된다!” 입니다.

🌱 해결 방법: 데코레이터 패턴

선택한 데코레이터 패턴을 소개하고 함께 알아봤던 다른 에러 핸들링 방법을 간단하게 소개하겠습니다.

데코레이터 패턴

@ CatchError 데코레이터를 구현하고 이를 사용하면 선언적으로 에러 처리가 가능합니다.
중복된 try-catch 제거하여 코드 가독성 및 유지보수성 향상된다는 장점이 있습니다. 그리고 관심사를 분리할 수 있다는 장점이 있습니다.
커스텀 데코레이터를 만들고 래핑 함수 내부에 try-catch 블록을 두어 예외를 처리하였습니다.
복사
export function CatchError(options: ErrorHandlerOptions = {}) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; //래핑 함수 const wrappedMethod = async function (this: any, ...args: any[]) { try { //원래 메서드 return await originalMethod.apply(this, args); } catch (error) { await handleError(error, args[0], options); if (options.rethrow) { throw error; } } }; ...
위에서 만든 데코레이터를 이렇게 적용할 수 있습니다.
복사
@CatchError({ reply: false }) async getProblemById(id: number): Promise<Problem> { const problem = await this.problemRepository.findById(id); if (!problem) { throw new AppException(ErrorMessage.NotFound.PROBLEM); } return problem; }

이벤트 기반 중앙처리 방식

모든 에러를 중앙에서 처리하는방식입니다.
한 곳에서 처리 가능하지만, 에러를 처리하는데 필요한 정보 전달(명령어, 버튼정보, 모달 정보 등등)이 어려워지고 로직이 복잡해질 수 있습니다.
그리고 현재 서비스는 스케줄러를 많이 사용해서 사용자가 명령어를 입력할 때 뿐만 아니라 서버에서 먼저 메시지를 보내는 경우가 많아서 적합하지 않다고 생각했습니다.
복사
@Injectable() class ErrorHandlingService { handleError(error: unknown, context?: CommandContext) { if (error instanceof DiscordException) { // Discord 에러 처리 } else if (error instanceof AppException) { // 애플리케이션 에러 처리 } } }

EventEmitter 방식

에러이벤트를 구독하는 옵저버를 구현하는 방식입니다.
느슨한 결합을 유지하면서 에러 처리가 가능하지만, 에러 추적이 어려다는 단점이 있습니다.
스택 트레이스가 분리될 수 있는데 에러 처리할 때 추적하는 것이 중요하다고 생각해서 선택하지 않았습니다.