Exception

장현욱(Artlogy)·2022년 11월 17일
1

Nest.js

목록 보기
12/18
post-thumbnail

Exception

개발한지 얼마 되지 않은 주니어 개발자들이 가장 많이 실수하는게, 예외처리를 꼼꼼히 하지 않는다는 점이다. 모든것은 예외가 발생할 수 있고 당장 잘되던 것도 사용자가 늘어나거나 유지보수가 진행됌에 따라 얼마든지 예외가 발생하는 법이다. 따라서 예상되거나 발생하면 치명적인 예외는 꼼꼼히 작성해주는 것이 좋다.


Nest Exception

throw new HttpException(
  {
    errorMessage: 'id는 0보다 큰 정수여야 합니다',
    foo: 'bar'
  },
  HttpStatus.BAD_REQUEST
);
{
    "errorMessage":"id는 0보다 큰 정수여야 합니다",
    "foo":"bar"
}

NestJS에서는 위 처럼 Exception을 직접 정의해서 전달해도 되지만, 표준 예외들 또한 정의되어 있어 간편하게 쓸 수 있다. 자주 사용하는건 ★를 메모해두었다.

  • BadRequestException★
  • UnauthorizedException★
  • NotFoundException★
  • ForbiddenException★
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException★
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException★
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

Nest기본 예외의 구조를 보면 다음과 같은 형태를 가진다.

export class BadRequestException extends HttpException {
    constructor(
    objectOrError?: string | object | any,
    description = 'Bad Request',
  ) {
    super(
      HttpException.createBody(
        objectOrError,
        description,
        HttpStatus.BAD_REQUEST,
      ),
      HttpStatus.BAD_REQUEST,
    );
  }
}

생성자를 보면 2개의 인자를 받는데 다음처럼 메세지와 error를 변환해서 출력할 수 있다.

throw new BadRequestException('id는 0보다 큰 정수여야 합니다', 'id format exception');
{
    "statusCode": 400,
    "message": "id는 0보다 큰 정수여야 합니다",
    "error": "id format exception"
}

Exception Filter

Nest에서 제공하는 기본 예외 필터외에 직접 예외 필터를 두어 원하는 대로 예외를 다루어 볼 것이다.

다음코드는 HttpException이 아닌 알 수 없는 예외를 처리하기 위한 필터로 예외 발생시 예외가 발생한 url, 시간, 응답객체를 콘솔에 출력하는 필터를 만든 것이다.

import { MyLogger } from '@config/logger.config';
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  Inject,
  InternalServerErrorException,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const req: Request = ctx.getRequest<Request>();
    const res: Response = ctx.getRequest<Response>();

    if (!(exception instanceof HttpException)) {
      exception = new InternalServerErrorException();
    }

    const response = (exception as HttpException).getResponse();

    const log = {
      timestamp: new Date(),
      url: req.url,
      response,
    };

    console.log(log, req.baseUrl);

    res.status((exception as HttpException).getStatus()).json(response);
  }
}

핵심은 @Catch()데코레이더이다. Catch()는 처리되지않은 예외를 잡으려고 할 때 사용한다. 우리가 다루는 대부분의 예외는 HttpException을 상속받은 클래스로 제공된다.

필터 적용

라우터 단위

@Controller('users')
export class UsersController {
    ...

  @UseFilters(HttpExceptionFilter)
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
    ...
}

컨트롤러 단위

@Controller('users')
@UseFilters(HttpExceptionFilter)
export class UsersController {
  ...
}

전역 범위

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter()); // 전역 필터 적용
  await app.listen(3000);
}

예외필터는 모듈(main.ts)외부에서 이루어 지기 때문에 의존성을 주입 받을려면 예외 필터를 커스텀 프로바이더로써 등록해야한다.

import { dataBaseConfig } from '@config/database.config';
import { Module } from '@nestjs/common';
import { AccountModule } from './account/account.module';
import { FileModule } from './file/file.module';
import { CustomConfigModule } from './config.module';
import { AuthGuard } from '@config/guard/jwt.guard';
import { HttpExceptionFilter } from '@config/filters/exception.filter';

@Module({
  imports: [CustomConfigModule, AccountModule, FileModule],
  providers: [
    AuthGuard,
    { provide: 'EXCEPTION_FILTER', useClass: HttpExceptionFilter },
  ],
  exports: [],
})
export class AppModule {}
import { MyLogger } from '@config/logger.config';
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  InternalServerErrorException,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: MyLogger) {}
  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const req: Request = ctx.getRequest<Request>();
    const res: Response = ctx.getRequest<Response>();

    if (!(exception instanceof HttpException)) {
      exception = new InternalServerErrorException();
    }

    const response = (exception as HttpException).getResponse();

    const log = {
      timestamp: new Date(),
      url: req.url,
      response,
    };

    this.logger.error(log, req.baseUrl);

    res.status((exception as HttpException).getStatus()).json(response);
  }
}

이제 일부러 500ERROR를 만들면 다음과 같이 커스텀 예외 필터가 로그에 출력된다.

  @Get('/error/:foo')
  @UseFilters(HttpExceptionFilter)
  @ApiOperation({
    summary: '에러 발생기',
    description: '예외 필터의 테스트를 위해 만듬',
  })
  error(foo: any): string {
    return foo.bar(1);
  }

0개의 댓글