Skip to content

6. Exception filters


Nest에는 애플리케이션 전체에서 처리되지 않은 모든 예외를 처리하는 예외 레이어가 내장되어 있다. 애플리케이션 코드에서 예외를 처리하지 않으면 이 계층에서 예외를 포착한 다음 자동으로 적절한 사용자 친화적인 응답을 보낸다.


001


기본적으로 이 작업은 HttpException 타입 및 해당 하위 클래스의 예외를 처리하는 내장 글로벌 예외 필터에 의해 수행된다. 예외가 인식되지 않는 경우 기본 제공 예외 필터는 다음과 같은 기본 JSON 응답을 생성한다.


{
  "statusCode": 500,
  "message": "Internal server error"
}


1. Throwing standard exceptions

Nest는 @nestjs/common 패키지에서 노출되는 내장 HttpException 클래스를 제공한다. 일반적인 HTTP REST/GraphQL API 기반 애플리케이션의 경우 특정 오류 조건이 발생할 때 표준 HTTP 응답 객체를 보내는 것이 가장 좋다.


예를 들어 CatsController에는 findAll() 메서드(GET 라우트 핸들러)가 있다. 이 라우트 핸들러가 어떤 이유로 예외를 던졌다고 가정해 보자. 이를 시연하기 위해 다음과 같이 하드코딩한다.


cats.controller.ts
@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}


Note

여기서는 @nestjs/common 패키지에서 가져온 헬퍼 열거형 HttpStatus를 사용했다.


클라이언트가 이 엔드포인트를 호출할 때 응답은 다음과 같다.


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


HttpException 생성자는 응답을 결정하는 두 개의 필수 인수를 사용한다:


  • response 인수는 JSON 응답 본문을 정의한다. 아래에 설명된 대로 string 또는 object일 수 있다.
  • status 인수는 HTTP 상태 코드를 정의한다.


기본적으로 JSON 응답 바디에는 두 가지 프로퍼티가 있다:


  • statusCode: status 인수에 제공된 HTTP 상태 코드
  • message: status에 따른 HTTP 오류에 대한 간단할 설명


JSON 응답 바디의 메시지 부분만 재정의하려면 response 인수에 문자열을 제공한다. 전체 JSON 응답 바디를 재정의하려면 response 인수에 객체를 전달한다. Nest는 객체를 직렬화하고 JSON 응답 바디로 반환한다.


두 번째 생성자 인수인 status는 유효한 HTTP 상태 코드여야 한다. 모범 사례는 @nestjs/common에서 가져온 HttpStatus 열거형을 사용하는 것이다.


다음은 전체 응답 바디를 재정의하는 예제이다.


cats.controller.ts
@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}


위의 예제를 사용하면 응답이 다음과 같이 표시된다.


{
  "status": 403,
  "error": "This is a custom message"
}


2. Custom exceptions

대부분의 경우 사용자 정의 예외를 작성할 필요가 없다. 만약 사용자 정의 예외를 만들어야 하는 경우 사용자 정의 예외가 기본 HttpException 클래스에서 상속되는 고유한 예외 계층을 만드는 것이 좋다. 이 접근 방식을 사용하면 Nest가 예외를 인식하고 자동으로 오류 응답을 처리한다. 이러한 사용자 정의 예외를 구현해 보자.


forbidden.exception.ts
export class ForbiddenException extends HttpException {
  constructor() {
    super("Forbidden", HttpStatus.FORBIDDEN);
  }
}


ForbiddenException은 기본 HttpException을 확장하므로 내장 예외 처리기와 원활하게 작동하며, 이 때문에 findAll() 메서드 내에서 사용할 수 있다.


cats.controller.ts
@Get()
async findAll() {
  throw new ForbiddenException();
}


3. Built-in HTTP exceptions

Nest는 기본 HttpException에서 상속되는 표준 예외를 제공한다. 이들은 @nestjs/common 패키지에서 노출되며 가장 일반적인 HTTP 예외를 나타낸다:


  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException


4. Exception filters

기본(내장) 예외 필터가 많은 경우를 자동으로 처리할 수 있지만 예외 레이어에 대한 전체 제어가 필요할 수 있다. 예를 들어, 일부 동적 요소를 기반으로 로깅을 추가하거나 다른 JSON 스키마를 사용하는 경우이다. 예외 필터는 정확히 이 목적을 위해 설계되었다. 이를 통해 정확한 제어 흐름과 클라이언트로 다시 전송되는 응답 내용을 제어할 수 있다.


HttpException 클래스의 인스턴스인 예외를 포착하고 이에 대한 사용자 정의 응답 로직을 구현하는 예외 필터를 만들어 보자. 이렇게 하려면 기본 플랫폼인 RequestResponse 객체에 액세스해야 한다. URL을 가져와 로깅 정보에 포함할 수 있도록 Request 객체에 액세스한다. Response.json() 메서드를 사용하여 전송되는 응답을 직접 제어하기 위해 Response 객체를 사용할 것이다.


http-exception.filter.ts
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,
    });
  }
}


Note

