NestJS Logo

원문을 읽으며 정리합니다.

Table of Contents

  1. Exception filters

Exception filters

NestJS는 애플리케이션에서 발생한 처리되지 않은 모든 예외를 processing 동안 책임지는 내장된 execptions layer를 가지고 있다. 코드에서 예외를 처리하지 않을때, 이 예외는 바로 excption 계층에서 catch 된다. 그런 다음 자동으로 적절하게 사용자 친화적인 response를 보낸다.

filter

Image by: https://docs.nestjs.com/assets/Filter_1.png

기본적으로, 이러한 액션은 내장된 global execption filter에 의해 수행된다. 이 filter는 HttpExecption 타입과 이의 서브클래스의 타입의 예외를 처리한다. HttpException 또는 이를 상속한 클래스가 아닌 unrecognized(인식받지 못하는) 예외가 발생했을때, 내장된 예외 필터는 일반적으로 아래와 같은 JSON response를 따른다.

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

Throwing standard exceptions

NestJS는 내장된 HttpException 클래스를 제공한다(@nestjs/common 패키지 로부터). 전형적인 HTTP REST/GraphQL API 기반의 애플리케이션에서, 특정 오류 조건이 발생할때 표준 HTTP response 객체를 보내는 것이 모범사례다.

예를들어, CatsControllerGET 라우트의 findAll() 핸들러가 있다. 아래와 같이 하드코딩 하여 예외를 throw 하도록 해보자.

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

client가 해당 엔드포인트에 요청을 하면 다음과 같은 응답을 볼 것이다.

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

HttpException 생성자는 응답을 결정짓는 2개의 인자를 받는다.

  • response: JSON response body를 정의한다. string 또는 object가 될 수 있다.
  • status: HTTP status code를 정의한다.

디폴트로, JSON response body는 2가지 속성을 포함한다.

  • statusCode: status 인자로 제공된 HTTP status code가 디폴트다.
  • message: status에 기반된 HTTP 에러의 짧은 설명이다.

response 인자로 제공된 문자열으로 JSON response body의 message 속성을 오버라이드 한다. response 인자로 전달된 object가 전체의 JSON response body를 오버라이드 한다. NestJS는 객체를 직렬화 하고 JSON response body로 반환할 것이다.

두번째 생성자 인자는 status이다. 이는 유효한 HTTP status code여야 한다. 모범 사례는 @nestjs/common 패키지로부터 import된 HttpStatus enum을 사용하는 것이다.

다음은 전체의 response body를 오버라이드 하는 예제이다.

// 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"
}

Custom exceptions

많은 경우에, 커스텀 예외를 작성할 필요가 없을 것이고, 아래 예제처럼 내장된 NestJS HTTP exception만 사용할 것이다. 만약 커스텀 예외를 만들 필요가 있다면, HttpException 을 상속한 커스텀 예외에 개인의 exceptions hierarchy를 생성하는 것은 좋은 사례다. 이와 같이 적절하게 한다면, NestJS는 너의 예외를 인식할 것이다. 그리고 자동으로 error response를 다룬다. 아래와 같이 커스텀 예외를 구현해 보자.

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

ForbiddenExceptionHttpException을 상속했기 때문에, 이는 내장된 예외 핸들러에서 잘 작동할 것이다. 그리고, 우리는 findAll() 메서드 안에서 throw 할 수 있다.

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

Built-in HTTP exceptions

NestJS는 HttpException 을 상속한 표준 예외의 집합을 제공한다. 이들은 @nestjs/common 패키지에서 찾을 수 있다. 그리고 대부분의 흔한 HTTP 예외를 포함한다.

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

Exception filters

기본 예외 filter가 자동으로 많은 경우를 처리할 수 있는 동안, exceptions layer를 넘어 full control을 원할 지도 모른다. 예를들어, logging을 추가하거나 동적 factors에 기반한 다른 JSON schema를 사용해야 할지도 모른다. Exception filters는 정확히 이러한 목적을 위해 설계되었다. 이들은 client로 반환되는 응답의 내용과 제어의 흐름을 정확하게 제어할 수 있다.

HttpException의 인스턴스인 예외를 catch하는 걸 책임지는 Exception filter를 만들어보자. 그리고 그 안에 커스텀 response logic을 구현하자. 이렇게 하기 위해선, 우리는 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,
    });
  }
}

모든 exception filter는 ExceptionFilter<T> 인터페이스의 제네릭을 구현해야 한다. 이렇게 사용하려면 catch(exception: T, host: ArgumentsHost) 메서드에 표시된 signature를 제공해야 한다.

@Catch(HttpException) 데코레이터는 exception filter에 필요한 메타데이터를 바인딩 하고 NestJS에게 이 필터는 HttpException 타입만의 예외를 찾는다고 말한다. @Catch() 데코레이터는 파라미터 한개를 가질수 도 있고, 콤마로 분리된 리스트를 가질수 도 있다. 이는 한번에 여러 타입의 예외를 필터에 세팅하는 것이다.

Arguments host

