6. Exception filters

Nest 에는 애플리케이션 전체에서 처리되지 않은 모든 예외를 처리하는 내장 예외 레이어가 함께 제공된다. 애플리케이션 코드에서 예외가 처리되지 않으면 이 계층에서 이를 잡은 다음 사용자에게 친숙한 적절한 응답을 자동으로 보낸다.

 

기본적으로 이 작업은 HttpException 유형의 예외 (및 해당 하위 클래스) 를 처리하는 내장된 전역 예외 필터 에 의해 수행된다. HttpException예외가 인식되지않으면(HttpException  에서 상속되는 클래스도 아니고) 내장된 예외 필터는 다음과 같은 기본 JSON 응답을 생성한다.

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

힌트) 전역 예외 필터는 http-errors라이브러리를 부분적으로 지원한다. 기본적으로 및 속성을 포함하는 모든 던져진 예외는 statusCode 와 message 가 적절하게 채워지고 응답으로 다시 전송된다. (인식할 수 없는 예외에 대한 기본값 InternalServerErrorException 대신)

 

표준 예외 던지기

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

예를 들어 CatsController 에는 findAll() 메소드( GET 라우트 핸들러) 가 있다고 하자. 이 경로 핸들러가 어떤 이유로 예외를 발생시킨다고 가정해보자. 이를 설명하기 위해 다음과 같이 하드 코딩 하였다.

// cats.controller.ts

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

힌트) 우리는 HttpStatus여기를 이용했다. 이는 @nestjs/common 패키지에서 가져온 헬퍼 열거형이다.

 

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

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

HttpException  생성자는 응답을 결정하는 두 가지 필수 인수를 사용한다.

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

기본적으로 JSON 응답 본문에는 다음 두 가지 속성이 포함되어 있다.

  • statusCodestatus: 기본값은 인수 에 제공된 HTTP status코드입니다.
  • message: status 에 근거한 HTTP 오류에 대한 간단한 설명이다.

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

 

두 번째 생성자 인수 - status-는 유효한 HTTP 상태 코드여야 합니다. 가장 좋은 방법은 @nestjs/common 에서 가져온 HttpStatus 열거형을 사용하는 것이다.

오류 원인 cause 제공하는 데 사용할 수 있는 세 번째 생성자 인수 options (선택 사항)가 있다.  cause 개체는 응답 개체로 직렬화되지 않지만 로깅 목적으로 유용할 수 있으며 HttpException 을 발생시킨 내부 오류에 대한 귀중한 정보를 제공한다.

 

다음은 전체 응답 본문을 재정의하고 오류 원인을 제공하는 예이다.

// cats.controller.ts

@Get()
async findAll() {
  try {
    await this.service.findAll()
  } catch (error) { 
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom message',
    }, HttpStatus.FORBIDDEN, {
      cause: error
    });
  }
}

위의 내용을 사용하면 응답이 다음과 같이 표시된다.

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

 

사용자 정의 예외

대부분의 경우 맞춤 예외를 작성할 필요가 없으며 다음 섹션에 설명된 대로 내장된 Nest HTTP 예외를 사용할 수 있다. 사용자 정의 예외를 생성해야 하는 경우 사용자 정의 예외가 기본 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();
}

 

내장 HTTP 예외

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

  1. BadRequestException
  2. UnauthorizedException
  3. NotFoundException
  4. ForbiddenException
  5. NotAcceptableException
  6. RequestTimeoutException
  7. ConflictException
  8. GoneException
  9. HttpVersionNotSupportedException
  10. PayloadTooLargeException
  11. UnsupportedMediaTypeException
  12. UnprocessableEntityException
  13. InternalServerErrorException
  14. NotImplementedException
  15. ImATeapotException
  16. MethodNotAllowedException
  17. BadGatewayException
  18. ServiceUnavailableException
  19. GatewayTimeoutException
  20. PreconditionFailedException

모든 내장 예외는 cause 파라미터를 사용하여 오류를, option 파라미터를 통해 오류 설명을 모두 제공 할 수 있다.

throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })

위 예외의 응답 결과는 아래와 같다.

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

 

예외 필터

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

HttpException  클래스의 인스턴스인 예외를 포착하고 이에 대한 사용자 정의 응답 논리를 구현하는 예외 필터를 만들어 보자. 이렇게 하려면 Request 와  Response 개체에 액세스해야 한다 . Request 원본을 꺼내서 url 을 로깅 정보에 포함할 수 있도록 Response 개체에 액세스 해야한다. 다음, 메소드 response.json() 를 통해 전송된 응답을 직접 제어하기 위해 객체를 사용할 것이다.

// http-excepiton.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,
      });
  }
}

힌트) 모든 예외 필터는 일반 ExceptionFilter<T>인터페이스를 구현해야 합니다. 이를 위해서는 catch(exception: T, host: ArgumentsHost) 메소드에 표시된 서명을 제공해야 한다 . T 는 예외 유형을 나타냅니다.

 

주의) @nestjs/platform-fastify 를 사용하는 경우 response.json()  대신  response.send()  사용할 수 있다. 

 

아규먼트 호스트

