💐
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() 를 사용할 수 있습니다.
그리고 실행 컨텍스트에서 요청, 응답 객체 등을 추출할 수 있습니다.
TypeScript
복사
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 블록을 두어 예외를 처리하였습니다.
TypeScript
복사
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;
}
}
};
...
위에서 만든 데코레이터를 이렇게 적용할 수 있습니다.
TypeScript
복사
@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;
}
이벤트 기반 중앙처리 방식
모든 에러를 중앙에서 처리하는방식입니다.
한 곳에서 처리 가능하지만, 에러를 처리하는데 필요한 정보 전달(명령어, 버튼정보, 모달 정보 등등)이 어려워지고 로직이 복잡해질 수 있습니다.
그리고 현재 서비스는 스케줄러를 많이 사용해서 사용자가 명령어를 입력할 때 뿐만 아니라 서버에서 먼저 메시지를 보내는 경우가 많아서 적합하지 않다고 생각했습니다.
TypeScript
복사
@Injectable()
class ErrorHandlingService {
handleError(error: unknown, context?: CommandContext) {
if (error instanceof DiscordException) {
// Discord 에러 처리
} else if (error instanceof AppException) {
// 애플리케이션 에러 처리
}
}
}
EventEmitter 방식
에러이벤트를 구독하는 옵저버를 구현하는 방식입니다.
느슨한 결합을 유지하면서 에러 처리가 가능하지만, 에러 추적이 어려다는 단점이 있습니다.
스택 트레이스가 분리될 수 있는데 에러 처리할 때 추적하는 것이 중요하다고 생각해서 선택하지 않았습니다.