Skip to content

NestJS 예외에 custom status code를 적용해보자

Dohyeon Han edited this page Dec 5, 2022 · 1 revision

프로젝트 기획 당시 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해야 했습니다. 즉 클래스화 혹은 상수화가 필요했습니다.

  1. CustomException을 상속 받는 개별 오류 클래스로 만든다.
export class UserNotFound extends CustomException {
  constructor() {
    super(20000, '유저를 찾을 수 없습니다.', HttpStatus.BAD_REQUEST);
  }
}

가장 먼저 떠오른 방식입니다. built in exception처럼 각각의 오류 상황을 class로 만드는 것이었습니다. 다른 예외와 같이 객체를 생성한다는 것에서 사용에 통일성을 주기는 하지만 불필요하게 코드의 양이 많아졌습니다.

  1. 배열로 만든다.
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 입니다."
}

얼리버드

프로젝트

개발일지

스프린트 계획

멘토링

데일리 스크럼

데일리 개인 회고

위클리 그룹 회고

스터디

Clone this wiki locally