서론
노드로 전향한지 만 1개월이 되었다.
현재 근무하고 있는 곳의 애플리케이션 코드를 보면, 따로 예외처리를 해주는 부분이 없어 에러 핸들링과 에러 로깅 작업을 커스텀으로 진행하려고 한다. 이를 위해 공식 문서를 활용해가며 학습할 필요를 느껴서 공식문서에 해당 내용을 확인하고 학습해보자.
NestJS의 예외 처리
Nest에는 애플리케이션 내에서 처리되지 않은 모든 예외를 처리하는 내장 예외 레이어가 존재한다. 애플리케이션 코드에서 예외 처리가 되지 않으면 예외 레이어에서 예외를 처리한다.
기본적으로 이 작업은 HttpException유형과 하위 클래스의 예외를 처리하는 내장 전역 예외 필터에 의해 수행된다. 예외를 인식할 수 없는 경우 다음과 같은 500에러를 내보낸다.
{
"statusCode": 500,
"message": "Internal server error"
}
HttpException
Nest는 @nestjs/common의 HttpException을 제공하며 이를 통해 다음과 같이 403에러에 대한 예외처리를 할 수 있다.
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
{
"statusCode": 403,
"message": "Forbidden"
}
기본 형태의 error response JSON을 재정의할 수 있다.
단 statusCode는 유효한 상태 코드여야한다.
@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"
}
@nestjs/common의 HttpStatus Enum을 사용하면 간편하게 상태코드를 정의할 수 있다.
Custom Exception
커스텀 Exception을 생성해야 하는 경우, HttpException에서 상속되는 예외 계층을 만드는 것이 좋다. 아래의 예시처럼 사용한다면, Nest가 사용자의 커스텀 Exception을 인식하고 오류 응답을 자동으로 처리한다.
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
@Get()
async findAll() {
throw new ForbiddenException();
}
또한 아래와 같은 내장 HttpException을 제공한다.
Exception Filter
Nest는 앞에서 언급한 것 처럼, 예외 필터 레이어가 있어서 원하는 대로 예외를 다룰 수 있다.
로그를 남기거나, 에러 메세지를 커스터마이징 할 수 있다.
아래의 예시는, 상태 코드 뿐 아니라, 에러 발생 시각과 에러가 발생된 요청 URL을 반환한다.
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(HttpExceptioin) 데코레이터는 필요한 메타데이터를 Exception Filter에 바인딩하여 Nest에게 이 특정 필터가 위의 내장 HttpException 유형의 예외를 찾고 있으며, 다른 예외는 없음을 알려준다. 또한 쉼표로 구분지어 여러 유형의 예외에 대한 필터를 한 번에 설정할 수 있다.
모든 Custom Exception Filter는 일반 ExceptionFilter interface를 구현해야 한다.
위 Custom Filter에 구현된 catch 메서드를 제공해야 한다는 뜻이다.
또한 @nestjs/platform-fastfy를 사용하는 경우, response.json() 대신 response.send()를 사용할 수 있다.
필터는, @UseFilters 데코레이터를 이용하여 적용시켜주면 되는데, 이 때 원하는 엔드포인트에 사용할 수 있다.
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
@Controller('user')
@UseFilters(HttpExceptionFilter)
export class UserController
애플리케이션 전역에서 작용하는 Filter로 설정하기 위해서는 main.ts 파일 안에서 다음과 같이 useGlobalFilters() 메서드를 사용한다.
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 {}
사용해보기
사내 백엔드 환경과 동일하게 셋팅한 후, 가볍게 복리계산 로직을 만들고, React로 간단하게 View를 만들어서 확인해보았다.
전역에서 사용할 것이기 때문에 위 포스팅의 전역 설정을 모두 마친 뒤, 아래처럼 코드를 작성했다.
async calculate(price: number, percent: number, time: number, type: string) {
if (time <= 0 || price <= 0) return null;
let priceArr = [price];
let data = new Array<CPResponse>();
let totalRevenue = 0;
for (let i = 1; i <= time; i++) {
const beforePrice = +priceArr[i - 1];
const interest = beforePrice * (+percent / 100);
const currentPrice = Math.round(beforePrice + interest);
const revenue = currentPrice - beforePrice;
const totalPercent = ((currentPrice - price) / (price / 100)).toFixed(2);
totalRevenue += revenue;
if (revenue === Infinity || !Number(revenue) || currentPrice === Infinity) {
throw new BadRequestException();
}
priceArr.push(currentPrice);
const cpResponse = new CPResponse(i + type, revenue, currentPrice, totalPercent + '%');
data.push(cpResponse);
}
return { data, totalRevenue };
아래 부분에서, 숫자가 기하급수적으로 커질 때 Infinity 값이 출력될 수 있으므로, 수의 범위를 벗어나면(?) BadRequest를 반환하도록 했다.
if (revenue === Infinity || !Number(revenue) || currentPrice === Infinity) {
throw new BadRequestException();
}
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!