BackEnd/Nest.js

[Nest.js] 예외 필터

Grace 2023. 5. 30. 15:07

소프트웨어를 개발하면서 예외 처리는 필수 사항입니다. 어떤 상황에서든 에러는 발생할 수 있고 개발자는 이 에러에 대응책을 마련해둬야 합니다.

예외가 발생할 만한 모든 곳에 예외 처리 코드를 삽입하는 것은 중복 코드를 양산할 뿐 아니라 기능 구현과 관련 없는 코드가 삽입되므로 핵심 기능 구현에 집중하지 못하게 됩니다. 예외가 발생했을 때 에러 로그와 콜 스택을 남겨 디버깅에 사용할 수 있는 별도의 모듈을 작성했다면, 예외 처리기 역시 따로 만들어 한곳에서 공통으로 처리하도록 해야 합니다.

예외 처리

Nest는 프레임워크 내에 예외 레이어를 두고 있습니다. 애플리케이션을 통틀어 제대로 처리하지 못한 예외를 처리하는 역할을 합니다. 아무런 작업을 하지 않아도 기본 예외 처리기가 예외를 잡아서 유저가 이해하기 쉬운 형태로 변환하여 전송합니다.

Nest는 예외에 대한 많은 클래스를 제공합니다. 에러가 발생했을 때 응답을 JSON 형식으로 바꿔주는데 이는 기본으로 내장된 전역 예외 필터가 처리합니다 .내장 예외 필터는 인식할 수 없는 에러(HttpExeption도 아니고 HttpExeption을 상속받지도 않은 에러)를 InternalServerErrorExeption으로 변환합니다. 500 InternalServerError는 요청을 처리하는 과정에서 서버가 예상하지 못한 상황에 놓였다는 것을 나타냅니다. InternalServerErrorExeption의 선언을 보면 HttpExeption을 상속받고 있고 HttpExeption은 다시 자바스크립트의 Error를 상속합니다. 결국 모든 예외는 Error 객체로부터 파생된 것입니다.

그 외 Nest에서 제공하는 모든 예외 역시 HttpExeption을 상속하고 있습니다. 이 예외 클래스들을 이용하여 상황에 따라 적절한 예외를 던지면 됩니다. 적절한 예외 처리는 API를 호출한 클라이언트에서 에러를 쉽게 이해하고 대처할 수 있도록 합니다.

아래는 HttpExeption클래스입니다.

export declare class HttpExeption extends Error {
  ...
  constructor(response: string | Record<string, any>, status: number);
  ...
}
  • response: JSON 응답의 본문입니다. 문자열이나 Record<string, any> 타입의 객체를 전달할 수 있습니다.
  • status: 에러의 속성을 나타내는 HTTP 상태 코드입니다.

JSON 응답의 본문은 STAtusCode와 message 속성을 기본으로 가집니다. 이 값은 예외를 만들 때 response와 status로 구성합니다.

미리 제공된 BadRequestExeption 대신 HttpExeption을 직접 전달하려면 다음과 같이 작성합니다.

throw new HttpExeption({
  errorMessage:: 'id는 0보다 큰 점수여야 합니다',
  foo: 'bar'
}, HttpStatus.BAD_REQUEST)

