[Nestjs Study] 6. Exception Filter

hiio420.official·2025년 6월 10일

nestjs

목록 보기
6/7


Nest에는 애플리케이션 전체에서 처리되지 않은 모든 예외를 처리하는 내장 예외 계층이 있습니다. 애플리케이션 코드에서 예외를 처리하지 못하면 이 계층에서 해당 예외를 포착하여 사용자에게 친숙한 적절한 응답을 자동으로 전송합니다.

기본적으로 이 작업은 내장된 전역 예외 필터 에 의해 수행되며 , 이 필터는 유형 HttpException(및 해당 하위 클래스)의 예외를 처리합니다. 예외가 인식되지 않는HttpException 경우( 또는 를 상속하는 클래스가 아닌 경우 HttpException), 내장된 예외 필터는 다음과 같은 기본 JSON 응답을 생성합니다.

표준 예외 발생

Nest는 패키지 HttpException에 내장된 클래스를 제공합니다 @nestjs/common. 일반적인 HTTP REST/GraphQL API 기반 애플리케이션의 경우, 특정 오류 발생 시 표준 HTTP 응답 객체를 전송하는 것이 가장 좋습니다.

예를 들어, 에 메서드( 라우트 핸들러) CatsController가 있습니다 . 이 라우트 핸들러가 어떤 이유로든 예외를 발생시킨다고 가정해 보겠습니다. 이를 보여주기 위해 다음과 같이 하드코딩합니다.


// cats.controller.ts

import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get(':id')
  async findOne(@Param('id') id: string) {
    const cat = await this.catsService.findById(id);
    if (!cat) {
      throw new NotFoundException(`고양이 ID ${id}를 찾을 수 없습니다.`);
    }
    return cat;
  }
}

생성자 HttpException는 응답을 결정하는 두 가지 필수 인수를 사용합니다.

  1. response는 JSON 응답 본문을 정의합니다. 아래 설명된 대로 string 또는 object 일 수 있습니다.
  2. HTTP 상태 코드를status 정의합니다 .

기본적으로 JSON 응답 본문에는 두 가지 속성이 포함됩니다.

  1. statusCodestatus: 인수 에 제공된 HTTP 상태 코드를 기본값으로 사용합니다.
  2. message: HTTP 오류에 대한 간략한 설명status
    JSON 응답 본문의 메시지 부분만 재정의하려면 response인수에 문자열을 제공하세요. JSON 응답 본문 전체를 재정의하려면 response인수에 객체를 전달하세요. Nest는 객체를 직렬화하여 JSON 응답 본문으로 반환합니다.

두 번째 생성자 인수 - status는 유효한 HTTP 상태 코드여야 합니다. .@nestjs/common HttpStatus에서 가져온 열거형을 사용하는 것이 가장 좋습니다.

세 번째 생성자 인수(선택 사항) 인 options-HttpException 는 오류 원인을 제공하는 데 사용할 수 있습니다 . 이 cause객체는 응답 객체로 직렬화되지 않지만, 로깅 목적으로 유용할 수 있으며, 오류 발생의 원인이 된 내부 오류에 대한 중요한 정보를 제공합니다 .

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

throw new HttpException(
  {
    statusCode: HttpStatus.BAD_REQUEST,
    message: '입력한 값이 유효하지 않습니다.',
    error: 'Bad Request',
    details: {
      field: 'email',
      issue: '이메일 형식이 아닙니다.',
    },
  },
  HttpStatus.BAD_REQUEST,
  {
    cause: new Error('ValidationError: 이메일 형식 오류'),
  },
);

다음은 전체 응답 본문을 재정의하고 오류 원인을 제공하는 예입니다.

생성자 인수 설명

1. response (첫 번째 인수)

  • 타입: string 또는 object
  • 역할: 클라이언트에 반환될 JSON 응답의 본문(body) 정의
  • 위 예시에서는 아래 객체 전체가 클라이언트에게 응답됩니다:
{
  "statusCode": 400,
  "message": "입력한 값이 유효하지 않습니다.",
  "error": "Bad Request",
  "details": {
    "field": "email",
    "issue": "이메일 형식이 아닙니다."
  }
}
  • 이처럼 커스텀 필드(details)를 넣어서 응답을 세부적으로 제어할 수 있습니다.

2. status (두 번째 인수)

  • 타입: number (HTTP 상태코드)
  • 역할: 응답의 HTTP 상태 코드를 정의
  • HttpStatus.BAD_REQUEST400이며, Nest에서는 @nestjs/common에서 제공하는 HttpStatus enum을 사용하는 것이 좋습니다.