catch() 메서드의 파라미터를 보자. exception 파라미터는 현재 진행중인 예외 객체다. host 파라미터는 ArgumentsHost 객체다. execution context chapter에서 나중에 살펴볼 ArgumentsHost는 강력한 utility 객체다. 여기 샘플 코드에선, 원래의 request handler에서(예외가 유발된 컨트롤러) 전달된 RequestResponse 객체의 참조를 얻기 위해 사용했다. 이 샘플 코드에선, RequestResponse 객체를 얻기 위해 ArgumentsHost의 헬퍼 메서드를 사용했다. ArgumentsHost에 대한 자세한 내용은 이 링크를 참고하자.

이 레벨에서 추상화의 이유는 ArgumentsHost 함수들은 모든 contexts에 있다(HTTP server context, Microservices, WebSockets 등). 실행 컨텍스트 챕터 안에서, ArgumentsHost의 강력함과 헬퍼 functions를 위한 근본적인 인자들을 적절하게 접근할 수 있게하는 방법을 보여줄 것이다. 이는 모든 contexts를 가로질러 제네릭 예외 필터를 작성할 수 있게 우리를 허락할 것이다.

Binding filters

우리의 새로운 HttpExceptionFilterCatsControllercreate() 메서드에 묶어보자.

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

@UseFilters() 데코레이터는 @nestjs/common 패키지에 있다.

여기서 @UseFilters() 데코레이터를 사용했다. 이는 @Catch() 데코레이터와 비슷해 보인다. 이는 단일 필터 인스턴스를 가지거나, 필터 인스턴스들의 리스트를 가진다. 여기서, 우리는 HttpExceptionFilter의 인스턴스를 생성했다. 그렇지 않으면, 인스턴스 대신 클래스를 전달할 지도 모른다. 남은 인스턴스화에 대한 책임은 프레임워크에게 있고, DI로 가능하다.

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

가능하다면 인스턴스 대신 클래스를 사용해 필터에 적용하는걸 선호한다. 이는 memory usage를 감소시킨다. NestJS가 너의 전체의 모듈을 가로질러 같은 클래스의 인스턴스를 재사용 한다.

위의 예제와 같이, HttpExceptionFilter는 오직 create() 라우트 핸들러에 적용되었다(method-scoped). Exception Filters는 다른 단계로 스코프될 수 있다(method, controller, scoped, global). 예를 들어, controller-scoped를 필터로 세팅한다면 아래와 같이 해야한다.

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

이 생성은 CatsController 안에서 모든 라우트 핸들러에 HttpExceptionFilter를 설정한 거다.

global-scoped 필터를 만들기 위해 다음과 같이 할 수 있다.

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

useGlobalFilters() 메서드는 gateway 또는 hybrid 애플리케이션을 위한 필터를 설정하지 않는다.

global-scoped 필터는 모든 컨트롤러와 모든 라우트 핸들러를 위해 전체 애플리케이션을 가로질러 사용된다. 의존성 주입의 조건으로, 위 예제에서 useGlobalFilters()와 같이 모듈 밖에서 등록된 global filter는 모듈의 context 밖에 있기 때문에 의존성 주입을 할 수 없다. 이러한 문제를 해결하기 위해, 다음과 같은 방법을 이용해서 global-scoped 필터를 모듈에 직접 등록할 수 있다.

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

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

이렇게 필터에 DI를 수행하기 위해 이런 접근을 사용할때, 모듈이 어디서 사용되는지 개의치않고 필터가 사실상 global이다. 이것은 어디서 수행되어야 하나? 필터(위의 예에선 HttpExceptionFilter)가 정의된 모듈을 선택해라. 또한, useClass만이 커스텀 프로바이더를 등록하는 유일한 방법이 아니다. 이 링크에서 더 알아보자.

너는 필요한 만큼 이 기술과 함께 필터를 추가할 수 있다. 간단하게 각 providers에 배열로 추가해라.

Catch everything

모든 처리되지 않은 예외를 catch 하기 위해선(예외 타입에 상관없이), @Catch() 데코레이터의 파라미터를 빈채로 남겨라.

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

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

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

위 예제에서 필터는 던져진 각 예외를 잡을 것이다(class에 개의치 않고).

Inheritance

일반적으로, 너는 너의 애플리케이션의 요구사항에 충족하는 커스텀 예외 필터를 생성할 것이다. 그러나, 내장된 디폴트 global exception filter를 상속하고, 특정 행동을 오버라이드 하기를 원할지도 모른다.

예외 처리를 기본 필터에 위임하려면, 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);
  }
}

BaseExceptionFilter를 상속한 Method-scoped와 Controller-scoped 필터는 new 키워드로 인스턴스화 되어서는 안된다. 대신에, 프레임워크가 그들을 자동으로 인스턴스화 할 것이다.

위 구현은 단지 접근법을 보여준다. 너의 상속된 예외 필터의 구현은 맞춤 비지니스 로직(다양한 조건 처리)이 포함될 수 있다.

Global filter는 base filter를 상속할 수 있다. 이는 두가지 방법으로 수행될 수 있다.

첫 번째 방법은 커스텀 글로벌 필터가 인스턴스화될때 HttpServer 참조를 주입하는 것이다.

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 토큰을 사용하는 것이다.

Next NestJS OVERVIEW(7) - Pipe
Intro NestJS OVERVIEW(0) - Intro