NestJS 노트 (1) : Exception Filters

SimJungUk·2020년 12월 26일
4

앞서

이 글은 NestJS Exception Filter 공식 문서를 기반으로, 타이핑하며 배우는 의미로 쓰는 글입니다.

Exceptions Layer

Nest는 모든 unhandled exceptions 를 exceptions layer 에서 처리한다. 만약 우리의 코드에서 예외가 처리되지 않았다면, 이 layer 에서 걸려서, 자동으로 유저에게 친숙한 response 를 보내준다.

이것은 별도의 설치 없이 내장된 global exception filter 에 의해 이루어진다. 이는 HttpExecution 타입의 예외들을 처리해준다. 예외가 인식되지 않았을 때 자동으로

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

을 보내준다.

Custom Exception

대부분의 경우에서, custom exception을 쓸 필요가 없다. Nest 에 내장된 HTTP Exception 을 사용하면 된다. 만약 정말 필요하다면, HttpExcetion 클래스로부터 상속받는 것이 좋다.

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

ForbiddenExceptionHttpException을 상속하기 때문에, 내장 exception handler와 함께 원활하게 돌아가고, 그러므로

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

처럼 쓰일 수 있다.

Exception Filters

비록 내장된 exception filter 가 자동으로 많은 예외를 처리해주지만, exceptions layer를 완전히 제어하고 싶을 수 있다. 예를 들면, log를 추가해주고 싶다거나, 여러 동적인 요인을 기반으로 다른 JSON schema를 사용하고 싶을 수 있다. 이런 목적을 위해 고안한 것이 Exception filter 이다. 이는 Client에게 보내지는 응답의 내용이나 제어의 정확한 flow를 제어할 수 있게 한다.

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

먼저 @Catch(HttpException) 데코레이터는, 이 filter가 HttpException에 해당하는 예외가 있는지 찾는다. @Catch() 의 파라미터는 하나일 수도 있고, 콤마로 구분되어 여러 개를 한번에 검사할 수도 있다.

catch() 내의 두 파라미터를 살펴보자. exception 은 현재 처리되고 있는 exception object 이다. host 파라미터는 ArgumentHost 오브젝트이다. 위의 코드에서, 우리는 이것을 원래의 request handler에서 통과된 RequestResponse 오브젝트들의 레퍼런스를 가져오기 위해서 사용했다.

여기까지 읽고 ArgumentHost가 왜 사용된지는 알았지만, 이게 정확히 뭔지는 모르겠다. 원문에서도, 모든 곳에서 underlying arguments들을 가져올 수 있는 ArgumentsHost와 그의 기능들의 강력함을 간략히 설명하고 있다. 일단은 넘어가도록 하자.

Binding Filters

위에서 새로 만든 HttpExceptionFilter를 적용하는 방법은

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

이다. 역시 콤마를 이용하여 여러 가지의 exception filter 를 적용할 수 있다. 위의 코드는 new HttpExceptionFilter()로 인스턴스를 만들었다. 하지만 대신에, class를 instance 대신 넣으면, 인스턴스화의 책임을 회피하면서, 의존성 주입 을 가능하게 할 수 있다.

@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() 메소드는 게이트웨이나 하이브리드 어플리케이션에 적용되지 않는다고 한다.
하지만 이렇게 main.ts 에서 하는 것은 의존성 주입이 불가능해진다. module 의 context 내에 있지 않기 때문이다. 이 문제를 해결하기 위해, module 파일에서 적용하는 것을 권장한다고 한다.

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

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

Catch Everything

만약 모든 unhandled exception을 catch 하고 싶다면, 파라미터를 비운채 Catch() 데코레이터를 사용하면 된다.

Inheritance

일반적으로, 당신은 당신의 applicatoin requirements를 완전히 수행할 수 있는, 완전히 customize 된 exception filter를 만들 것이다. 그러나, 당신이 단순하게 내장된 global exception filter 를 extend 하고, 특정 상황에 따라 어떤 기능을 override 하고 싶을 수 있다.

base filter에 예외 처리를 위임하기 위해서, BaseExceptionFilter를 extend 한 뒤 상속된 catch() 메소드를 call해야 한다.

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

위는 단지 접근법을 시연한 것 뿐이다. 실제로는 당신의 business logic 에 맞추어서 실행되어야 할 것이다.

후기

이 포스팅을 쓰면서 Exception Filters 에 대하여 어느 정도 감을 잡을 수 있었다. 완전한 예외의 처리를 위해서 사용될 수 있을 것이다.

0개의 댓글