상똥이의 Back-End 공부방
[Nest.js] Exception filters 본문
Exception filters
Nest는 애플리케이션 전체에서 처리되지 않은 예외를 처리해주는 예외 레이어가 내장되어있습니다. 애플리케이션 코드에서 예외처리가 되지 않은 경우 이 레이어에 인식되고 자동으로 사용자 친화적인 응답을 내보냅니다.
별도의 설치 과정이 필요 없이, 이는 HttpException 타입의 예외를 처리하는 내장된 전역 예외 필터에 의해 수행됩니다. 만약 예외상황이 인식되지 않으면 내장된 예외 필터가 아래의 JSON 형식의 응답을 생성합니다.
{
"statusCode": 500,
"message": "Internal server error"
}
HINT
전역 예외 필터는 http-errors 라이브러리를 부분적으로 지원합니다. 기본적으로, 상태코드와 메시지를 포함하는 예외가 발생하면 적절히 그 값이 만들어지고 응답으로 전송됩니다.
Throwing standard exceptions
Nest는 @nestjs/common 패키지에서 제공되는 HttpException 클래스를 제공합니다. 전형적인 HTTP REST/GraphQL API 기반의 애플리케이션은 특정한 오류가 발생했을 때 표준 HTTP 응답 객체를 보내는 것이 가장 좋습니다.
예를 들어, CatsController에는 findAll() 메서드가 있습니다. 이 경로 처리기가 어떠한 이유로 예외를 반환한다고 추정해보겠습니다. 이를 입증하기 위해 아래와 같이 하드코딩해보겠습니다.
// cats.controller.ts
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
HINT
HttpStatus를 사용했습니다. 이는 @nestjs/common 패키지에서 임포트할 수 있는 enum입니다.
만약 클라이언트가 이 엔드포인트를 호출하면 응답은 아래와 같이 나타납니다.:
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException 생성자는 응답을 결정하는 두 가지 인수를 가집니다.:
- 응답 인수는 JSON 응답 본문을 정의합니다. 문자열이나 아래와 같은 객체형태입니다.
- 상태 인수는 HTTP 상태 코드를 정의합니다.
기본적으로 JSON 응답은 다음의 두 속성을 포함합니다.
- 상태코드: 상태 인수에 제공되는 HTTP 상태 코드 기본값
- 메시지: 상태에 기반한 HTTP 에러에 관한 짧은 설명
JSON 응답 본문의 메시지 부분만 재정의하려면 응답 인수에 문자열을 제공하면 됩니다. 응답 본문의 전체를 재정의하고 싶다면, 응답 인수 내의 객체를 거치면 됩니다. Nest는 객체를 직렬화해서 JSON 응답 본문으로 반환할 것입니다.
두 번째 생성자 인자(상태)는 유효한 HTTP 응답 코드여야 합니다. 가장 좋은 방법은 @nestjs/common패키지에서 사용할 수 있는 HttpStatus를 활용하는 것입니다.
세 번째 생성자 인자(옵션)는 선택적인 것으로, 에러의 원인을 제공하는 데 사용됩니다. 이 원인 객체는 응답 객체에 직렬화되지는 않지만 HttpException이 발생하게 만든 내부 오류에 대한 귀중한 정보를 제공하므로 로그를 남기는 목적에 유용할 수 있습니다.
아래는 전체 응답 본문을 재정의하고 에러의 원인을 제공하는 예시입니다.:
// cats.controller.ts
@Get()
async findAll() {
try {
await this.service.findAll()
} catch (error) {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN, {
cause: error
});
}
}
위와 같이 사용하면, 응답은 아래와 같이 나타납니다.:
{
"status": 403,
"error": "This is a custom message"
}
Custom exceptions
많은 경우에 예외를 직접 작성하지 않아도 되며 다음 섹션에서 설명할 내장된 Nest HTTP exception을 사용할 수 있습니다. 만약 사용자 정의된 예외를 만들어야 하는 경우 HttpException 클래스에서 상속하고, 고유한 예외 계층을 만드는 것이 좋습니다. 이런 접근 방식으로 Nest는 사용자 정의된 예외를 인식할 수 있고 자동으로 에러 응답을 관리할 것입니다. 이와 같은 사용자 정의 예외를 구현해보겠습니다.:
// forbidden.exception.ts
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
ForbiddenException이 HttpException을 확장하기 때문에 내장된 예외처리기와 원활히 작동할 것입니다. 따라서, findAll() 메서드 안에서 사용할 수 있습니다.
// cats.controller.ts
@Get()
async findAll() {
throw new ForbiddenException();
}
Built-inHTTP exceptions
Nest는 HttpException을 상속하는 표준 예외들을 제공합니다. @nestjs/common 패키지로부터 가져올 수 있으며 보통의 HTTP 예외를 대표합니다.:
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- HttpVersionNotSupportedException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableEntityException
- InternalServerErrorException
- NotImplementedException
- ImATeapotException
- MethodNotAllowedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
- PreconditionFailedException
모든 내장된 예외들은 에러 원인과 에러 설명을 파라미터를 통해 제공할 수 있습니다.
throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })
위와 같이 사용하면 응답은 아래와 같이 나타납니다.:
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400,
}
Exception filters
기본(내장) 예외 필터가 자동으로 많은 경우를 처리할 수 있지만, 예외 레이어에 대한 완전한 제어가 필요할 수 있습니다. 예를 들어, 로깅을 추가하거나 일부 동적 요소를 기반으로 다른 JSON 스키마를 사용할 수 있습니다. 예외 필터는 정확히 이를 위해 제작되었습니다. 이를 통해 정확한 제어 흐름과 클라이언트로 다시 전송되는 응답 내용을 제어할 수 있습니다.
HttpException 클래스의 인스턴스인 예외를 포착하고 이에 대한 사용자 지정 응답 로직을 구현하는 예외 필터를 만들어 보겠습니다. 이를 위해서는 기본 플랫폼 요청 및 응답 개체에 액세스해야 합니다. 원본 URL을 꺼내서 이를 로깅 정보에 포함할 수 있도록 요청 개체에 액세스합니다. response.json() 메서드를 사용하여 전송된 응답을 직접 제어하기 위해 Response 개체를 사용합니다.
// 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,
});
}
}
HINT
모든 예외 필터는 ExceptionFilter<T> 제네릭 인터페이스를 포함해야 합니다. 이는 캐치(exeption: T, host: ArgumentsHost) 메서드를 사용하도록 요구합니다. 이를 위해서는 표시된 서명과 함께 catch(예외: T, 호스트: ArgumentsHost) 메서드를 제공해야 합니다. T는 예외 유형을 나타냅니다.
WARNING
@nestjs.platform-fastify를 사용한다면 response.json() 대신 response.send()를 사용할 수 있습니다. fastify의 정확한 타입을 호출하세요.
@Catch(HttpException) 데코레이터는 필수 메타데이터를 예외 필터에 바인딩하여 이 특정 필터가 HttpException 유형의 예외만 찾고 있음을 Nest에 알립니다. @Catch() 데코레이터는 파라미터 한 개 또는 리스트를 필요로 하기도 합니다. 이는 여러 타입의 예외를 한 번에 처리하는 필터를 구축할 수 있게 해줍니다.
Arguments host
catch() 메서드의 파라미터를 살펴보겠습니다. 예외 파라미터는 현재 처리 중인 예외 객체입니다. host 파라미터는 ArgumentsHost 객체입니다. ArgumentsHost는 실행 컨텍스트 챕터*에서 자세히 살펴볼 유용한 객체입니다. 이 코드 샘플에서는 이를 사용하여 원래 요청 처리기(예외가 발생한 컨트롤러에 있음)로 전달되는 요청 및 응답 개체에 대한 참조를 얻습니다. 이 코드 샘플에서는 ArgumentsHost에 대한 몇 가지 도우미 메서드를 사용하여 원하는 요청 및 응답 개체를 가져왔습니다. 여기에서ArgumentsHosthere에 대해 자세히 알아보세요.
* 이러한 추상화 수준의 이유는 ArgumentsHost가 모든 컨텍스트(예: 현재 작업 중인 HTTP 서버 컨텍스트는 물론 마이크로서비스 및 WebSocket도 포함)에서 작동하기 때문입니다. 실행 컨텍스트 장에서는 ArgumentsHost 및 해당 도우미 함수를 사용하여 실행 모든 컨텍스트에 대한 적절한 기본 인수에 액세스할 수 있는 방법을 살펴보겠습니다. 이를 통해 모든 컨텍스트에서 작동하는 일반 예외 필터를 작성할 수 있습니다.
Binding filters
새로운 HttpExceptionFilter를 CatsController의 create() 메소드에 연결해 보겠습니다.
// cats.controller.ts
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
HINT
@UseFilters() 데코레이터는 @nestjs/common 패키지에서 호출됩니다.
@UseFilters() 데코레이터를 사용했습니다. @Catch() 데코레이터와 비슷하게, 단일 필터 인스턴스 또는 필터 인스턴스 리스트를 받을 수 있습니다. 여기서는 HttpExceptionFilter 인스턴스를 생성했습니다. 또는 인스턴스 대신 클래스를 전달하여 인스턴스화에 대한 책임을 프레임워크에 맡기고 의존성 주입을 활성화할 수 있습니다.Alternatively, you may pass the class (instead of an instance), leaving responsibility for instantiation to the framework, and enabling dependency injection.
// cats.controller.ts
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
HINT
가능하면 인스턴스 대신 클래스를 사용해 필터를 만들도록 권고합니다. Nest는 전체 모듈에서 동일한 클래스의 인스턴스를 쉽게 재사용할 수 있으므로 메모리 사용량이 줄어듭니다.
// cats.controller.ts
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
이 생성자 집합은 CatsController에 정의된 모든 경로에 HttpExceptionFilter를 설정합니다.
전역 필터를 생성하기 위해, 아래와 같이 작성해야 합니다.:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
WARNING
useGlobalFilters() 메서드는 게이트웨이 또는 하이브리드 애플리케이션에 대한 필터를 설정하지 않습니다.
전역 필터는 모든 컨트롤러와 모든 경로 처리기 등 전체 애플리케이션에서 사용됩니다. 의존성 주입 측면에서, 모든 모듈 외부에서 등록된 전역 필터(위의 예에서와 같이 useGlobalFilters() 사용)는 모든 모듈의 컨텍스트 외부에서 수행되므로 종속성을 주입할 수 없습니다. 이 문제를 해결하려면 다음 구성을 사용하여 모든 모듈에서 직접 전역 범위 필터를 등록할 수 있습니다.:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
HINT
필터에 의존성 주입을 위해 이런 접근 방식을 사용할 때 유의해야 할 것은, 이 구성이 사용된 어느 모듈이든 상관 없이 필터는 사실 전역적입니다. 필터(HttpExceptionFilter 위의 예에서는)가 정의된 모듈을 선택합니다. 또한, useClass 사용자 지정 공급자 등록을 처리하는 유일한 방법은 아닙니다. 여기에서 자세히 확인하세요.
이 기술을 사용하면 필요한 만큼 필터를 추가할 수 있습니다. 단순히 공급자 배열에 각각을 추가하면 됩니다.
Catch everything
In order to catch every unhandled exception (regardless of the exception type), leave the @Catch() decorator's parameter list empty, e.g., @Catch().
In the example below we have a code that is platform-agnostic because it uses the HTTP adapter to deliver the response, and doesn't use any of the platform-specific objects (Request and Response) directly:
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter 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);
}
}
WARNING
모든 것을 포착하는 예외 필터와 특정 유형에 바인딩된 필터를 결합할 때 특정 필터가 바인딩된 유형을 올바르게 처리할 수 있도록 "Catch everything" 필터를 먼저 선언해야 합니다.
Inheritance
보통은 애플리케이션의 요구사항에 맞춰 사용자 정의된 필터를 생성할 것입니다. 그러나 내장된 기본 전역 예외 필터를 단순히 확장하고 특정 요인에 따라 동작을 재정의하려는 경우의 사용 사례가 있을 수 있습니다.
예외 처리를 기본 필터에 위임하려면 BaseExceptionFilter를 확장하고 상속된 catch() 메서드를 호출해야 합니다.
// all-exceptions.filter.ts
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);
}
}
WARNING
BaseExceptionFilter를 확장하는 메서드 범위 및 컨트롤러 범위 필터는 new로 인스턴스화하면 안 됩니다. 대신 프레임워크가 자동으로 인스턴스화하도록 하세요.
전역 필터는 기본 필터를 확장할 수 있습니다. 이는 두 가지 방식으로 이뤄질 수 있습니다.
첫 번째 방법은 사용자 정의 전역 필터를 인스턴스화할 때 HttpAdapter 참조를 삽입하는 것입니다.:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
두 번째 방법은 여기에 표시된 대로 APP_FILTER 토큰을 사용하는 것입니다.
원문: https://docs.nestjs.com/exception-filters
'Nest.JS > Docs' 카테고리의 다른 글
[Nest.js] Middleware (0) | 2024.09.05 |
---|---|
[Nest.js] Modules (1) | 2024.09.04 |
[Nest.js] Providers (0) | 2024.09.04 |
[Nest.js] Controllers (0) | 2024.09.04 |
[Nest.js] First steps (0) | 2024.03.19 |