Exception Filters

이연중·2021년 7월 27일
0

NestJS

목록 보기
6/22
post-custom-banner

Nest에는 어플리케이션 전체에서 처리되지 않은 모든 예외를 처리하는 Exception Layer가 내장되어 있다.

어플리케이션 코드에서 예외를 처리하지 않으면, 해당 Layer에서 예외를 포착해 사용자에게 자동으로 응답을 보낸다.

기본적으로 이 작업은 HttpException 유형의 예외를 처리하는 내장된 Global Exception Filter에 의해 수행된다.

예외가 인식되지 않는 경우, 내장된 Exception Filter는 다음과 같은 JSON 응답을 생성한다.

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

Throwing Standard Exceptions


Nest는 @nestjs/common 패키지에서 export된 내장 HttpException 클래스를 제공한다.

일반적인 HTTP REST/GraphQL API 기반 어플리케이션의 경우 특정 오류 조건이 발생할 때 표준 HTTP 응답 객체를 보내는 것이 좋다.

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

위 요청에 대한 응답은 다음과 같다.

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

HttpException 생성자는 두개의 매개변수를 가진다.

  1. response 인수: JSON 응답 본문 정의. string, object 형식
  2. status 인수: HTTP 상태코드 정의. @nestjs/common에서 가져온 HttpStatus 열거형을 사용하는 것이 좋다.

응답 본문에는 두가지의 속성이 있다.

  1. statusCode: status 인수에 제공된 HTTP 상태코드가 기본값
  2. message: status에 따른 HTTP 오류에 대한 간단한 설명

JSON 응답 본문에 메시지 부분만 재정의하려면 response 인수에 문자열을 전달하면 된다.

전체 JSON 응답 본문을 재정의 하려면 response 인수에 객체를 전달하면 된다. Nest는 객체를 직렬화하고 JSON 응답 본문으로 반환한다.

다음은 전체 응답 본문을 재정의하는 예이다.

@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


Custom Exceptions는 기본 HttpException 클래스에서 상속받아 작성하는 것이 좋다.

해당 접근 방식을 이용하면, Nest가 예외를 인식하고 자동으로 오류에 대한 응답을 처리한다.

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

이렇게 HttpException을 상속받아 작성하게 되면, 내장된 Exception Handler와 원활하게 작동하기에 findAll() 메서드 내에서도 사용할 수 있게 된다.

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

Built-in HTTP Exceptions


Nest는 기본 HttpException에서 상속되는 Standard Exception 집합을 제공한다.

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

Exception Filters


내장 Exception Filter가 자동으로 많은 경우를 처리하지만, Custom하게(Custom하게 응답을 구성하고 싶은 경우) Exception Layer에 대한 제어를 할 수도 있다.

HttpException 클래스의 인스턴스인 예외를 포착하고 이에 대해 Custom하게 로직을 구현하는 예외 필터를 만들어보겠다.

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 인터페이스를 catch(exception: T, host: ArgumentsHost) 메서드를 작성하여 implement 해야한다.

@Catch(HttpException) 데코레이터는 필요한 메터데이터를 예외 필터에 바인딩하여 해당 필터가 HttpException 타입의 예외만 찾고 있다는 것을 Nest에 알려준다.(HttpException은 나한테 넘겨!)

@Catch() 데코레이터 안에는 단일 매개변수 또는 쉼표로 구분된 목록을 기입할 수 있어 한번에 여러 타입의 예외 필터를 설정할 수도 있다.

exception이라는 매개변수는 현재 처리되고 있는 exception object이다.

host라는 매개변수는 ArgumentHost의 객체인데, request handler에서 통과된 RequestResponse 객체를 참조하기 위해 사용한다.

Binding Filters


새로운 HttpException FilterCatsControllercreate() 메서드에 연결해보겠다.

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

여기서는 @Usefilters() 데코레이터를 사용했다. 마찬가지로 단일 인스턴스 또는 쉼표로 구분된 필터 인스턴스 목록을 사용할 수 있다.

또한, 여기서 HttpExceptinFilter의 인스턴스를 생성했다. 이 대신에 다음과 같이 클래스를 전달하여 인스턴스의 관리를 프레임워크에 넘기면서, DI를 수행할 수 있다.

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

전체 모듈에서 동일한 클래스의 인스턴스를 쉽게 재사용할 수 있으므로 메모리 사용량을 줄이기에 웬만하면 인스턴스를 새로 생성하는 대신 클래스를 전달하는 것이 좋다.

위처럼 메서드 범위에 예외 필터를 적용할 수도 있고, 컨트롤러, 전역 범위 등 다양한 수준으로 범위를 지정할 수 있다.

다음은 컨트롤러 범위로 설정한 경우이다.

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

다음은 전역 범위로 설정한 경우이다.

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

전역 범위 필터는 전체 어플리케이션에서 사용된다. useGlobalFilters() 메소드는 게이트웨이나 하이브리드 어플리케이션에 적용되지 않는다

DI의 관점에서 볼 때, 전역 범위 필터는 모듈 외부에서 등록되었기에 해당 필터에 대한 DI를 수행할 수 없다. module이 context 내에 있지 않기 때문이다.

이를 해결하기 위해 다음과 같이 App모듈에서 직접 등록해주면 된다.

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

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

Catch Everything


처리되지 않은 모든 예외를 처리하기 위해서는 @Catch() 데코레이터의 매개변수 목록을 비워두면 된다.

Inheritance


내장된 Global Exception Filter를 확장, 재정의 하기 위해서는 BaseExceptionFilter를 상속받고 catch() 메서드를 호출하면 된다.

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

참고

https://docs.nestjs.kr/exception-filters

profile
Always's Archives
post-custom-banner

0개의 댓글