NESTJS를 배워보자(7) - Exception filters

yoon·2023년 7월 18일
0

NESTJS를 배워보자

목록 보기
7/21
post-thumbnail

Exception filters

nest의 공식문서를 토대로 작성합니다.

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

기본적으로 이 작업은 내장된 전역 예외 필터에 의해 수행되며 이 필터는 HttpException 유형(및 그 하위 클래스)의 예외를 처리합니다. 예외가 인식되지 않는 경우 기본 제공 예외 필터는 다음과 같은 기본 JSON 응답을 생성합니다.

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

HINT
전역 예외 필터는 부분적으로 http-error 라이브러리를 지원함. 기본적으로 statusCode 및 메시지 속성을 포함하는 모든 예외가 올바르게 채워지고 응답으로 다시 전송됨.

Throwing standard exceptions

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

예를 들어 CatsController에는 findAll() 메소드가 있습니다. 이 route handler가 모종의 이유로 예외를 던진다고 가정합시다.
이를 위한 하드코딩:

# cats.controller.ts

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

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

클라이언트가 이 엔드포인트를 요청하면 응답은 이렇게 될 것입니다.

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

HttpException 생성자는 응답을 결정하는 두 개의 필수 인수를 받습니다.

  • response : JSON 응답 body를 정의. 문자열 또는 객체.
  • status : HTTP 상태 코드를 정의.

기본적으로 JSON 응답 body는 두 개의 속성을 가집니다.

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

JSON 응답 body의 message 부분만 재정의하려면 응답 인수에 문자열을 입력합니다. 전체 JSON 응답 body를 재정의하려면 응답 인수에 객체를 전달합니다. Nest는 객체를 직렬화하여 JSON 응답 body로 반환합니다.

두 번째 생성자 인자인 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"
}

Custom exceptions

많은 경우 사용자 정의 예외를 작성할 필요가 없으며 다음 섹션에 설명된 대로 기본 제공 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();
}

그러니까 되도록 내장된 전역 예외 필터를 사용하고 사용자 정의 예외를 적용하려면 HttpException을 확장해서 만들어라!인 것 같습니다.

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

모든 기본 제공 예외는 options를 사용하여 오류 cause와 오류 설명을 모두 제공할 수 있습니다.

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

이는 다음 응답을 얻을 수 있습니다:

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

Exception filters

기본 예외 필터가 많은 경우를 자동으로 처리할 수 있지만 예외 계층을 완전히 제어하고 싶을 수도 있습니다. 예를 들어 로깅을 추가하거나 일부 동적 요인에 따라 다른 JSON 스키마를 사용하고 싶을 수도 있습니다. 예외 필터는 바로 이러한 목적을 위해 설계되었습니다. 예외 필터를 사용하면 정확한 제어 흐름과 클라이언트에 다시 전송되는 응답의 내용을 제어할 수 있습니다.

HttpException 클래스의 인스턴스인 예외를 포착하고 이에 대한 사용자 정의 응답 로직을 구현하는 예외 필터를 만들어 봅시다. 이를 위해 Request, Response 객체에 접근해야 합니다.
Request 객체에 접근해 원본 url을 가져와 로깅 정보에 포함할 수 있습니다.
Response 객체에 접근해 response.json() 메소드를 사용하여 전송된 응답을 직접 제어할 수 있습니다.

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

HINT
모든 예외 필터는 일반 ExceptionFilter <T> 인터페이스를 구현해야 함. 이를 위해 catch 메소드에 지정된 signature를 제공해아 함. T는 예외의 유형을 나타냄.

WARNING
fastify를 사용하는 경우 response.json() 대신 response.send()를 사용함.

@Catch(HttpException) 데코레이터는 필요한 메타데이터를 예외 필터에 바인딩하여 이 특정 필터가 HttpException 유형의 예외만 찾고 있음을 Nest에 알려줍니다.
@Catch() 데코레이터는 단일 매개변수 또는 쉼표로 구분된 목록을 받습니다. 이를 통해 한 번에 여러 유형의 예외에 대한 필터를 설정할 수 있습니다.

Arguments host

catch() 메소드의 매개변수를 봅시다. exception 매개변수는 현재 처리 중인 예외 객체입니다. host 매개변수는 ArgumentsHost 객체입니다. ArgumentsHost는 execution context 챕터에서 자세히 살펴볼 강력한 유틸리티 객체입니다. 위 코드 샘플에서는 이 객체를 사용하여 예외가 발생한 컨트롤러에서 원래 요청 handler로 전달되는 RequestResponse 객체에 대한 참조를 얻습니다.
위 코드 샘플에서는 ArgumentsHost의 몇 가지 헬퍼 메소드를 사용하여 원하는 RequestResponse 객체를 가져왔습니다.
ArgumentsHost에 대해 자세히 보려면 여기로