모든 예외 필터는 제네릭 ExceptionFilter<T> 인터페이스를 구현해야 한다. 이렇게 하려면 표시된 시그니처와 함께 catch(exception: T, host: ArgumentsHost) 메서드를 제공해야 한다. T는 예외 타입을 나타낸다.


@Catch(HtppException) 데코레이터는 필요한 메타데이터를 예외 필터에 바인딩하여 이 특정 필터가 HttpException 타입의 예외를 찾고 있다는 것을 Nest에 알린다. @Catch() 데코레이터는 단일 매개변수 또는 쉼표로 구분된 목록을 사용할 수 있다. 이를 통해 한 번에 여러 타입의 예외에 대한 필터를 설정할 수 있다.


5. Arguments host

catch() 메서드의 매개변수를 살펴보자. exceptoin 매개변수는 현재 처리 중인 예외 객체이다. host 매개변수는 ArgumentsHost 객체이다. ArgumentsHost는 강력한 유틸리티 객체이다. 이 코드 샘플에서는 이를 사용하여 원래 요청 핸들러로 전달되는 RequestResponse 객체에 대한 참조를 얻는다. 그리고 코드 샘플에서 ArgumentsHost에 대한 몇 가지 헬퍼 메서드를 사용하여 원하는 RequestResponse 객체를 가져왔다.


6. Binding filters

HttpExceptionFilterCatsControllercreate() 메서드에 연결해 보자.


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


여기서는 @UseFilters() 데코레이터를 사용했다. @Catch() 데코레이터와 유사하게 단일 필터 인스턴스 또는 쉼표로 구분된 필터 인스턴스 목록을 사용할 수 있다. 여기에서 HttpExceptionFilter의 인스턴스를 생성하여 연결했지만, 인스턴스 대신 클래스를 전달하여 프레임워크에 인스턴스화에 대한 책임을 맡기고 종속성 주입을 활성화할 수도 있다.


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


Note

가능하면 인스턴스 대신 클래스를 사용하여 필터를 적용하는 것이 좋다. Nest는 전체 모듈에서 동일한 클래스의 인스턴스를 쉽게 재사용할 수 있으므로 메모리 사용량을 줄일 수 있다.


위의 예제에서 HttpExceptionFilter는 단일 create() 라우트 핸들러에만 적용되어 메서드 범위가 된다. 예외 필터는 메서드 범위, 컨트롤러 범위, 글로벌 범위와 같은 다양한 레벨에서 범위를 지정할 수 있다. 예를 들어 다음과 같이 필터를 컨트롤러 범위로 설정할 수 있다.


cats.controller.ts
@UseFilters(new HttpExceptionFilter())
export class CatsController {}


이 구성은 CatsController 내부에 정의된 모든 라우터 핸들러에 대해 HttpExceptionFilter를 설정한다.


글로벌 범위 필터를 만들려면 다음과 같이 작성한다.


main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();


글로벌 범위 필터는 모든 컨트롤러와 모든 라우트 핸들러에 대해 전체 애플리케이션에서 사용된다. 종속성 주입의 관점에서, 모듈 외부에서 등록된 글로벌 필터는 종속성을 주입할 수 없다. 이는 이것이 모든 모듈의 컨텍스트 외부에서 수행되기 때문이다. 이 문제를 해결하기 위해 다음 구성을 사용하여 모든 모듈에서 직접 글로벌 범위 필터를 등록할 수 있다.


app.module.ts
import { Module } from "@nestjs/common";
import { APP_FILTER } from "@nestjs/core";

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


7. Catch everything

처리되지 않은 모든 예외를 예외 타입에 관계없이 포착하려면 @Catch() 데코레이터의 매개변수 목록을 비워 둔다.


아래 예제처럼 HTTP 어댑터를 사용하여 응답을 전달하고 플랫폼별 객체(RequestResponse)를 직접 사용하지 않으면 플랫폼에 구애받지 않는 코드가 된다.


import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from "@nestjs/common";
import { HttpAdapterHost } from "@nestjs/core";

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

  catch(exception: unknown, host: ArgumentsHost): void {
    // In certain situations `httpAdapter` might not be available in the
    // constructor method, thus we should resolve it here.
    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);
  }
}


8. Inheritance

일반적으로 애플리케이션 요구 사항을 충족하도록 제작된 사용자 정의 예외 필터를 만들 경우가 많을 것이다. 이런 경우 기본 제공되는 기본 글로벌 예외 필터를 단순히 확장하고 특정 요소에 따라 동작을 재정의하는 것이 편리하다.


예외 처리를 기본 필터에 위임하려면 BaseExceptionFilter를 확장하고 상속된 catch() 메서드를 호출해야 한다.


all-exceptions.filter.ts
import { Catch, ArgumentsHost } from "@nestjs/common";
import { BaseExceptionFilter } from "@nestjs/core";

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}


글로벌 필터는 기본 필터를 확장할 수 있다.


첫 번째 방법은 사용자 정의 글로벌 필터를 인스턴스화할 때 HttpAdapter 참조를 삽입하는 것이다.


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

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

  await app.listen(3000);
}
bootstrap();


두 번째 방법은 APP_FILTER 토큰을 사용하는 것이다.


References