본문 바로가기

Backend/NestJS

NestJS - 대충 서비스 만들어보기 (13)

이번 포스팅에선 NestJS 에서 제공하는 예외 필터 기능을 사용해보겠습니다.

 

- 예외 (Exception)

 

예외 처리는 무언가를 개발하는 과정에선 필수 사항인 것 같습니다. 개발자는 항상 에러가 발생할 수 있는 상황을 대비해야하고, 그에 따라

빠르게 에러를 전파하던지 또는 아예 무시되도록 하던지 하는 방법을 선택할 수도 있죠.

 

예전에 Express 로 처음 개발할 땐 에러 처리를 controller 부분에서 필수 파라미터가 undefined 일 땐 res.json(400)... 를,
그 이후 로직에는 전체에 try - catch 를 걸어서 뭔 에러던 다 catch 에서 잡고 500 으로 처리했었습니다.

이러다보니 코드 전체에 try - catch 및 res.json.. 이 난무했고, 보기엔 상당히 지저분했죠. 이런 것도 AOP 측면에서 코드를 좀 더 깔끔히

만들 수 있을텐데, 그런 내용도 이번에 약간이나마 넣어보도록 하겠습니다.

 

- 예외 처리

 

NestJS 는 프레임워크 내에 예외 레이어를 별도로 두고 있습니다. 이 레이어에선 어플리케이션에서 처리하지 못한 예외를 처리하는 역할을

합니다. 별다른 처리를 하지 않았을 때 이 레이어에서 예외를 어떻게 처리하는지 먼저 보도록 하겠습니다.

 

...

@Controller('ads')
export class AdsController {
  ...

  @Get(':id')
  fidOne(@Param('id') id: number) {
    return this.adsService.findOne(id);
  }
}

 

기존 코드의 AdsController 에서 /ads/:id 요청을 처리하던 부분에 ParseIntPipe 만 제거했습니다.

id 에 의도적으로 string 을 넣어서 이 에러가 어떻게 처리되는지를 보기 위함입니다.

원래는 ParseIntPipe 에서 파라미터를 validation 할 때 실패로 떨어질테지만요.

 

 

number 가 전달되어야 할 파라미터에 string 을 전달했을 때 NestJS 가 처리하는 내용입니다.

예외 레이어가 별도로 이 요청을 500 에러로 처리하고 앱은 여전히 요청을 처리할 수 있는 상태입니다.

위처럼 에러에 대한 응답을 JSON 형식으로 바꿔주는 것 또한 기본 내장된 예외 레이어가 처리합니다.

 

import {
  ...,
  BadRequestException,
} from '@nestjs/common';
...

@Controller('ads')
export class AdsController {
  ...

  @Get(':id')
  fidOne(@Param('id') id: number) {
    if (id < 1) {
      throw new BadRequestException('id 는 0 보다 큰 정수여야 합니다.');
    }
    return this.adsService.findOne(id);
  }
}

 

위 코드는 예외에 대해 NestJS 가 제공하는 예외 클래스를 사용한 예시입니다.

 

 

실제로 이렇게 하지는 않을테지만, NestJS 가 제공하는 예외 클래스를 어떻게 활용하면 되는지를 간단하게 예시로 가져와 봤습니다.

NestJS 가 제공하는 모든 예외는 HttpException 을 상속합니다. 그리고 예외 레이어에선 HttpException 도 아니면서

HttpException 을 상속하지도 않는 예외에 대해선 전부 InternalServerErrorException 으로 처리합니다.

 

- 예외 필터

 

NestJS 에선 기본으로 내장되어 있는 예외 레이어 외에 직접 예외 필터 레이어를 작성해 처리할 수 있습니다.

일반적으론 예외가 발생했을 때 로그를 남기거나 응답 객체를 원하는대로 커스터마이징하고 싶을 때 사용합니다.

저는 이번 예시로 TypeORM 에서 발생하는 에러를 잡는 예외 필터를 한번 작성해보겠습니다.

 

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
import { QueryFailedError, TypeORMError } from 'typeorm';

@Catch(TypeORMError)
export class TypeORMExceptionFilter implements ExceptionFilter {
  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    let status = HttpStatus.INTERNAL_SERVER_ERROR;

    switch (exception.constructor) {
      case QueryFailedError:
        status = HttpStatus.BAD_REQUEST;
        break;
      default:
        break;
    }

    const log = {
      timestamp: new Date(),
      message: exception.message,
    };

    res.status(status).json(log);
  }
}

 

방식은 NestJS 에서 제공하는 ExceptionFilter 를 구현하는 TypeORMExceptionFilter 클래스를 먼저 만들어줍니다. catch 메서드를 원하는대로 구현하면 됩니다. 해당 필터 위에 Catch 데코레이터는 처리되지 않는 예외를 잡을 때 사용하는데, 아무런 인자도 넣지 않으면

모든 예외를 잡게 됩니다. 저는 TypeORM 과 관련된 에러만 잡기 위해 TypeORMError 를 인자로 넣었습니다.

실제로 TypeORM 의 에러라면 응답 코드는 500 이 적절할테고 production 환경에선 특별히 메세지를 보여줄 필요도 없지만 예시로

보이기 위해 400 에러로 처리하고 에러 메세지를 응답에 포함하도록 했습니다.

 

import {
  ...,
  UseFilters,
} from '@nestjs/common';
import { AdsService } from './ads.service';
import { CreateAdDto } from './dto/create-ad.dto';
import { TypeORMExceptionFilter } from './aop/exception-filter';

@Controller('ads')
export class AdsController {
  ...

  @UseFilters(TypeORMExceptionFilter)
  @Get(':id')
  findOne(@Param('id') id: number) {
    return this.adsService.findOne(id);
  }
}

 

위에서 깜빡했는데 새로 작성한 TypeORMExceptionFilter 는 src 하위에 aop 라는 디렉토리를 만들고 그 하위에 작성했습니다.

작성한 예외 필터는 위와 같이 UseFilters 데코레이터를 사용해 특정 엔드포인트에 적용할 수 있습니다.

 

 

이제 예외 필터를 적용한 엔드포인트에 이전과 동일한 요청을 보내보면, 의도한대로 400 에러로 처리되면서 응답에 에러 메세지도

포함되는 것을 확인할 수 있습니다. 예시가 좀 간단하긴 합니다만 예외 필터에서 이보다 더 무언가를 할 필요도 없긴 합니다.

NestJS 가 제공하는 예외 필터를 활용해 좀 더 관점 지향 프로그래밍을 할 수 있게 되었고, 예시에서 적용한 특정 엔드포인트 외에도

특정 컨트롤러 또는 어플리케이션 전체에 적용도 가능하니 관리하는 어플리케이션에 필요한 경우 참고해보시면 좋을 듯 합니다.