다음은 Nest에서 제공하는 표준 예외들입니다. 자주 쓰지 않는 것들도 포함되어 있지만, 어떤 상황에서 어떤 에러를 내야 하는지 확인해보세요.

  • 400 BadRequestExeption: 서버가 클라이언트 오류를 감지해 요청을 처리할 수 없거나 하지 않음
  • 401 UnauthorizedExeption: 요청된 리소스에 대한 유효한 인증 자격 증명이 없기 때문에 클라이언트 요청이 완료되지 않음
  • 404 NotFoundExeption: 서버가 요청받은 리소스를 찾을 수 없음
  • 403 ForbiddenExeption: 서버에 요청이 전달되었지만, 권한 때문에 거절되었음
  • 406 NotAcceptableExeption: 서버가 요청의 주도적인 컨텐츠 협상 헤더에 정의된 허용 가능한 값 목록과 일치하는 응답을 생성할 수 없으며, 서버가 기본 표현을 제공하지 않음
  • 408 RequestTimeoutExeption: 서버가 사용하지 않는 연결을 끊고 싶음
  • 409 ConflictExeption: 서버의 현재 상태와 요청이 충돌했음
  • 410 GoneExeption: 원본 서버에서 대상 리소스에 더 이상 접근할 수 없으며, 이상태가 영구적일 가능성이 있음
  • 505 HttpVersionNotSupportedExeption: 요청에 사용된 HTTP 버전을 서버가 지원하지 않음
  • 413 PayloadTooLargeExeption: 요청 엔티티가 서버에 의해 정의된 제한보다 큼
  • 415 UnsupportedMediaTypeExeption: 클라이언트가 보낸 페이로드가 지원하지 않는 형식이기 때문에 서버가 요청을 수락하지 않음
  • 422 UnprocessableEntityExeption: 서버가 요청을 이해하고 문법도 올바르지만 요청된 지시에 따를 수 없음
  • 500 InternalServerErrorExeption: 요청을 처리하는 과정에서 서버가 예상하지 못한 상황에 놓였음
  • 501 NotImplementedExeption: 요청을 수행할 수 있는 기능을 서버가 지원하지 않음
  • 418 ImATeapotExeption: 서버가 요청을 이해할 수 없음(HTTP 표준에서 공식으로 인정되는 상태 코드는 아님)
  • 405 MethodNotAllowedExeption: 서버가 요청 메서드를 알고 있지만 대상 리소스가 이 메서드를 지원하지 않음
  • 502 BadGatewayExeption: 서버가 게이트웨이나 프록시 서버 역할을 하면서 업스트림 서버로부터 유효하지 않은 응답을 받음
  • 503 ServiceUnavailableExeption: 서버가 요청을 처리할 준비가 되지 않음
  • 504 GatewayTimeoutExeption: 서버가 게이트웨이 혹은 프록시의 역할을 하는 동안 시간 안에 업스트림 서버로부터 요청을 마치기 위해 필요한 응답을 받지 못했음
  • 412 PreconditionFailedExeption: 대상 리소스에 대한 액세스가 거부됨

Nest에서 제공하는 기본 예외 클래스는 모두 생성자가 다음과 같은 모양을 가집니다.

constructor(objectOrError?: string | object | any, description?: string);

예외 필터

Nest에서 제공하는 전역 예외 필터 외에 직접 예외 필터 레이어를 둬, 원하는 대로 예외를 다룰 수 있습니다. 예외가 일어났을 때 로그를 남기거나 응답 객체를 원하는 대로 변경하고자 하는 등의 요구 사항을 해결하고자 할 때 사용합니다.

import { ArgumentsHost, Catch, ExeptionFilter, HttpExeption, 
InternalServerErrorExeoption } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch() // 처리되지 않은 모든 예외를 잡으려고 할 때 사용합니다.
export class HttpExeptionFilter implements ExeptionFilter {
  catch(exeption: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();
    
    if(!(exeption instanceof HttpExeption)) { /* 우리가 다루는 대부분의 예외는 이미 Nest에서
      HttpExeption을 상속받는 클래스들로 제공한다고 했습니다. HttpExeption이 아닌 예외는 알 수 없는
      에러이므로 InternalServerErrorExeption으로 처리되도록 했습니다. */
      exeption = new InternalServerErrorExeption();
    }
    
    const response = (exeption as HttpExeption).getResponse();
    
    const log = {
      timestamp: new Date(),
      url: req.url,
      response,
    }
    
    console.log(log);
    
    res.status((exeption as HttpExeption).getStatus())
      .json(response);
  }
}

예외 필터는 @UseFilter 데커레이터로 컨트롤러에 직접 적용하거나 전역으로 적용할 수 있습니다. 예외 필터는 전역 필터 하나만 가지도록 하는 것이 일반적입니다.

  • 특정 엔드포인트에 적용할 때
@Controller('users')
export class UsersControllr {
  ...
  @UseFilters(HttpExeptionFilter)
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.crate(createUserDto)
  }
}
  • 특정 컨트롤러 전체에 적용할 때
@Controller('users')
@UseFilters(HttpExeptionFilter)
export class UsersController {
  ...
}
  • 애플리케이션 전체에 적용할 때
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExeptionFilter()); // 전역 필터 적용
  await app.listen(3000)
}

부트스트랩 과정에서 전역 필터를 적용하는 방식은 필터에 의존성 주입을 할 수 없다는 제약이 있습니다. 예외 필터의 수행이 예외가 발생한 모듈 외부에서 이루어지기 때문입니다. 의존성 주입을 받고자 한다면 예외 필터를 커스텀 프로바이더로 등록하면 됩니다.

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

@Module({
  provider: [
    {
      provide: APP_FILTER,
      useClass: HttpExeptionFilter
    }
  ]
})

'BackEnd > Nest.js' 카테고리의 다른 글

[Nest.js] 태스크 스케줄링  (0) 2023.05.31
[Nest.js] 인터셉터  (0) 2023.05.31
[Nest.js] 로깅  (0) 2023.05.30
[Nest.js] JWT 인증/인가  (0) 2023.05.26
[Nest.js] 파이프와 유효성 검사  (0) 2023.05.16