이 글은 NestJS Exception Filter 공식 문서를 기반으로, 타이핑하며 배우는 의미로 쓰는 글입니다.
Nest는 모든 unhandled exceptions 를 exceptions layer 에서 처리한다. 만약 우리의 코드에서 예외가 처리되지 않았다면, 이 layer 에서 걸려서, 자동으로 유저에게 친숙한 response 를 보내준다.
이것은 별도의 설치 없이 내장된 global exception filter 에 의해 이루어진다. 이는 HttpExecution
타입의 예외들을 처리해준다. 예외가 인식되지 않았을 때 자동으로
{
"statusCode": 500,
"message": "Internal server error"
}
을 보내준다.
대부분의 경우에서, custom exception을 쓸 필요가 없다. Nest 에 내장된 HTTP Exception 을 사용하면 된다. 만약 정말 필요하다면, HttpExcetion
클래스로부터 상속받는 것이 좋다.
//foridden.exception.ts
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
ForbiddenException
이 HttpException
을 상속하기 때문에, 내장 exception handler와 함께 원활하게 돌아가고, 그러므로
@Get()
async findAll() {
throw new ForbiddenException();
}
처럼 쓰일 수 있다.
비록 내장된 exception filter 가 자동으로 많은 예외를 처리해주지만, exceptions layer를 완전히 제어하고 싶을 수 있다. 예를 들면, log를 추가해주고 싶다거나, 여러 동적인 요인을 기반으로 다른 JSON schema를 사용하고 싶을 수 있다. 이런 목적을 위해 고안한 것이 Exception filter 이다. 이는 Client에게 보내지는 응답의 내용이나 제어의 정확한 flow를 제어할 수 있게 한다.
//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,
});
}
}
먼저 @Catch(HttpException)
데코레이터는, 이 filter가 HttpException에 해당하는 예외가 있는지 찾는다. @Catch()
의 파라미터는 하나일 수도 있고, 콤마로 구분되어 여러 개를 한번에 검사할 수도 있다.
catch() 내의 두 파라미터를 살펴보자. exception
은 현재 처리되고 있는 exception object 이다. host
파라미터는 ArgumentHost
오브젝트이다. 위의 코드에서, 우리는 이것을 원래의 request handler에서 통과된 Request
와 Response
오브젝트들의 레퍼런스를 가져오기 위해서 사용했다.
여기까지 읽고 ArgumentHost
가 왜 사용된지는 알았지만, 이게 정확히 뭔지는 모르겠다. 원문에서도, 모든 곳에서 underlying arguments
들을 가져올 수 있는 ArgumentsHost
와 그의 기능들의 강력함을 간략히 설명하고 있다. 일단은 넘어가도록 하자.
위에서 새로 만든 HttpExceptionFilter
를 적용하는 방법은
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
이다. 역시 콤마를 이용하여 여러 가지의 exception filter 를 적용할 수 있다. 위의 코드는 new HttpExceptionFilter()
로 인스턴스를 만들었다. 하지만 대신에, class를 instance 대신 넣으면, 인스턴스화의 책임을 회피하면서, 의존성 주입 을 가능하게 할 수 있다.
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
이렇게 말이다. 아래처럼 클래스를 적용하는 방식이 가능하면 좋다. 메모리 사용을 줄여준다고 한다. 컨트롤러 전체에 적용하려면,
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
이렇게 적용해주면 된다. 모든 곳에 적용되는 필터를 만드려면? 당연히 main.ts 에 적용하면 된다.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
useGlobalFilters()
메소드는 게이트웨이나 하이브리드 어플리케이션에 적용되지 않는다고 한다.
하지만 이렇게 main.ts 에서 하는 것은 의존성 주입이 불가능해진다. module 의 context 내에 있지 않기 때문이다. 이 문제를 해결하기 위해, module
파일에서 적용하는 것을 권장한다고 한다.
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
만약 모든 unhandled exception을 catch 하고 싶다면, 파라미터를 비운채 Catch()
데코레이터를 사용하면 된다.
일반적으로, 당신은 당신의 applicatoin requirements를 완전히 수행할 수 있는, 완전히 customize 된 exception filter를 만들 것이다. 그러나, 당신이 단순하게 내장된 global exception filter 를 extend 하고, 특정 상황에 따라 어떤 기능을 override 하고 싶을 수 있다.
base filter에 예외 처리를 위임하기 위해서, BaseExceptionFilter
를 extend 한 뒤 상속된 catch()
메소드를 call해야 한다.
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);
}
}
위는 단지 접근법을 시연한 것 뿐이다. 실제로는 당신의 business logic 에 맞추어서 실행되어야 할 것이다.
이 포스팅을 쓰면서 Exception Filters 에 대하여 어느 정도 감을 잡을 수 있었다. 완전한 예외의 처리를 위해서 사용될 수 있을 것이다.