ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • NestJs TypeOrm Exception
    공부하기/node.js 2023. 2. 24. 20:09

    소프트웨어를 개발하면서 예외처리는 필수 사항이다.

    만약 예외 상황이 발생할 경우 프로그램이 강제로 종료될 수 있어 사용자에게 나쁜 경험을 제공하며 심각할 경우 데이터 손실을 초래할 수 있다.

    예외 처리를 통해 발생한 오류 메시지를 적절하게 처리하면, 디버깅과 오류 추적이 용이해지고 오류가 발생한 원인과 위치를 파악할 수 있다.

    또 예기치 않은 입력이나 상황에 대응할 수 있어 안정성을 높이고 예외 상황에 대한 처리 방법을 명시적으로 나타내 코드의 가독성을 높여준다.

     

    목표

    • 예측 가능한 에러 httpException 처리
      • Http Response 값 체크 후 에러 처리
    • query error 전부 핸들링
      • 데이타베이스 에러는 전부 500 에러로 처리
    • 개발자의 실수로 발생한 서버에러 핸들링
      • 개발자의 실수로 발생하는 타입 에러 처리

    예측 가능한 에러 httpException 처리

    1. 추상클래스 ErrorController를 선언하고 공통 함수를 사용하는 컨트롤러에서 extends 받아서 validation 함수를 사용한다.

    import {
      BadRequestException,
      InternalServerErrorException,
    } from '@nestjs/common';
    import { QueryResult } from 'typeorm';
    
    export abstract class ErrorController {
      isEmptyArray(arr: Array<object>, msg: string): void {
        // 파라메터가 배열 타입이 아닌 경우는 repository에 문제일 가능성이 매우 높다.
        if (!Array.isArray(arr)) throw new InternalServerErrorException();
        if (arr.length === 0) throw new NotFoundException(msg);
      }
    
      isEmpty(obj: object, msg: string) {
        if (!obj) throw new NotFoundException(msg);
      }
    }

    2. 상속받은 컨트롤러에서 결과값을 체크한 후 리턴한다.

    ...
    
    @Controller('survey')
    export class SurveyController extends ErrorController {
      constructor(
        ...
      ) {
        super();
      }
    
      @Get('find')
      async findAll(
        @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
        @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number,
        @Query(
          'sort',
          new DefaultValuePipe(SORT_OPTION.ASC),
          new ParseEnumPipe(SORT_OPTION),
        )
        sort: SORT_OPTION,
      ) {
        const result = await this.findAllSurveyInPort.execute({
          page,
          size,
          sort,
        });
        this.isEmptyArray(result, '설문지가 존재하지 않습니다.');
        return result;
      }
    
      @Get('find/:id')
      async findOne(@Param('id', ParseIntPipe) id: number) {
        const result = await this.findOneSurveyInPort.execute(id);
        this.isEmpty(result, `id: ${id} 설문지를 찾을 수 없습니다.`);
        return result;
      }
    
      @Get('/search')
      async search(
        @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
        @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number,
        @Query(
          'sort',
          new DefaultValuePipe(SORT_OPTION.ASC),
          new ParseEnumPipe(SORT_OPTION),
        )
        sort: SORT_OPTION,
        @Query('sort', new DefaultValuePipe(''))
        keyword: string,
      ) {
        const result = await this.searchSurveyInPort.execute({
          keyword,
          page,
          size,
          sort,
        });
        this.isEmptyArray(result, '설문지가 존재하지 않습니다.');
        return result;
      }
    
      @Post('create')
      async create(@Body() surveyCreateDto: SurveyCreateDto) {
        const result = await this.createSurveyInPort.execute(surveyCreateDto);
        this.isEmpty(result, `설문지 생성에 실패했습니다.`);
        return result;
      }
    
      @Patch('update/:id')
      async update(
        @Body() surveyUpdateDto: SurveyUpdateDto,
        @Param('id', ParseIntPipe) id: number,
      ) {
        const result = await this.updateSurveyInPort.execute({
          ...surveyUpdateDto,
          id,
        });
        this.isEmpty(result, `id: ${id} 설문지를 찾을 수 없습니다.`);
        return result;
      }
    
      @Delete('delete/:id')
      async delete(@Param('id', ParseIntPipe) id: number) {
        const result = await this.deleteSurveyInPort.execute(id);
        return result;
      }
    }

     

    테스트

    [설문지 findone / DB에 저장된 id값 요청]
    
    {
        "id": 1,
        "created_at": "...",
        "updated_at": "...",
        "name": "...",
        "description": "..."
    }
    
    [설문지 findone / DB에 저장되지 않은 id값 요청]
    
    {
        "statusCode": 400,
        "message": "id: 13409583094 설문지를 찾을 수 없습니다.",
        "error": "Not Found"
    }
    
    [설문지 findall, search / 존재하는 page 요청]
    [
    	{
            "id": 1,
            "created_at": "...",
            "updated_at": "...",
            "name": "...",
            "description": "..."
    	},
        ...
    ]
    
    [설문지 findall, search / 존재하지 않는 page 요청]
    {
        "statusCode": 400,
        "message": "설문지가 존재하지 않습니다.",
        "error": "Not Found"
    }

     

    • [설문지 findone / DB에 저장된 id값 요청] : 정상적으로 설문지 리턴
    • [설문지 findone / DB에 저장되지 않은 id값 요청] : 의도한대로 NotFoundException 리턴
    • [설문지 findall, search / 존재하지 않는 page 요청] : 의도한대로 NotFoundException 리턴
    • [설문지 findall, search / 존재하는 page 요청] : 정상적으로 설문지 배열 리턴

     

    개발자의 실수로 발생한 서버에러 핸들링

    1.  TypeExceptionFilter 생성

    import {
      ExceptionFilter,
      Catch,
      ArgumentsHost,
      HttpException,
      InternalServerErrorException,
    } from '@nestjs/common';
    import { Request, Response } from 'express';
    
    @Catch(TypeError)
    export class TypeExceptionFilter implements ExceptionFilter {
      catch(exception: Error, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
        response.status(500).json({
          statusCode: 500,
          message: '서버 에러',
          error: 'Internal Server Error',
        });
      }
    }

    2. main.ts -> 글로벌 필터 적용

    ...
    import { TypeExceptionFilter } from './common/filter/type-exception.filter';
    
    async function bootstrap() {
    ...
      app.useGlobalFilters(new TypeExceptionFilter());
      ...
    }
    bootstrap();

    3. 의도적으로 타입 에러를 발생시켜 테스트 진행

      @Get('find/:id')
      async findOne(@Param('id', ParseIntPipe) id: number) {
        // @ts-ignore
        console.log(null.test());
     ...
      }

     

    테스트

    {
        "statusCode": 500,
        "message": "서버 에러",
        "error": "Internal Server Error"
    }

    -> 의도적으로 타입 에러를 발생시킨 엔드포인트에 접근할 시 TypeExceptionFilter 정상 동작

     

    Query error 전부 핸들링

    1. TypeOrmExceptionFilter 생성

    import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
    import { Request, Response } from 'express';
    import { TypeORMError } from 'typeorm';
    
    @Catch(TypeORMError)
    export class TypeOrmExceptionFilter implements ExceptionFilter {
      catch(exception: Error, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
        response.status(500).json({
          statusCode: 500,
          message: exception.message,
          error: 'Internal Server Error',
        });
      }
    }

    2. main. ts -> TypeOrmExceptionFilter 글로벌 필터 적용

    ...
    
    async function bootstrap() {
     ...
     	app.useGlobalFilters(new TypeOrmExceptionFilter(), new TypeExceptionFilter());
     ...
    }
    bootstrap();

    3. 의도적으로 타입 에러를 발생시켜 테스트 진행

    자신이 사용하는 레파지토리중 테스트할 레파지토리를 선택한 후 일부러 데이터베이스 에러를 발생시킴
    
    execute(
        params: SurveyFindOneOutPortInputDto,
      ): Promise<SurveyFindOneOutPortOutputDto> {
        return this.surveyRepo.findOne({
          where: {
            id: params,
            //@ts-ignore
            존재하지않는컬럼: 123,
          },
        });
      }

     

    테스트

    {
        "statusCode": 500,
        "message": "Property \"존재하지않는컬럼\" was not found in \"Survey\". Make sure your query is correct.",
        "error": "Internal Server Error"
    }

    -> 의도적으로 타입 에러를 발생시킨 엔드포인트에 접근할 시 TypeOrmExceptionFilter 정상 동작

     

     

    NestJs Filter 탐색 결과

    1. 내가 의도한 특정 에러만 컨트롤 할 수 있다.
    2. 애플리케이션에서 발생하는 모든 에러를 일관된 타입으로 리턴할 수 있다.
    3. 런타임에서 예측할 수 없는 exception까지 컨트롤 할 수 있다.

     

Designed by Tistory.