티스토리 뷰
NestJS에 Exception filters를 추가합니다.
서론
애플리케이션에서 발생하는 에러는 관련 정보를 포함하도록 정의하는 것이 좋습니다.
그래야 어떤 요청에서 어떤 에러가 나타났는지 정확히 확인하고 그에 맞는 대응을 할 수 있을테니까요.
NestJS Exception
NestJS에서는 기본적으로 내장된 전역 예외 필터를 제공하고 있습니다. @nestjs/common 패키지를 통해 사용할 수 있죠.
전역 예외 필터는 HttpException, 즉 애플리케이션 레벨에서 발생하는 에러를 다룹니다.
아래와 같은 에러를 작성하고 호출하면,
import { ForbiddenException } from '@nestjs/common';
async throwException() {
throw new ForbiddenException();
}
다음과 같은 응답을 받을 수 있습니다.
{
"statusCode": 403,
"message": "Forbidden"
}
인식할 수 없는 에러(HttpException도 아니고, HttpException를 상속받지도 않은 에러)의 경우에는 `Internal server error(500)`로 표현됩니다.
{
"statusCode": 500,
"message": "Internal server error"
}
기존에 지원되는 메시지만으로는 표현할 수 없는 다양한 예외 상황이 존재할 수 있겠죠?
때문에 커스텀도 지원하고 있습니다.
다음과 같이 HttpException 클래스를 이용해 원하는 메세지와 상태코드로 에러를 날리는 것이 가능합니다.
이를 이용하면 본인의 비지니스 로직에 맞는 상태코드와 메시지를 정의해서 사용할 수도 있습니다.
async throwException() {
throw new HttpException('Api is not found', 404);
}
{
"statusCode": 404,
"message": "Api is not found"
}
하지만 각 예외 상황마다 일일히 메세지나 상태코드를 작성하는건 번거로운 일입니다.
그리고 상태코드나 메시지만으론 에러가 발생 이유를 한눈에 파악하기가 힘들 것 같아요.
앞서 언급했다 싶이 에러는 많은 정보를 가지고 있는게 좋으니까요.
재사용성도 높이고, 다른 정보도 나타내도록 메시지를 커스텀 할 수 있으면 좋을것 같습니다. 🤔
Exception을 별도의 파일로 분리하기
재사용성을 높이기 위한 간단한 방법은 별도의 파일, 클래스로 분리한 뒤 import해 사용하는 것입니다.
앞서 작성한 오류를 별도의 파일 `test.exception.ts`으로 분리하겠습니다.
//test.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class TestNotFoundException extends HttpException {
constructor() {
super('Api is not found', HttpStatus.NOT_FOUND);
}
}
HttpException 클래스를 상속받아서 생성자에 메시지와 사용할 상태코드를 명시해주었습니다.
이제 로직에 클래스를 선언해주기만 하면 커스텀한 메시지와 상태코드가 담긴 에러를 사용할 수 있습니다.
import { TestNotFoundException } from 'src/exceptions/test.exception';
async throwException() {
throw new TestNotFoundException();
}
Exception filters
NestJS에서는 ExceptionFilter클래스를 상속받아 전역 예외 필터를 확장할 수 있도록 지원하고 있습니다.
이를 이용해 응답결과를 커스텀 하는것도 가능합니다.
에러 응답에 경로(path)와 발생 시간(timestamp)를 추가해보겠습니다.
`http-exception.filter.ts`를 아래와 같이 작성합니다.
import { Request, Response } from 'express';
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
@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,
message: exception.message,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}
에러 발생 시 정의된 json이 반환됩니다. 에러에 정의된 상태코드와 메세지는 그대로 사용하고, 경로에는 요청이 발생한 url이, 발생시간은 Date 클래스를 통해 생성된 시간 문자열이 입력됩니다.
필터를 전역적으로 적용하기 위해 `main.ts`에 `app.useGlobalFilter`를 정의한 `HttpExceptionFilter`로 지정합니다.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = 3000;
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(port);
}
bootstrap();
이제 오류 발생시 아래와 같이 경로와 발생시간이 함께 출력됩니다. 이러한 형식은 로깅(logging)과 디버깅에도 유용합니다. ⭐
{
"statusCode": 404,
"message": "Api is not found"
"path": "/test",
"timestamp": "2024-03-20T12:34:42.441Z"
}
class-validator 메세지가 나타나지 않는 경우
class-validator로 dto의 유효성 검사를 하는 경우, 위의 exception filter를 적용하면 기존의 메시지가 나타나지 않고`Internal server error(500)`로 나타나게 됩니다.
스택오버플로우에서 저와 같은 문제점을 겪는 글을 발견했습니다.
댓글에는 private로 설정되어 있으니 `exception["response"]["message"]`으로 우회해 접근하라고 나오라고 되어있네요.
음...일단은 구조를 좀 더 파악해 봅시다.
exception에 getResponse() 메소드가 정의 되어있어 이를 이용해 response를 가져와 보았습니다.
let errorMessage = exception.getResponse();
errorMessage의 타입이 `string | object` 인것을 보니 message가 단일로 넘어올때도 있고,
다른 부가 정보와 함께 객체로 넘어오는 경우도 있는것 같습니다.
실제 출력값은 아래와 같습니다.
{
"statusCode": 404,
"message": {
"message": [
"name should no to be empty",
"name must be a string value"
],
"error" : "Bad Request"
"statusCode": 400
},
"path": "/test",
"timestamp": "2024-03-20T12:34:42.441Z"
}
class-validator 자체 응답폼인 것 같습니다. message가 배열로 넘어오고 있었네요.
간단하게 obj 인지만 확인해 메세지를 꺼내도록 수정해주었습니다.
import { Request, Response } from 'express';
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
@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();
let errorMessage: any = exception.getResponse();
if (typeof errorMessage === 'object') {
errorMessage = errorMessage.message;
}
response.status(status).json({
statusCode: status,
message: errorMessage,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}
이제 class-validator의 에러메시지가 fillter를 통과하더라도 잘 나타나게 됩니다.
감사합니다.
공부한 내용을 복습/기록하기 위해 작성한 글이므로 내용에 오류가 있을 수 있습니다.
'TS | NestJS' 카테고리의 다른 글
[NestJS] Logging Interceptor 추가하기 (0) | 2024.03.23 |
---|---|
[TS] extends와 implements (0) | 2024.03.22 |
[NestJS] Swagger 적용하기 (feat. API 문서화) (0) | 2024.03.15 |
[TS] DeepMerge 타입 구현해보기 (0) | 2024.03.06 |
[TS] 타입이 추론되는 String.prototype.split - 2 (0) | 2024.03.05 |