-
Notifications
You must be signed in to change notification settings - Fork 0
NestJS 예외에 custom status code를 적용해보자
프로젝트 기획 당시 custom status code가 있으면 어떤 오류인지에 따라 클라이언트에서 분기 처리를 쉽게 할 수 있을 것이라고 판단했습니다.
NestJS에는 built-in-exception이 존재합니다. 코드 내에서 여러 exception 중 하나를 throw하면 아래와 같이 데이터를 받을 수 있습니다.
// NotFoundException
{
"statusCode": 404,
"message": "Not Found"
}
built-in-exception은 HttpException을 상속 받고 있고, 해당 예외들은 exception filter를 거치게 됩니다. 하지만 여기서 문제가 발생했습니다.
built-in-exception이나 HttpException 객체를 생성할 때는 200, 404와 같은 이미 지정되어있는 HttpStatus값을 할당해야 한다는 것입니다. 따라서 기존 statusCode자리에는 custom status code가 들어갈 수 없습니다. 이를 해결하기 위해서 custom exception filter를 적용했습니다.
/**
* 모든 exception을 처리하는 필터
* throw 된 것인 ErrorInfo, HttpException 혹은 그 외 타입인지 판단하여 CustomException 생성 후 응답 객체로 변환
*/
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res = ctx.getResponse<Response>();
const req = ctx.getRequest<Request>();
// throw된 모든 exception들은 customException으로 바뀐다.
const customException = this.getCustomException(exception);
const response = customException.getErrorResponse();
const log = {
timestamp: new Date(),
url: req.url,
response,
};
console.log(log);
res.status(customException.getStatus()).json(response);
}
...
}
또한 custom exception class를 다음과 같이 정의했습니다.
export class CustomException extends HttpException {
private readonly code: number;
constructor(
code: number,
message: string | Record<string, any>,
statusCode: number,
) {
super(message, statusCode);
this.code = code;
}
getCode(): number {
return this.code;
}
getErrorResponse(): ErrorResponse {
return {
code: this.code,
message: this.getResponse(),
};
}
}
해당 필터를 main.ts의 bootstrap()에서 적용시켜 줍니다.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter()); // 전역 필터 적용
...
}
이제 코드 내부에서 throw를 하면 원하는 대로 에러 메세지를 내보내는 것이 가능해졌습니다.
여기서 또 하나 고민이 생겼습니다. 같은 오류에 대해서 같은 값으로 CustomException을 throw해야 했습니다. 즉 클래스화 혹은 상수화가 필요했습니다.
- CustomException을 상속 받는 개별 오류 클래스로 만든다.
export class UserNotFound extends CustomException {
constructor() {
super(20000, '유저를 찾을 수 없습니다.', HttpStatus.BAD_REQUEST);
}
}
가장 먼저 떠오른 방식입니다. built in exception처럼 각각의 오류 상황을 class로 만드는 것이었습니다. 다른 예외와 같이 객체를 생성한다는 것에서 사용에 통일성을 주기는 하지만 불필요하게 코드의 양이 많아졌습니다.
- 배열로 만든다.
export const errors: { readonly [key: string]: ErrorInfo } = {
INTERNER_ERROR: [
99999,
'내부 오류가 발생했습니다.',
HttpStatus.INTERNAL_SERVER_ERROR,
],
INVALID_ID: [20001, '유효하지 않은 ID 입니다.', HttpStatus.BAD_REQUEST],
}
이 방식은 1번 방식보다 간결하게 예외를 정의할 수 있었고 저희가 사용하고 있는 방식입니다. 저희는 이 배열값을 그대로 throw하여 위에서 정의한 HttpExceptionFilter에서 CustomException 객체로 만들어주는 방식으로 구현했습니다. 객체 생성을 HttpExceptionFilter에서 해주기 때문에 코드 내에서 new CustomException(...errors.INVALID_ID)와 같이 따로 객체를 생성하지 않고 errors.INVALID_ID를 바로 throw를 해줄 수 있습니다.
물론 built in exception과 사용법의 통일성이 없다는 단점도 있다고 생각하지만, new CustomException(...errors.INVALID_ID)와 같이 사용해도 정상적으로 동작하도록 HttpExceptionFilter에서 로직을 구성하였고 팀 내에서 사용 규칙만 정하면 된다고 생각하여 큰 문제는 아니라고 판단했습니다.
HttpExceptionFilter에서 배열 타입을 받을 수 있도록 수정해줍니다.
export type ErrorInfo = [number, string, HttpStatus];
-----
catch(exception: Error | ErrorInfo, host: ArgumentsHost)
이제 throw errors.INVALID_ID와 같이 사용하면 다음과 같은 결과를 받을 수 있습니다.
// 400 Bad Request
{
"code": 20001,
"message": "유효하지 않은 ID 입니다."
}
- 📃 기획서
- 📂 Backlog
- 📊 ERD, 폴더 구조
- 🗓️ 회의록