간단히 말하면 ArgumentsHost 객체인 hostRequest, Response에 접근한다 입니다.

Binding filters

새로운 HttpExceptionFilterCatsControllercreate() 메소드에 연결합니다.

# cats.controller.ts

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

HINT
@UseFilters() 데코레이터는 @nestjs/common 패키지에서 import.

여기서는 @UseFilters() 데코레이터를 사용했습니다. @Catch() 데코레이터와 마찬가지로 단일 필터 인스턴스 또는 쉼표로 구분된 필터 인스턴스 목록을 받을 수 있습니다. 위에서는 HttpExceptionFilter 인스턴스를 생성했습니다. 또는 아래처럼 인스턴스 대신 클래스를 전달하여 인스턴스화에 대한 책임을 프레임워크에 맡기고 의존성 주입을 활성화할 수 있습니다.

# cats.controller.ts

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

해당 엔드포인트를 호출하면

이렇게 나오는 것을 볼 수 있습니다.

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

위 예제에서 HttpExceptionFilter는 단일 create() route handler에만 적용되어 메소드 범위가 지정됩니다. 예외 필터는 메소드 범위, 컨트롤러 범위, 또는 전역 범위 등 다양한 범위를 지정할 수 있습니다. 예를 들어 컨트롤러 범위로 필터를 설정하려면:

# cats.controller.ts

@UseFilters(new HttpExceptionFilter())
export class CatsController {}

이 구조는 CatsController 내부에 정의된 모든 route handler에 대해 HttpExceptionFilter를 설정합니다.

전역 범위로 필터를 설정하려면:

# main.ts

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

WARNING
useGlobalFilters() 메소드는 게이트웨이 또는 하이브리드 애플리케이션에 대한 필터를 설정하지 않음.

전역 범위 필터는 모든 컨트롤러와 모든 route handler, 전체 애플리케이션에서 사용됩니다. 종속성 주입과 관련하여 모듈 외부에서 등록한 전역 필터는 모듈 컨텍스트의 외부에서 수행되므로 종속성을 주입할 수 없습니다. 이 문제를 해결하기 위해 다음 구성을 사용하여 모든 모듈에서 직접 전역 범위 필터를 등록할 수 있습니다.

# app.module.ts

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

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

HINT
이 방식을 사용하여 필터에 대한 종속성 주입을 수행할 때는 이 구조가 사용되는 모듈에 관계없이 필터가 실제로는 전역임. 이 작업은 필터가 정의된 모듈을 선택. 또한 useClass만이 사용자 지정 provider 등록을 처리하는 유일한 방법은 아님. 자세히 보려면 여기로

이 방법을 사용하여 필요한 만큼 필터를 추가할 수 있으며 각 필터를 providers 배열에 추가하면 됩니다.

Catch everything

처리되지 않은 모든 예외를 잡으려면(예외 유형에 관계없이) @Catch() 데코레이터의 매개변수 목록을 비우면 됩니다.

아래의 예는 HTTP 어댑터를 사용하여 응답을 전달하고 플랫폼 별 객체를 직접 사용하지 않기 때문에 플랫폼에 구애받지 않는 코드입니다.

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

WARNING
모든 것을 캐치하는 예외 필터와 특정 유형에 바인딩된 필터를 결합하는 경우 특정 필터가 바인딩된 유형을 올바르게 처리할 수 있도록 모든 것을 캐치하는 필터를 먼저 선언해야 함.

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

WARNING
메소드 범위 및 컨트롤러 범위 필터는 BaseExceptionFilter를 확장하는 필터를 새로 인스턴스화해서는 안 됨. 대신 프레임워크가 자동으로 인스턴스화하도록 둬야 함.

위의 구현은 접근 방식을 보여주는 것일 뿐입니다. 확장 예외 필터의 구현에는 맞춤형 비즈니스 로직이 포함될 수 있습니다.

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

첫 번째 방법은 사용자 지정 전역 필터를 인스턴스화할 때 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 토큰을 사용하는 것입니다. 자세히 보려면 여기로

제 코드입니다. <> 😎
저도 그냥 막 이것 저것 적용해본 거라 깔끔하게 정리되진 않았습니다.

고생하셨습니다!
다음 글에서 만나요~~😀


저도 아직 배우는 단계입니다. 지적 감사히 받겠습니다. 함께 열심히 공부해요!!

profile
백엔드 개발자 지망생

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

너무 좋은 글이네요. 공유해주셔서 감사합니다.

답글 달기