NestJS 帶你飛! 시리즈 번역 08# Exception & Exception filters

2023. 6. 1. 17:35개발 문서 번역/NestJS

728x90
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다. 

 

Exception이란 무엇인가요?

간단하게 말하면 시스템에 오류가 발생한 상황입니다.

프로그램이 성공적으로 완료되지 못한 상황에는 개발자가 예상치 못한 오류를 일으키거나 사용자가 원하지 않은 상황을 야기합니다.

일반적으로 시스템은 이 "예외"에 대한 처리를 하여 만약 예외 상황이 발생하였을 경우 해당하는 오류 메세지를 전달합니다. 

자주 예로 들었던 레스토랑을 예로 들자면 고객의 클레임이 걸려오면 해당 인원이 손님에게 직접 찾아가 클레임을 처리하고 이 인원이 하는 매 한마디 한마디가 생각을 거치지 않고 내뱉는 것이 아닌 아닌 일관된 클레임 처리방식으로 나온 답변을 하고 있다는 생각을 들게 해야합니다.

 

JavaScript에는 가장 자주 쓰이는 예외 처리방법인 Error를 사용합니다. 이 Error는 Exception의 개념입니다.

오류 메세지를 통일된 양식으로 사용자에게 제공합니다.

throw new Error('오류가 발생하였습니다.');

 

Nest 예외처리 원리

예외가 던져지면 이 예외들을 어딘가에서 잡아와야 합니다. 또 이 예외에 알맞은 응답을 가진 양식도 준비하여야 합니다.

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

애플리케이션 코드에서
예외를 처리하지 않으면 이 레이어에서 예외를 포착하여 적절히 사용자 친화적인 응답을 자동으로 보냅니다.

*Nest의 라이프 사이클에 대해 알고싶다면? 

더보기

MiddleWare -> Guard -> Interceptor -> Pipe -> Controller -> Interceptor 입니다.
아래의 그림에서는 guard에서 부터 예외 상황이 발생할시에는 
@useCatch로 exception filter에서 처리할 수 있습니다.
exception filter를 global로 설정하여 원하는 동작을 수행할 수 도 있습니다.

 

작은 실험을 하나 해보겠습니다. app.controller.ts의 내용을 수정하여 getHello() 메서드가 Error를 던지도록 해보겠습니다.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new Error('에러 발생했음!');
    return this.appService.getHello();
  }
}

 

http://127.0.0.1:3000 으로 접속해보면.. 우리가 정의했던 에러 발생했음! 이라는 메세지는 안보이고

아래와 같은 에러 메세지를 확인할 수 있습니다.

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

 

이렇게 오류가 뜨는 원인은 위에서 말했던것과 같은 이유입니다.
Nest에 내장된 Exception Filter가 이 예외의 오류 유형을 감지하려고 하기 때문입니다.
이 Exception Filter는 Nest 내장 HttpException이나 클래스를 상속받는 Exception만을 처리할수 있습니다.

이 두개의 유형이 아닌 예외는 모두 Internal server error를 출력하게 되어있습니다.

 

표준 예외 (Standard Exception, 標準 Exception)

Nest에 내장 예외 클래스 중 하나인 HttpException은 매우 유연한 사용자 경험을 제공해주며, 이 HttpExceptionclass 이기도 합니다.

constructor 설정 후 두개의 파라미터에 예외 메시지와 해당하는 Http Status Code를 작성합니다.

아래는 app.controller.ts의 예제입니다.

import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new HttpException('에러 발생!', HttpStatus.BAD_REQUEST);
    return this.appService.getHello();
  }
}

http://localhost:3000 에 접속해보면 에러 메세지가 같음을 확인할 수 있습니다.

{
  "statusCode": 400,
  "message": "에러 발생!"
}

 

Nest가 설정해주는 양식을 쓰지 않고 싶을때도 있을겁니다.
이때는 에러 메세지의 파라미터를 Object로 바꿔주면 됩니다.이렇게 작성하면 Nest가 자동으로 지정한 양식에 덮어 씁니다.

아래는 app.controller.ts 예제입니다.

import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new HttpException(
      {
        code: HttpStatus.BAD_REQUEST,
        msg: '에러 발생!'
      },
        HttpStatus.BAD_REQUEST
      );
    return this.appService.getHello();
  }
}

http://127.0.0.1:3000 에 접속해보면 양식이 이미 우리가 정해두었던 양식과 일치하는걸 확인할 수 있습니다.

{
  "code": 400,
  "msg": "에러 발생!"
}

 

내장 Http Exception(Built-In Http Exception, 內建 Http Exception)

Nest는 HttpException에서 상속된 Exception을 사용할 수 있습니다.

개발자에게 다른 에러에서는 다른 Exception을 사용하게 하여 다양한
예외 상황에 알맞는 에러 메세지를 제공해 줄 수 있도록 하고 있습니다.

아래는 사용할 수 있는 내장 Http Exception 입니다.

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

이 포스팅에서는 BadRequestException 을 사용하여 app.controller.ts를 수정해보겠습니다.

import { BadRequestException, Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new BadRequestException('에러 발생!');
    return this.appService.getHello();
  }
}

