nest의 공식문서를 토대로 작성합니다.
Nest에는 처리되지 않은 모든 예외를 처리하느 예외 계층이 내장되어 있습니다. 예외가 애플리케이션 코드에서 처리되지 않으면 이 계층에서 예외를 포착하여 적절한 사용자 친화적인 응답을 자동으로 전송합니다.
기본적으로 이 작업은 내장된 전역 예외 필터에 의해 수행되며 이 필터는 HttpException
유형(및 그 하위 클래스)의 예외를 처리합니다. 예외가 인식되지 않는 경우 기본 제공 예외 필터는 다음과 같은 기본 JSON 응답을 생성합니다.
{
"statusCode": 500,
"message": "Internal server error"
}
HINT
전역 예외 필터는 부분적으로http-error
라이브러리를 지원함. 기본적으로statusCode
및 메시지 속성을 포함하는 모든 예외가 올바르게 채워지고 응답으로 다시 전송됨.
Nest는 @nestjs/common
패키지에서 노출되는 기본 제공 HttpException
클래스를 제공합니다. 일반적인 HTTP REST/GraphQL API 기반 애플리케이션의 경우 특정 오류 조건이 발생할 때 표준 HTTP 응답 객체를 전송하는 것이 가장 좋습니다.
예를 들어 CatsController
에는 findAll()
메소드가 있습니다. 이 route handler가 모종의 이유로 예외를 던진다고 가정합시다.
이를 위한 하드코딩:
# cats.controller.ts
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
HINT
여기서는HttpStatus
를 사용. 이것은@nestjs/common
패키지에서 가져온 열거형 헬퍼.
클라이언트가 이 엔드포인트를 요청하면 응답은 이렇게 될 것입니다.
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException
생성자는 응답을 결정하는 두 개의 필수 인수를 받습니다.
response
: JSON 응답 body를 정의. 문자열 또는 객체.status
: HTTP 상태 코드를 정의.기본적으로 JSON 응답 body는 두 개의 속성을 가집니다.
statusCode
: status
인수에 제공된 HTTP 상태 코드.message
: status
에 따른 HTTP 오류에 대한 간단한 설명.JSON 응답 body의 message
부분만 재정의하려면 응답 인수에 문자열을 입력합니다. 전체 JSON 응답 body를 재정의하려면 응답 인수에 객체를 전달합니다. Nest는 객체를 직렬화하여 JSON 응답 body로 반환합니다.
두 번째 생성자 인자인 status
는 유효한 HTTP 상태 코드여야 합니다. 가장 좋은 방법은 @nestjs/common
에서 가져온 HttpStatus
열거형을 사용하는 것입니다.
오류의 cause
를 제공하는 데 사용할 수 있는 세 번째 생성자 인자(선택사항)인 options
가 있습니다. 이 cause
객체는 응답 객체로 직렬화되지는 않지만 로깅 목적으로 유용할 수 있으며 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"
}
많은 경우 사용자 정의 예외를 작성할 필요가 없으며 다음 섹션에 설명된 대로 기본 제공 Nest HTTP 예외를 사용할 수 있습니다. 사용자 정의 예외를 작성해야 하는 경우, 사용자 정의 예외가 기본 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();
}
그러니까 되도록 내장된 전역 예외 필터를 사용하고 사용자 정의 예외를 적용하려면 HttpException
을 확장해서 만들어라!인 것 같습니다.
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
모든 기본 제공 예외는 options
를 사용하여 오류 cause
와 오류 설명을 모두 제공할 수 있습니다.
throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })
이는 다음 응답을 얻을 수 있습니다:
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400,
}
기본 예외 필터가 많은 경우를 자동으로 처리할 수 있지만 예외 계층을 완전히 제어하고 싶을 수도 있습니다. 예를 들어 로깅을 추가하거나 일부 동적 요인에 따라 다른 JSON 스키마를 사용하고 싶을 수도 있습니다. 예외 필터는 바로 이러한 목적을 위해 설계되었습니다. 예외 필터를 사용하면 정확한 제어 흐름과 클라이언트에 다시 전송되는 응답의 내용을 제어할 수 있습니다.
HttpException
클래스의 인스턴스인 예외를 포착하고 이에 대한 사용자 정의 응답 로직을 구현하는 예외 필터를 만들어 봅시다. 이를 위해 Request
, Response
객체에 접근해야 합니다.
Request
객체에 접근해 원본 url
을 가져와 로깅 정보에 포함할 수 있습니다.
Response
객체에 접근해 response.json()
메소드를 사용하여 전송된 응답을 직접 제어할 수 있습니다.
# 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>
인터페이스를 구현해야 함. 이를 위해catch
메소드에 지정된 signature를 제공해아 함.T
는 예외의 유형을 나타냄.
WARNING
fastify
를 사용하는 경우response.json()
대신response.send()
를 사용함.
@Catch(HttpException) 데코레이터는 필요한 메타데이터를 예외 필터에 바인딩하여 이 특정 필터가 HttpException
유형의 예외만 찾고 있음을 Nest에 알려줍니다.
@Catch() 데코레이터는 단일 매개변수 또는 쉼표로 구분된 목록을 받습니다. 이를 통해 한 번에 여러 유형의 예외에 대한 필터를 설정할 수 있습니다.
catch()
메소드의 매개변수를 봅시다. exception
매개변수는 현재 처리 중인 예외 객체입니다. host
매개변수는 ArgumentsHost
객체입니다. ArgumentsHost
는 execution context 챕터에서 자세히 살펴볼 강력한 유틸리티 객체입니다. 위 코드 샘플에서는 이 객체를 사용하여 예외가 발생한 컨트롤러에서 원래 요청 handler로 전달되는 Request
및 Response
객체에 대한 참조를 얻습니다.
위 코드 샘플에서는 ArgumentsHost
의 몇 가지 헬퍼 메소드를 사용하여 원하는 Request
및 Response
객체를 가져왔습니다.
ArgumentsHost
에 대해 자세히 보려면 여기로
간단히 말하면 ArgumentsHost
객체인 host
로 Request
, Response
에 접근한다 입니다.
새로운 HttpExceptionFilter
를 CatsController
의 create()
메소드에 연결합니다.
# cats.controller.ts
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
HINT
@UseFilters()
데코레이터는@nestjs/common
패키지에서 import.
여기서는 @UseFilters()
데코레이터를 사용했습니다. @Catch()
데코레이터와 마찬가지로 단일 필터 인스턴스 또는 쉼표로 구분된 필터 인스턴스 목록을 받을 수 있습니다. 위에서는 HttpExceptionFilter
인스턴스를 생성했습니다. 또는 아래처럼 인스턴스 대신 클래스를 전달하여 인스턴스화에 대한 책임을 프레임워크에 맡기고 의존성 주입을 활성화할 수 있습니다.
# cats.controller.ts
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
해당 엔드포인트를 호출하면
이렇게 나오는 것을 볼 수 있습니다.
HINT
가능하면 인스턴스 대신 클래스를 사용하여 필터를 적용하는 것을 선호. Nest는 전체 모듈에서 동일한 클래스의 인스턴스를 쉽게 재사용할 수 있으므로 메모리 사용량을 줄일 수 있음.
위 예제에서 HttpExceptionFilter
는 단일 create()
route handler에만 적용되어 메소드 범위가 지정됩니다. 예외 필터는 메소드 범위, 컨트롤러 범위, 또는 전역 범위 등 다양한 범위를 지정할 수 있습니다. 예를 들어 컨트롤러 범위로 필터를 설정하려면:
# cats.controller.ts
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
이 구조는 CatsController
내부에 정의된 모든 route handler에 대해 HttpExceptionFilter
를 설정합니다.
전역 범위로 필터를 설정하려면:
# main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
WARNING
useGlobalFilters()
메소드는 게이트웨이 또는 하이브리드 애플리케이션에 대한 필터를 설정하지 않음.
전역 범위 필터는 모든 컨트롤러와 모든 route handler, 전체 애플리케이션에서 사용됩니다. 종속성 주입과 관련하여 모듈 외부에서 등록한 전역 필터는 모듈 컨텍스트의 외부에서 수행되므로 종속성을 주입할 수 없습니다. 이 문제를 해결하기 위해 다음 구성을 사용하여 모든 모듈에서 직접 전역 범위 필터를 등록할 수 있습니다.
# 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
이 방식을 사용하여 필터에 대한 종속성 주입을 수행할 때는 이 구조가 사용되는 모듈에 관계없이 필터가 실제로는 전역임. 이 작업은 필터가 정의된 모듈을 선택. 또한useClass
만이 사용자 지정 provider 등록을 처리하는 유일한 방법은 아님. 자세히 보려면 여기로
이 방법을 사용하여 필요한 만큼 필터를 추가할 수 있으며 각 필터를 providers 배열에 추가하면 됩니다.
처리되지 않은 모든 예외를 잡으려면(예외 유형에 관계없이) @Catch()
데코레이터의 매개변수 목록을 비우면 됩니다.
아래의 예는 HTTP 어댑터를 사용하여 응답을 전달하고 플랫폼 별 객체를 직접 사용하지 않기 때문에 플랫폼에 구애받지 않는 코드입니다.
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
모든 것을 캐치하는 예외 필터와 특정 유형에 바인딩된 필터를 결합하는 경우 특정 필터가 바인딩된 유형을 올바르게 처리할 수 있도록 모든 것을 캐치하는 필터를 먼저 선언해야 함.
일반적으로 애플리케이션 요구 사항을 충족하기 위해 완전히 사용자 정의된 예외 필터를 만듭니다. 그러나 제공되는 기본 전역 예외 필터를 간단히 확장하고 특정 요인에 따라 동작을 재정의하려는 사용 사례가 있을 수 있습니다.
예외 처리를 기본 필터에 위임하려면 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
를 확장하는 필터를 새로 인스턴스화해서는 안 됨. 대신 프레임워크가 자동으로 인스턴스화하도록 둬야 함.
위의 구현은 접근 방식을 보여주는 것일 뿐입니다. 확장 예외 필터의 구현에는 맞춤형 비즈니스 로직이 포함될 수 있습니다.
전역 필터는 기본 필터를 확장할 수 있습니다. 이 작업은 두 가지 방법 중 하나로 수행할 수 있습니다.
첫 번째 방법은 사용자 지정 전역 필터를 인스턴스화할 때 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
토큰을 사용하는 것입니다. 자세히 보려면 여기로
제 코드입니다. <> 😎
저도 그냥 막 이것 저것 적용해본 거라 깔끔하게 정리되진 않았습니다.
고생하셨습니다!
다음 글에서 만나요~~😀
저도 아직 배우는 단계입니다. 지적 감사히 받겠습니다. 함께 열심히 공부해요!!
너무 좋은 글이네요. 공유해주셔서 감사합니다.