(NestJS) Nest JS - Exception Filters

최건·2025년 5월 8일

참고 문서

Exception Filters란?

  • NestJS에서 예외 발생 시 에러 응답의 형식과 처리 방식을 개발자가 직접 제어할 수 있도록 해주는 전용 에러 처리 컴포넌트이다.
  • NestJS 애플리케이션 전반에서 발생하는 예외를 가로채어, 적절한 응답 형식을 생성하거나 추가적인 처리를 수행할 수 있도록 해주는 구조화된 예외 처리 메커니즘이다.
  • NestJS의 기본 Exception Filter는 기본적으로 HttpException 또는 그 하위 클래스들을 처리하도록 설계되어 있다.

HttpException이란?

  • NestJS는 REST API 및 GraphQL API에서 에러 발생 시 HTTP 상태 코드에 맞는 표준화된 에러 응답을 보내기 위해 HttpException 클래스를 제공한다.
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
  • 이처럼 예외를 발생시키면 NestJS가 자동으로 다음과 같은 응답을 만들어 준다.
{
  "statusCode": 403,
  "message": "Forbidden"
}

HttpException 생성자 인자

new HttpException(response, status, options?)
  • respons : JSON 응답에 담길 본문. 문자열이면 메시지로 처리되고, 객체를 넘기면 그대로 응답 바디로 사용됨
  • status: HTTP 상태 코드 (HttpStatus.FORBIDDEN 등)
  • options (선택) 내부적으로만 사용하는 cause 값을 포함할 수 있음 (로깅에 유용)

메시지 커스터마이징 방법

(1) 기본 메시지 사용 (문자열)

throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);

응답:

{
  "statusCode": 403,
  "message": "Forbidden"
}
  • 문자열만 넘기면 message 필드만 변경됨. Nest가 자동으로 statusCode를 추가.

(2) 전체 JSON 응답 구조 커스터마이징 (객체 전달)

throw new HttpException(
  {
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  },
  HttpStatus.FORBIDDEN,
  {
    cause: error  // 내부 로깅용, 클라이언트에게는 응답되지 않음
  }
);

응답:

{
  "status": 403,
  "error": "This is a custom message"
}
  • 이 경우 statusCode 대신 status가 들어가고, message 대신 error라는 사용자 지정 키가 사용됨.
  • 내부적으로 로깅할 때 어떤 원인(exception)이 있었는지 추적할 수 있게 함

Custom Exceptions란?

  • NestJS에서 기본으로 제공하는 BadRequestException,NotFoundException 등의 예외 클래스 외에, 프로젝트 상황에 맞게 새롭게 정의한 예외 클래스

예시: forbidden exception 사용

// forbidden.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}
// cats.controller.ts
@Get()
async findAll() {
  throw new ForbiddenException();
}

응답:

{
  "statusCode": 403,
  "message": "Forbidden"
}

@nestjs/common 에서 지원해주는 Exception

클래스 이름의미상태 코드
BadRequestException잘못된 요청400
UnauthorizedException인증 실패401
ForbiddenException접근 금지403
NotFoundException리소스 없음404
ConflictException리소스 충돌409
InternalServerErrorException서버 에러500
ServiceUnavailableException서비스 사용 불가503
ImATeapotException유머용 상태 코드 🍵418
⋯ 등 총 20여 개 예외를 제공

사용방법

throw new NotFoundException('User not found');

응답:

{
  "statusCode": 404,
  "message": "User not found"
}

고급 사용법 : options 파라미터 사용

throw new BadRequestException('Something bad happened', {
  cause: new Error(),
  description: 'Some error description',
});
항목설명
'Something bad happened'기본 메시지 (message 필드에 들어감)
cause내부 로깅이나 추적용 원인 예외 (응답에는 포함되지 않음)
description사용자 정의 상세 설명 (error 필드에 포함됨)

응답:

{
  "message": "Something bad happened",
  "error": "Some error description",
  "statusCode": 400
}

왜 커스텀 Exception Filter를 쓰는가?

NestJS의 기본 예외 필터는 HttpException 기반 예외에 대해 잘 작동하지만, 다음과 같은 요구가 있을 때는 직접 예외 필터를 구현해야 한다

  • 예외 발생 시 로깅 추가
  • 응답 구조를 API 사양에 맞게 수정
  • 클라이언트 요청 경로, 타임스탬프 등을 응답에 포함
  • 예외 타입에 따라 분기 처리