http://127.0.0.1:3000 에 접속하면 아래와 같은 결과를 얻을 수 있습니다.

{
  "statusCode":400,
  "message":"에러 발생!",
  "error":"Bad Request"
}

Nest에서 이미 설정한 양식을 사용하지 않고 싶을때가 있을것입니다.

위에서도 언급했듯 Object로 덮어주면 됩니다.

import { BadRequestException, Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new BadRequestException({ msg: '에러 발생!' });
    return this.appService.getHello();
  }
}

http://127.0.0.1:3000에 접속하면 Nest에서 정해준 양식이 아닌 우리가 정한 양식으로 응답이 돌아온 것을 확인할 수 있습니다.

{
  "msg": "에러 발생!"
}

 

커스텀 예외(Custom Exception, 自訂 Exception)

HttpExceptionclass라고 언급 했었습니다.

대부분의 경우에선 Custom Exception을 사용할 필요는 없습니다.

이미 Nest에서 제공하는 Exception으로도 충분하니까요.
하지만 정말 필요하다면 HttpException 클래스를 상속받아 사용할 수 있습니다.

아래는 src/exceptions 폴더 아래에 있는 custom.exception.ts의 예제입니다.

import { HttpException, HttpStatus } from '@nestjs/common';

export class CustomException extends HttpException {
	constructor () {
		super('알수없는 오류', HttpStatus.INTERNAL_SERVER_ERROR);
	}
}

아래는 app.controller.ts의 예제입니다.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { CustomException } from './exceptions/custom.exception';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new CustomException();
    return this.appService.getHello();
  }
}

http://127.0.0.1:3000에 접속해보면 아래와 같은 결과를 확인 할 수 있습니다.

{
  "statusCode":500,
  "message":"알수없는 오류"
}

 

커스텀 예외 필터 (Custom Exception Filter, 自訂 Exception Filter)

모든 예외처리에 대한 완전한 제어를 원할 수도 있습니다.

예를들어 로깅을 추가하거나 이 레이어에서 반환 양식을 정의하거나 하는 경우에 말이죠.

Exception Filter는 반드시 @Catch(...exceptions: Type<any>[]) 데코레이터로 예외를 잡아내야 합니다.

특정한 클래스의 Exception도 지정하거나 전부 잡아낼 수 있습니다.

@Catch 안에 어떠한 파라미터도 입력하지 않으면 예외 전부를 잡아냅니다.

또한 해당 classExceptionFilter<T> 인터페이스를 구현해야 하며, 이를 위해서 catch(exception: T, host: ArgumentHost) 메서드를 제공해야 합니다. 이때 T는 예외 타입을 나타냅니다.

그렇다면 src/filters에 새로 http-exception.filter.ts를 만든 후 HttpException을 잡아내는 Exception Filter를 하나 만들어보도록 하겠습니다.

import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();
    const message = exception.message;
    const timestamp = new Date().toISOString();

    const responseObject = {
        code: status,
        message,
        timestamp
    };
    response.status(status).json(responseObject);
  }
}

이 예제는 HttpException을 잡아내고 싶어합니다. 예외를 잡아낼 때 이 Exception과 ArgumentHost 라는것이 있는데

이 ArgumentHost를 통해 Response의 객체를 받아올 수 있습니다.

아래의 양식으로 프론트단에 전송됩니다.

{
  "code": 400,
  "message": "오류 발생!",
  "timestamp": "2023-06-01T09:41:15.216Z"
}

ArgumentsHost

class는 요청과 상관있는 파라미터의 값을 받아올 때 사용되며 하나의 추상 개념으로
Nest는 REST API, WebSocket, MicroService을 구현할 수 있습니다. 매 구조의 파라미터는 다를수도 있습니다.
이때 추상화를 통해 합치는것이 가장 알맞은 방법입니다.
Express를 기반으로 하는 REST API의 경우 Request, ResponseNextFunction을 추상화 하였습니다.

MicroService라면 내용물이 조금 다를것 입니다.
ArgumentHost는 이러한 하위 정보를 가져오기 위한 공통 인터페이스를 제공해 줍니다.

 

현재 애플리케이션의 유형 가져오기

getType() 메서드를 통해 현재 애플리케이션의 유형을 받아올 수 있습니다. REST API라면 문자열 http를 받아옵니다. :

host.getType() === 'http'; // true

 

캡슐화된 파라미터 가져오기

getArgs()를 통해 현재 애플리케이션의 캡슐화된 파라미터를 받아올 수 있습니다.
Express를 기반으로 한 REST API라면 Request, ResponseNextFunction입니다.

const [req, res, next] = host.getArgs();

위의 캡슐화된 파라미터들이 배열 형식으로 선언되있는걸 확인할 수 있습니다.
Nest에서는 해당하는 파라미터의 인덱스 값을 받아올 수 있는 메서드 getArgByIndex(index: number)를 사용할 수 있습니다.

const req = host.getArgByIndex(0);

위의 방법은 배열을 통해 파라미터를 받아오는 방법입니다.