catch() 메소드의 매개변수를 살펴보자 . exception 파라미터는 현재 처리 중인 예외 개체이고,  host 파라미터ArgumentsHost 객체이다 .  실행 컨텍스트 장에서 더 자세히 살펴볼 강력한 유틸리티 개체이다 . 이 코드 샘플에서는 이를 사용하여 원래 요청 처리기(컨트롤러에서 예외가 발생하고 있음)로 전달되는 Request  Response 개체 에 대한 참조를 얻는다 . 이 코드 샘플에서는 원하는 RequestResponse 개체를 가져오기 위해  가지 헬퍼 메서드를 사용했다. ArgumentsHost 에 대한 자세한 논의는 다른 장에서 하겠다.

*이러한 추상화 수준이 필요한 이유는 모든 컨텍스트(예: 현재 작업 중인 HTTP 서버 컨텍스트는 물론 마이크로서비스 및 WebSocket도 포함)에서 ArgumentsHost 기능을 작동시키기 위함이다. 실행 컨텍스트 챕터에서는 ArgumentsHost 의 기능과 헬퍼 함수를 사용하여 어떤 실행 컨텍스트에서라도 적절한 기본 인수에 액세스할 수 있는 방법을 살펴보겠다 . 이를 통해 모든 컨텍스트에서 작동하는 일반 예외 필터를 작성할 수 있다.

 

바인딩 필터

HttpExceptionFilterCatsControllercreate의 create() 메소드에 연결해 보자.

//cats.controller.ts

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

힌트) @UseFilters() 데코레이터는 @nestjs/common 패키지에서 가져옵니다.

 

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

 

힌트) 가능하면 인스턴스 대신 클래스를 사용하여 필터를 적용하는 것이 좋다. 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();

주의) 이 useGlobalFilters() 방법은 게이트웨이 또는 하이브리드 애플리케이션에 대한 필터를 설정하지 않습니다.

 

전역 범위 필터는 전체 애플리케이션, 모든 컨트롤러 및 모든 라우트 핸들러에 사용된다. 종속성 주입 측면에서, 모든 모듈 외부에서 등록된 전역 필터( useGlobalFilters()위의 예 참조)는 모든 모듈의 컨텍스트 외부에서 수행되므로 종속성을 주입할 수 없다. 이 문제를 해결하려면 다음 구성을 사용하여 모든 모듈에서 직접 전역 범위 필터를 등록할 수 있다.

// app.module.ts

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

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

힌트) 이 접근 방식을 사용하여 필터에 대한 종속성 주입을 수행하는 경우 이 구성이 사용되는 모듈에 관계없이 필터는 실제로 전역적이라는 점에 유의해야한다. 이 작업은 필터(위 예에서  HttpExceptionFilter)가 정의된 모듈에서 해야한다. 또한 useClass 가 사용자 지정 공급자 등록을 처리하는 유일한 방법은 아닙니다. 다른 장을 참고하자.

 

이 기술을 사용하면 필요한 만큼 필터를 추가할 수 있다. 단순히 공급자 배열에 각각을 추가하면 된다.

 

모든 예외 잡기(Catch everything)

처리되지 않은 모든 예외를 포착하려면 (예외 유형에 관계없이) @Catch( )데코레이터의 매개변수 목록을 비워 두십시오(예: @Catch()).

아래 예에는 응답을 전달하기 위해 HTTP 어댑터를 사용하고 플랫폼별 객체( Request및 Response)를 직접 사용하지 않기 때문에 플랫폼에 구애받지 않는다.

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);
  }
}

주의) 모든 것을 포착하는 예외 필터와 특정 유형에 바인딩된 필터를 결합할 때 특정 필터가 바인딩된 유형을 올바르게 처리할 수 있도록 "Catch everything" 필터를 먼저 선언해야 합니다.

 

상속

일반적으로 애플리케이션 요구 사항을 충족하도록 제작된 완전히 사용자 정의된 예외 필터를 만든다. 그러나 내장된 기본 전역 예외 필터를 단순히 확장하고 특정 요인에 따라 동작을 재정의하려는 경우가 있을 수 있다.

 

예외 처리를 기본 필터에 위임하려면 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 를 확장하는 메서드 범위 및 컨트롤러 범위 필터는 new 로 인스턴스화하면 안된다. 대신 프레임워크가 자동으로 인스턴스화하도록 한다.

 

위의 구현은 접근 방식을 보여주는 쉘일 뿐입니다. 통상 확장된 예외 필터의 구현에는 맞춤형 비즈니스 로직(예: 다양한 조건 처리)이 포함된다.

전역 필터는 기본 필터를 확장 할 수 있다 . 이는 두 가지 방법 중 하나로 수행할 수 있다.

첫 번째 방법은 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 토큰을 사용하는 것이다 .

'Backend(Framework) > NestJS 개요(공식문서 번역)' 카테고리의 다른 글

8. Guards  (0) 2023.11.19
7. Pipes  (0) 2023.11.19
5. Middleware  (1) 2023.11.16
4. Modules  (1) 2023.11.16
3. Provider  (1) 2023.11.16
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유