네스트는 빌트인 예외 레이어를 기본으로 제공하며, 애플리케이션에서 발생하는 핸들링되지않은 예외를 프로세싱하는 책임이 있다. 애플리케이션 코드에서 예외가 핸들링 되지 않았다면, 이 레이어에서 이를 잡아서, 자동으로 사용자 친화적은 응답을 반환하게된다.
이 동작은 빌트인 전역 예외 필터에 의해서 일어나며, HttpException
타입의 예외들을 핸들링한다. 예외가 인지되지 않았다면(HttpException
또는 HttpException
의 자식 클래스가 아닌 경우), 빌트인 예외 필터는 다음과 같은 JSON 응답을 반환한다.
{
"statusCode": 500,
"message": "Internal server error"
}
전역 예외 필터는 부분적으로
http-errors
라이브러리를 지원한다. 기본적으로,statusCode
와message
프로퍼티를 가지는 예외는 올바르게 채워지고 응답으로 반환 될 것이다.
네스트는 @nestjs/common
패키지를 통해 빌트인 HttpException
클래스를 제공한다. 일반적인 HTTP REST/GraphQL 기반의 애플리케이션에서, 표준 HTTP 응답 객체를 특정 에러 발생시 반환하는 것이 best practive로 여겨진다.
예를 들어, CatsController
내에서, findAll()
메서드가 있고, 이 라우트에서 어떤 이유로 예외를 던진다고 생각해보자.
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
만약 클라이언트에서 이 엔드포인트로 요청을 보내면, 다음과 같은 응답을 받게된다.
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException
생성자는 두가지 필수 인자를 받아서 응답을 결정한다.
response
: JSON 응답의 바디 - string
/ object
status
: HTTP 상태 코드디폴트로, JSON 응답 바디는 두가지 프로퍼티를 가진다
statusCode
: status
값에 넘겨진 값이 디폴트message
: HTTP 에러에 관한 짧은 설명message
프로퍼티 부분을 바꾸려면, response
인자에 객체를 넣어주면된다. 네스트가 이 객체를 직렬화해서 JSON 응답 바디로 반환해준다.
두번째 인자인 status
의 경우에는 HTTP 상태 코드를 반환하면 되는데, HttpStatus
enum을 사용하는 것이 best practice다.
다음은 응답 바디의 전체를 오버라이딩하는 예다.
@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN);
}
위의 엔드포인트에 요청을 보내게되면 다음과 같은 응답을 받게된다.
{
"status": 403,
"error": "This is a custom message"
}
많은 경우에, 커스텀 예외를 작성할 필요는 없고, 그냥 네스트 HTTP 예외를 사용하면된다. 하지만 그래도 커스텀 예외를 만들어야 한다면, 자체 예외 hierarchy를 만드는 것이 추천된다. 이 커스텀 예외들은 HttpException
클래스를 상속받아야 하며, 이렇게하면 네스트가 여전히 예외를 인지하고, 적절한 응답을 반환할 수 있다.
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
ForbiddenException
이 HttpException
을 상속받기 때문에, 빌트인 예외 핸들러와 잘 동작할 것이며, findAll()
메서드에서 사용해도된다.
@Get()
async findAll() {
throw new ForbiddenException();
}
네스트는 HttpException
을 상속받는 몇가지 표준 예외를 제공한다.
BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
HttpVersionNotSupportedException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableEntityException
InternalServerErrorException
NotImplementedException
ImATeapotException
MethodNotAllowedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
PreconditionFailedException
빌트인 예외 핸들러가 많은 경우에 잘 동작하지만, 예외 레이어에 대한 풀 컨트롤을 갖길 원할 수 있다. 예를 들어, 로깅 기능을 축하하거나, 다른 JSON 스키마를 사용하는 등의 요구사항이 있을 수 있다. 예외 필터는 이 목적을 위해서 설계되었다. 이를 통해서 정확한 플로우 통제와 클라이언트로 반환할 응답을 직접 정할 수 있다.
HttpException
클래스의 인스턴스를 모두 잡는 예외 필터를 만들고, 커스텀 응답 로직을 타도록 만들어보자. 이를 위해서, 플랫폼 Request
, Response
객체에 접근할 수 있어야한다. Request
객체를 통해서 원래의 url
을 가져오고, 로깅 정보에 포함시킨다. Response
객체를 통해서 어떤 응답을 보낼지 직접 정한다.
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,
});
}
}
모든 예외 필터는
ExceptionFilter<T>
인터페이스를 구현해야한다. 이 인터페이스는catch(exception: T, host: ArgumentsHost)
메서드를 필요로한다. 여기서T
는 예외의 타입을 뜻한다.
@Catch(HttpException)
데코레이터는 예외 필터에 필요한 메타데이터를 바인딩해주며, 네스트가 어떤 예외 타입을 보고 있는지 알 수 있게 해준다. @Catch()
데코레이터는 하나 이상의 파라미터를 받으며, 여러 예외 타입에 대한 필터를 한번에 설정할 수 있도록 도와준다.
catch()
메서드에 넘겨지는 파라미터를 살펴보자. exception
파라미터는 현재 가공하고자 하는 예외 객체다. host
파라미터는 ArgumentsHost
객체로, 다음 문서에서 더 자세히 살펴볼 강력한 유틸리티 객체다.
위 예시에서는 host
파라미터를 통해서 원래 요청 핸들러에게 넘겨진 Request
와 Response
객체에 대한 레퍼런스를 받았다. ArgumentsHost
에 대한 자세한 내용은 다음 문서에서 다룬다.
ArgumentsHost
를 이런식으로 사용하는 이유는 모든 문맥(HTTP 서버, 마이크로서비스, 웹소켓 등)에서 똑같이 기능하도록 만들기 위해서다. 다음 문서에서 어떻게 적절하게 underlying arguments에 접근할 수 있는지 다룬다. 이를 통해서 모든 문맥에서 동일하게 동작하는 예외 필터를 만들 수 있다.
HttpExceptionFilter
와 CatsController
의 create()
메서드를 묶어보자.
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
위 예시에서 @UseFilters()
데코레이터를 사용했다. @Catch()
데코레이터와 유사하게, 하나 이상의 필터 인스턴스를 받을 수 있다. 위 예시에서는 HttpExceptionFilter
인스턴스를 하나 만들었으나, 클래스만 넘기고 인스턴스화는 네스트의 의존성 주입 기능에 맡겨도 무방하다.
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
가능하다면 인스턴스를 넘기기보다 클래스를 넘기는 것이 좋다. 그렇게하면 네스트가 판단해서 같은 인스턴스를 여러 모듈에서 재사용할 수 있다.
위 예시에서 HttpExceptionFilter
는 create()
라우트 핸들러에만 적용된다. 예외 필터는 다양한 레벨에서 사용할 수 있다 - 메서드, 컨트롤러, 전역. 아래 예시에서는 컨트롤러에 적용해본다.
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
이렇게 해주면 CatsController
에 있는 모든 라우트 핸들러가 HttpExceptionFilter
를 사용하게 된다.
전역 스코프 필터를 만들고 싶다면, 아래와 같이 적용한다.
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 {}
위 같은 방법을 사용할 경우, 어느 모듈에서 의존성 주입을 진행하던 항상 전역 필터가 된다는 점을 알아야한다. 해당 필터가 정의된 모듈 쪽에서 선언해주는 것이 좋다.
이 방법으로 원하는 만큼 필터를 추가할 수 있으며, 간단히 providers
배열에 추가만 해주면된다.
핸들링되지 않은 모든 예외를 잡기 위해서, @Catch()
데코레이터에게 인자를 넘겨주지 않으면 된다.
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
위 예외 필터는 클래스 상관없이 모든 예외를 잡게된다.
일반적으로 애플리케이션 요구사항 충족을 위해서 완전히 커스터마이즈된 예외 필터를 만들게 될 것이다. 하지만, 간단하게 기존 전역 예외 필터를 살짝만 확장하거나 override하고 싶을 수 있다.
예외 프로세싱을 베이스 필터에게 위임하기 위해서는, BaseExceptionFilter
를 extend하고 상속받은 catch()
메서드를 호출하면된다.
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);
}
}
BaseExceptionFilter
를 extend하는 메서드/컨트롤러 스코프 필터는new
키워드를 통해서 인스턴스화되면 안된다. 클래스를 넘기고 네스트가 알아서 하게 둬야한다.
위의 예시는 접근법을 보여주기 위함이다. 실제로 구현된 확장 예외 필터는 비즈니스 로직을 포함할 수 있다.
전역 필터도 베이스 필터를 extend 할 수 있다. 두가지 방법으로 가능하다.
첫 번째는 커스텀 글로벌 필터를 만들 때 HttpServer
레퍼런스를 주입하는 것이다.
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
토큰을 사용하는 방법이다.
비즈니스 로직 구현만큼 중요한 에러처리/예외 핸들링 부분이다. 현재까지 적용해본건 전역 필터밖에 없는데, 이쪽 스터디를 아예 안하고 적용해보다보니 거의 건드리지 못했고, 개선할 부분이 꽤 있을 것 같다.
종류별로 Exception을 제공해서 따로 외부 라이브러리 사용없이 그냥 네스트가 빌트인으로 제공하는 것들을 사용하는 것이 좋다는 생각이 든다..