3. options (세 번째 인수) - 선택

  • 타입: { cause?: Error }
  • 역할: 로깅 또는 디버깅용 내부 에러 정보를 담는 옵션
  • 이 값은 클라이언트에게는 보이지 않지만, Nest의 내부 로거에서 예외 원인(cause)을 추적하거나 에러 스택을 기록하는 데 유용합니다.

실제 사용 예: 유효성 검사 실패 시

예를 들어, 사용자가 이메일 필드에 잘못된 값을 보냈을 때, 아래와 같이 사용할 수 있습니다:

if (!isValidEmail(input.email)) {
  throw new HttpException(
    {
      statusCode: 400,
      message: '입력한 이메일이 유효하지 않습니다.',
      error: 'Bad Request',
      details: {
        field: 'email',
        issue: '형식이 이메일이 아닙니다.',
      },
    },
    HttpStatus.BAD_REQUEST,
    {
      cause: new Error('이메일 정규표현식 검사 실패'),
    },
  );
}

이렇게 하면 프론트엔드에서는 구체적인 오류 메시지를 기반으로 사용자에게 피드백을 줄 수 있고, 백엔드에서는 cause를 기반으로 로깅이나 디버깅이 용이해집니다.

예외 로깅

기본적으로 예외 필터는 (해당 필터에서 상속되는 모든 예외)와 같은 내장 예외를 기록하지 않습니다 . 이러한 예외가 발생하면 일반 애플리케이션 흐름의 일부로 처리되므로 콘솔에 표시되지 않습니다.WsExceptionRpcException 및 HttpException와 같은 다른 내장 예외에도 동일한 동작이 적용됩니다 .

@nestjs/common IntrinsicException예외는 모두 패키지 에서 내보내는 기본 클래스를 상속합니다 . 이 클래스는 일반적인 애플리케이션 동작에 포함되는 예외와 그렇지 않은 예외를 구분하는 데 도움이 됩니다.

이러한 예외를 기록하려면 사용자 지정 예외 필터를 만들 수 있습니다. 다음 섹션에서 이 작업을 수행하는 방법을 설명하겠습니다.

사용자 정의 예외

대부분의 경우 사용자 지정 예외를 작성할 필요가 없으며, 기본 Nest HTTP 예외를 사용할 수 있습니다. 사용자 지정 예외를 생성해야 하는 경우, 사용자 지정 예외가 기본 클래스를 상속하는 자체 예외 계층 구조를 HttpException으로 만드는 것이 좋습니다 . 이 방법을 사용하면 Nest가 예외를 인식하고 오류 응답을 자동으로 처리합니다. 이러한 사용자 지정 예외를 구현해 보겠습니다.



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

내장된 HTTP 예외

Nest는 기본 .NET Framework에서 상속되는 표준 예외 집합을 제공합니다 HttpException. 이러한 예외는 @nestjs/common패키지에 포함되어 있습니다.

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException
    모든 내장 예외는 매개변수 cause를 사용하여 오류와 오류 설명을 모두 제공할 수도 있습니다.

예외 필터

기본(내장) 예외 필터는 여러 사례를 자동으로 처리할 수 있지만, 예외 계층을 완벽하게 제어 하고 싶을 수도 있습니다. 예를 들어, 로깅을 추가하거나 일부 동적 요소에 따라 다른 JSON 스키마를 사용하고 싶을 수 있습니다. 예외 필터는 바로 이러한 목적을 위해 설계되었습니다. 이를 통해 클라이언트로 전송되는 응답의 내용과 제어 흐름을 정확하게 제어할 수 있습니다.

클래스의 인스턴스인 예외를 포착하고, 이에 대한 사용자 지정 응답 로직을 구현하는 예외 필터를 만들어 보겠습니다 . 이를 위해서는 기본 플랫폼 과 HttpException 객체 에 접근해야 합니다 . 객체에 접근하여 원본 객체를 추출 하고 로깅 정보에 포함할 것입니다. 또한, 이 객체를 사용하여 메서드를 통해 전송되는 응답을 직접 제어합니다.

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

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    // 예외 로깅
    this.logger.error(
      `[${request.method}] ${request.url} ${status} - ${JSON.stringify(exceptionResponse)}`,
    );

    // 클라이언트에게 반환
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      ...(typeof exceptionResponse === 'string'
        ? { message: exceptionResponse }
        : exceptionResponse),
    });
  }
}

바인딩 필터


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

또는 전역


// 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();

또는 전역 모듈


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

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

Catch Every

모든 예외를 처리할 때에는 @Catch를 비워두세요


import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';

@Catch()
export class CatchEverythingFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    // In certain situations `httpAdapter` might not be available in the
    // constructor method, thus we should resolve it here.
    const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

0개의 댓글