하지만 이 방법은 다른 곳에서 재사용할 때 어려움이 있을 수 있습니다.
각각의 곳에서 서로 다른 캡슐화된 파라미터를 가지기 때문입니다. 이럴때 아래와 같이 작성하여 받아올 수 있습니다.

const rpcCtx: RpcArgumentsHost = host.switchToRpc(); // MicroService의 캡슐화된 내용
const httpCtx: HttpArgumentsHost = host.switchToHttp(); // REST의 캡슐화된 내용
const wsCtx: WsArgumentsHost = host.switchToWs(); // WebSocket의 캡슐화된 내용

 

Exception Filter 사용

사용 방법은 정말 간단합니다. 크게 보면 두가지의 선택지가 있는데요.

1. 단일 리소스 : Controller의 메소드에서 @UseFilters 데코레이터를 추가해 사용합니다.

이는 단일 리소스에만 적용됩니다.

2. Controller : Controller에 바로 @UseFilters 데코레이터를 추가해 사용합니다.
이는 해당 Controller에 모두 적용됩니다.

 

@UseFilters의 파라미터는 사용할 ExceptionFilter를 지정해주면 됩니다. 

아래 단일 리소스의 예제가 있습니다. app.controller.ts를 수정하여 테스트 해봅시다.

import { BadRequestException, Controller, Get, UseFilters } from '@nestjs/common';
import { AppService } from './app.service';
import { HttpExceptionFilter } from './filters/http-exception.filter';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  @UseFilters(HttpExceptionFilter)
  getHello(): string {
    throw new BadRequestException('오류 발생!');
    return this.appService.getHello();
  }
}

아래는 Controller에 적용한 예제입니다.

import { BadRequestException, Controller, Get, UseFilters } from '@nestjs/common';
import { AppService } from './app.service';
import { HttpExceptionFilter } from './filters/http-exception.filter';

@Controller()
@UseFilters(HttpExceptionFilter)
export class AppController {
  constructor(private readonly appService: AppService) {
  }

  @Get()
  getHello(): string {
    throw new BadRequestException('오류 발생!');
    return this.appService.getHello();
  }
}

http://127.0.0.1:3000에 접속해 나온 결과는 아래와 같습니다.

{
  "code":400,
  "message":"에러 발생!",
  "timestamp":"2023-06-01T09:25:54.102Z"
}
주의 : @UseFilters가 가져온 Exception Filter는 class 자체여도, 인스턴스여도 가능합니다.
둘의 차이는 class를 통해 Nest에서 의존성 주입을 하여 인스턴스 관리를 할수 있냐 없냐의 차이입니다.
만약 특별히 필요한 상황이 아니라면 class를 통해 가져오도록 합시다.

 

전역 예외 필터 (Global Exception Filter, 全域 Exception Filter)

내 Exception Filter를 전역적으로 모든 리소스에 대해 사용하고 싶을 경우 사용하면 됩니다.

main.ts에 수정을 해보겠습니다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';

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

useGlobalFilters를 통해 전역적으로 Exception Filter를 적용시켰습니다. 편리하지 않나요?

 

의존성 주입을 통해 전역 예외 필터 구현하기

위의 방법은 모듈 외부에서 전역 설정이 이루어집니다.

의존성 주입을 통해 전역으로 예외 필터를 구현하고 싶다면 어떤 방법이 있을까요?

Nest는 이 문제의 답안을 제공해주는데, AppModule에 설정하면 됩니다.

의존성 주입이라면 Provider와 아주 밀접한 관계를 맺고 있습니다.

token을 지정할 때 APP_FILTER로 구현하면 됩니다.

아래는 useClass를 통해 생성할 인스턴스의 클래스를 만들어 보았습니다.

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HttpExceptionFilter } from './filters/http-exception.filter';

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

 

마치며

어떻게 예외를 잘 처리할수 있을까에 대한 궁금증은 하나의 학문과 같을정도로 방대합니다.

Nest는 Exception의 예외 양식을 정의하여 정형화된 예외처리 방식을 제공하며 유연성 있게 대처할 수 있는 방법을 제공해줍니다.
또한 Exception Filter를 통일하여 처리할 경우에는 일련의 귀찮은 작업들을 하지 않을 수 있어 개발하기 무척 편리해집니다.

오늘 배운 지식을 다시한번 정리하며 포스팅을 마치겠습니다.

 

1. Exception은 예외 객체입니다.

2. Nest에 내장된 표준 Exception은 HttpException입니다.

3. Nest에 내장된 예외 처리 메커니즘의 이름은 Exception Filter이며 자동으로 예외를 처리해주며 그에 맞는 응답 양식도 제공하고 있습니다.

4. 내장된 Exception Filter는 HttpException이 아닌 예외를 받았을 경우 전부 Internal server error를 응답합니다.

5. HttpException은 사전에 지정된 Object의 양식을 덮어씌워 작동합니다.

6. Nest에는 많은 Http Exception이 내장되어 있습니다.

7. Exception Filter는 커스텀하여 사용할 수 있으며 개별/전역적으로 적용할 수 있습니다.

8. 전역 Exception Filter는 의존성 주입을 통해 구현할 수 있습니다.