예시

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();                         // HTTP 컨텍스트 추출
    const response = ctx.getResponse<Response>();            // Express의 Response 객체
    const request = ctx.getRequest<Request>();               // Express의 Request 객체
    const status = exception.getStatus();                    // 상태코드 추출

    // 로깅 서비스 이용 등...
    response.status(status).json({                           // 직접 응답 구성
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}
구성 요소설명
@Catch(HttpException)이 필터는 HttpException만 처리함
ExceptionFilter<T>Nest의 예외 필터 인터페이스. T는 처리할 예외 타입
catch()예외가 발생했을 때 호출되는 메서드
ArgumentsHost현재 실행 컨텍스트(HTTP, WebSocket 등)를 제공
switchToHttp()HTTP 컨텍스트로 전환
getRequest() / getResponse()Express의 Request/Response 객체 추출
response.status().json()직접 응답을 작성 (상태 코드, 경로, 시간 포함)

Catch(...)

  • 특정 예외 클래스만 걸러내기 위한 데코레이터
  • 특정 예외만 등록해서 걸러내기 가능
  • ex) @Catch(BadRequestException, ForbiddenException)

Exception Filter 적용 범위

1. 메서드 범위(Method-scoped)

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

2. 컨트롤러 범위(Controller-scoped)

@Controller()
@UseFilters(HttpExceptionFilter)
export class CatsController {
  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    throw new ForbiddenException();
  }

  @Get()
  async findAll() {
    throw new ForbiddenException();
  }
}

3. 전역 범위(Global-scoped)

방법 A. main.ts에서 적용

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());  // 전역 필터 등록
  await app.listen(3000);
}
  • new HttpExceptionFilter()로 직접 인스턴스화하므로 의존성 주입 불가
  • 예: 로깅 서비스, 설정 서비스 등 주입 불가

방법 B. AppModule에서 APP_FILTER 토큰을 활용한 등록 (DI 가능)

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}
  • NestJS가 자동으로 인스턴스를 생성하고 의존성 주입도 지원
  • 진짜 글로벌 필터이면서도 유연하게 활용 가능

모든 예외 잡기

  • Catch() 데코레이터에 아무 인자도 주지 않음으로써 예외의 종류와 상관없이 전부 잡는 필터를 만드는 것
  • @Catch()에 아무 인자도 넣지 않으면 모든 종류의 예외(즉, HttpException 이외의 일반 Error, TypeError, ReferenceError 등도 포함)를 포괄 처리할 수 있다.
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';

@Catch()
export class CatchEverythingFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

응답:

// ex) 
const responseBody = {
  statusCode: 500,
  timestamp: new Date().toISOString(),
  path: httpAdapter.getRequestUrl(ctx.getRequest()),
};

httpAdapter.reply(ctx.getResponse(), responseBody, 500);

->
{
  "statusCode": 500,
  "timestamp": "2025-05-08T06:30:45.123Z",
  "path": "/users"
}

	

BaseExceptionFilter 상속 후 커스터마이징

  • 기존 Nest의 예외 처리 로직을 유지하면서, 거기에 추가 로직을 더하고 싶을 때 사용한다.
  • BaseExceptionFilter는 Nest가 기본적으로 사용하는 내장 예외 필터
  • 기본 처리 로직을 재사용하면서 필요한 부분만 오버라이딩 할 수 있다.

Logger + Sentry 연동 예시

import {
  Catch,
  ArgumentsHost,
  HttpException,
  Logger,
} from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { Request } from 'express';
import * as Sentry from '@sentry/node';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest<Request>();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : 500;

    // Logger
    this.logger.error(
      `[${request.method}] ${request.url} -> ${status}`,
      (exception as any).stack,
    );

    // Sentry 전송
    Sentry.captureException(exception);

    // Nest 기본 처리 수행 (statusCode, message 응답 전송)
    super.catch(exception, host);
  }
}

적용 방법 1. main.ts에서 HttpAdapter 수동 주입

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));

  await app.listen(3000);
}

적용 방법 2. APP_FILTER 토큰을 이용한 의존성 주입 방식

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
  ],
})
export class AppModule {}

응답 예시

요청: GET /users/999

예외: throw new NotFoundException('User not found')

클라이언트 응답:

{
  "statusCode": 404,
  "message": "User not found"
}

로그 (콘솔):

[ERROR] AllExceptionsFilter - [GET] /users/999 -> 404
Error: User not found
    at ...

Sentry: 예외 상세 정보가 자동으로 전송

profile
개발이 즐거운 백엔드 개발자

0개의 댓글