NestJS Request Lifecycle (5) - Exception Filter

2023. 1. 13. 18:15ㆍBackend/NestJS

이전 κΈ€


μ˜ˆμ™Έ ν•„ν„°

NestJSμ—μ„œ μ˜ˆμ™Έ ν•„ν„°λž€ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λ ˆλ²¨μ—μ„œ μ²˜λ¦¬λ˜μ§€ μ•Šμ€ λͺ¨λ“  μ˜ˆμ™Έλ₯Ό μ‚¬μš©μž μΉœν™”μ μΈ λ°©μ‹μœΌλ‘œ μ²˜λ¦¬ν•˜λŠ” 역할을 ν•œλ‹€. λ”°λΌμ„œ μ˜ˆμ™Έ ν•„ν„°λ₯Ό μ΄μš©ν•˜λ©΄ ν”„λ‘œκ·Έλž˜λ¨Έκ°€ μ˜ˆμƒμΉ˜ λͺ»ν•œ μ—λŸ¬κ°€ λ°œμƒν•˜λ”λΌλ„ μ„œλ²„κ°€ 죽지 μ•Šκ³ , λ˜ν•œ λ‘œκΉ… λ“±μ˜ μž‘μ—…μ„ ν•¨κ»˜ μˆ˜ν–‰ν•  수 μžˆλ‹€. 

NestJS μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 생성 μ‹œ 기본적으둜 NestJS κΈ°λ³Έ μ˜ˆμ™Έ ν•„ν„°κ°€ λ‚΄μž₯λœλ‹€.

 

NestJS의 μ˜ˆμ™Έ

NestJS κΈ°λ³Έ 제곡 μ˜ˆμ™Έ

NestJSμ—μ„œλŠ” 기본적으둜 HttpExceptionμ΄λΌλŠ” κΈ°λ³Έ μ˜ˆμ™Έλ₯Ό μ œκ³΅ν•˜λ©°, 이λ₯Ό μƒμ†ν•œ μˆ˜λ§Žμ€ λ‚΄μž₯ μ˜ˆμ™Έκ°€ μ‘΄μž¬ν•œλ‹€.

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException

μœ„μ— μž‘μ„±ν•œ μ˜ˆμ‹œλ“€μ€ HttpException을 μƒμ†ν•œ κΈ°λ³Έ λ‚΄μž₯ μ˜ˆμ™Έλ“€μ˜ 일뢀뢄이닀. μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ‚΄μ—μ„œ μœ„μ˜ μ˜ˆμ‹œλ“€μ„ λ˜μ§€λ„λ‘ λ‘œμ§μ„ μž‘μ„±ν•˜λ©΄, NestJS의 κΈ°λ³Έ λ‚΄μž₯ μ˜ˆμ™Έ ν•„ν„°κ°€ 이λ₯Ό μΊμΉ˜ν•˜μ—¬ μ‚¬μš©μžμ—κ²Œ 각 μ˜ˆμ™Έμ— λŒ€ν•œ λ°˜ν™˜μ„ μˆ˜ν–‰ν•œλ‹€.

Custom μ˜ˆμ™Έ μž‘μ„±

λ‹€μ–‘ν•œ μ˜ˆμ™Έ 상황에 λŒ€μ²˜ν•˜κΈ° μœ„ν•΄ HttpException의 μƒμ„±μžμ— ν•˜λ“œμ½”λ”©μ„ ν•  μˆ˜λ„ μžˆμ§€λ§Œ μž¬μ‚¬μš©μ„±, μœ μ§€λ³΄μˆ˜μ„±μ„ μœ„ν•΄μ„œ HttpException을 μƒμ†ν•˜μ—¬ λ”°λ‘œ 객체둜 κ΄€λ¦¬ν•˜λŠ” 것을 μΆ”μ²œν•œλ‹€. 

μ˜ˆμ‹œλ₯Ό ν•˜λ‚˜ λ“€μ–΄λ³΄μž. μ‚¬μš©μžμ˜ νšŒμ›κ°€μž… μš”μ²­μ—μ„œ 이미 μ‘΄μž¬ν•˜λŠ” 이메일을 μ‚¬μš©ν•  경우 μ‚¬μš©μžμ—κ²Œ μ˜ˆμ™Έ 처리λ₯Ό ν•΄μ•Όν•œλ‹€λ©΄ λ‹€μŒκ³Ό 같이 μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μžˆλ‹€.

import { BadRequestException } from '@nestjs/common';

export class DuplicateEmailSignupException extends BadRequestException {
  constructor() {
    super('The email you inputed is duplicated.');
  }
}

이 λ•Œ HttpException을 μƒμ†ν•˜λŠ” 것이 μ•„λ‹Œ 그의 μžμ‹ 클래슀인 BadRequestException을 μƒμ†ν–ˆλŠ”λ°, κ²°κ΅­ 이 λ˜ν•œ HttpException의 ν•˜μœ„ 클래슀이기 λ•Œλ¬Έμ— 잘 μž‘λ™ν•œλ‹€.

이λ₯Ό μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” DB λ‚΄ 쀑볡 이메일이 μ‘΄μž¬ν•  λ•Œ κ·Έμ € μœ„ 클래슀의 μΈμŠ€ν„΄μŠ€λ₯Ό 던져주기만 ν•˜λ©΄ λœλ‹€. μ•„λž˜λŠ” 그의 μ˜ˆμ‹œμ΄λ‹€.

  async signup({ email, password }: SignupDto): Promise<boolean> {
    const duplicatedUser = await this.userRepository.findOneBy({
      email,
      deletedAt: null,
    });
    
    // μ˜ˆμ™Έ 처리
    if (duplicatedUser) {
      throw new DuplicateEmailSignupException();
    }
    ...
  }

 

μ˜ˆμ™Έ ν•„ν„° μž‘μ„±

NestJSμ—λŠ” 이미 κΈ°λ³Έ λ‚΄μž₯ μ˜ˆμ™Έ ν•„ν„°κ°€ μ‘΄μž¬ν•˜κΈ° λ•Œλ¬Έμ— λ”°λ‘œ 둜직 μž‘μ„±μ΄ ν•„μš”μ—†μ§€λ§Œ, κΈ°λ³Έ λ‚΄μž₯ ν•„ν„°μ˜ κΈ°λŠ₯을 ν™•μž₯ν•˜κΈ° μœ„ν•΄μ„œλŠ” Custom μ˜ˆμ™Έ ν•„ν„°μ˜ μž‘μ„±μ΄ ν•„μš”ν•˜λ‹€.

Custom μ˜ˆμ™Έ ν•„ν„° μž‘μ„±

NestJSμ—μ„œ Custom μ˜ˆμ™Έ ν•„ν„°λ₯Ό μž‘μ„±ν•˜κΈ° μœ„ν•œ 방법은 크게 2가지가 μ‘΄μž¬ν•œλ‹€.

  • ExceptionFilter μΈν„°νŽ˜μ΄μŠ€λ₯Ό 상속(implements)ν•˜μ—¬ catch λ©”μ„œλ“œ 직접 κ΅¬ν˜„
  • BaseExceptionFilter 클래슀λ₯Ό 상속(extends)ν•˜μ—¬ κΈ°λŠ₯ μΆ”κ°€

μΈν„°νŽ˜μ΄μŠ€ 상속을 ν†΅ν•œ μ˜ˆμ™Έ ν•„ν„° 생성

ν•΄λ‹Ή 방식은 μ˜ˆμ™Έ ν•„ν„°λ‘œμ„œ ν•„μš”ν•œ μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ°€μ Έμ˜¨ λ’€ λ‚΄λΆ€ κ΅¬ν˜„μ€ μƒˆλ‘œν•˜λŠ” 방식이닀. μ˜ˆμ‹œλŠ” μ•„λž˜μ™€ κ°™λ‹€.

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

// HttpException μ—λŸ¬λ₯Ό 처리
@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 λ°μ½”λ ˆμ΄ν„°λŠ” ν•΄λ‹Ή ν•„ν„°κ°€ μ–΄λ–€ μ’…λ₯˜μ˜ Exception을 μ²˜λ¦¬ν•˜λŠ”μ§€μ— λŒ€ν•œ 메타데이터λ₯Ό μ œκ³΅ν•œλ‹€. λ§Œμ•½ λͺ¨λ“  μ’…λ₯˜μ˜ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κ³  μ‹Άλ‹€λ©΄, 인자λ₯Ό μΆ”κ°€ν•˜μ§€ μ•ŠμœΌλ©΄ λœλ‹€.

catch λ©”μ„œλ“œμ˜ exception μΈμžλŠ” ν•΄λ‹Ή ν•„ν„°κ°€ μ²˜λ¦¬ν•˜λŠ” μ˜ˆμ™Έμ΄λ‹€. ν•΄λ‹Ή μ˜ˆμ‹œμ—μ„œλŠ” HttpExceptionλ§Œμ„ μ²˜λ¦¬ν•˜κ³  있기 λ•Œλ¬Έμ— exception의 νƒ€μž…μ€ HttpException이 λœλ‹€. 

catch λ©”μ„œλ“œμ˜ host μΈμžλŠ” ArgumentsHost이닀. 이λ₯Ό ν†΅ν•˜λ©΄ ν˜„μž¬ μ‹€ν–‰μ»¨ν…μŠ€νŠΈμ—μ„œ λ‹€μ–‘ν•œ 정보에 접근이 κ°€λŠ₯ν•˜λ‹€. (ex. Express의 Request, Response 객체; WebSocketμ—μ„œ Client, Data 객체 λ“±)

μœ„ μ˜ˆμ‹œλŠ” ν•„ν„°κ°€ μ„€μ •λœ λ°”μš΄λ”λ¦¬ λ‚΄μ—μ„œ λ°œμƒν•œ HttpException에 λŒ€ν•΄, μ‚¬μš©μžμ—κ²Œ μƒνƒœ μ½”λ“œ, μ˜ˆμ™Έ λ°œμƒμΌμ‹œ, μ˜ˆμ™Έ λ°œμƒ 경둜λ₯Ό 담은 JSON을 λ°˜ν™˜ν•˜λŠ” ν•„ν„°λ‘œ λ³Ό 수 μžˆλ‹€.

κΈ°λ³Έ μ˜ˆμ™Έ ν•„ν„° 클래슀λ₯Ό μƒμ†ν•˜μ—¬ κΈ°λŠ₯을 μΆ”κ°€ν•˜λŠ” 방식

ν•΄λ‹Ή 방식은 기쑴의 λ‚΄μž₯ μ˜ˆμ™Έ ν•„ν„°μ˜ κΈ°λŠ₯을 μœ μ§€ν•œμ±„ 좔가적인 κΈ°λŠ₯을 덧뢙이기 μœ„ν•œ 방식이닀. κ³΅μ‹λ¬Έμ„œμ˜ μ˜ˆμ‹œλŠ” μ•„λž˜μ™€ κ°™λ‹€.

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}

μœ„ μ˜ˆμ‹œμ—μ„œλŠ” λͺ¨λ“  μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜λŠ” ν•„ν„°λ₯Ό μœ„ν•΄ @Catch λ°μ½”λ ˆμ΄ν„°μ— 인자λ₯Ό ν• λ‹Ήν•˜μ§€ μ•Šμ•˜λ‹€. λ˜ν•œ catch λ©”μ„œλ“œ λ‚΄λΆ€μ—μ„œ λΆ€λͺ¨ 클래슀의 catch λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λŠ”λ°, μ΄λŠ” λ‹¨μˆœνžˆ μ ‘κ·Ό 방식을 보이기 μœ„ν•¨μ΄λ©° μ‹€μ œ κ΅¬ν˜„μ—μ„œλŠ” 각 상황에 λ§žλŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 좔가될 수 μžˆλ‹€.

κ³΅μ‹λ¬Έμ„œμ— λ”°λ₯΄λ©΄ μœ„ 방식을 컨트둀러, λ©”μ„œλ“œ λ²”μœ„μ˜ ν•„ν„°λ‘œ μ‚¬μš©ν•  경우 μΈμŠ€ν„΄μŠ€λ₯Ό ν• λ‹Ήν•˜λŠ” 것이 μ•„λ‹Œ, 클래슀 자체λ₯Ό ν• λ‹Ήν•˜μ—¬ NestJS에 μΈμŠ€ν„΄μŠ€ν™”λ₯Ό μœ„μž„ν•΄μ•Όν•œλ‹€.(μ „μ—­ ν•„ν„° 섀정을 μœ„ν•΄ λͺ¨λ“ˆ λ°–μ—μ„œ useGlobalFilters λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•œλ‹€λ©΄, κ·Έ λ•Œμ—λŠ” μΈμŠ€ν„΄μŠ€λ₯Ό ν• λ‹Ήν•΄μ•Όν•œλ‹€.)

 

μ˜ˆμ™Έ ν•„ν„° 적용

NestJSμ—μ„œ Custom μ˜ˆμ™Έ ν•„ν„°λ₯Ό μ μš©ν•˜λŠ” λ°©λ²•μ—λŠ” 크게 4가지가 μ‘΄μž¬ν•œλ‹€.

  1. λͺ¨λ“ˆ 밖에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ ν•„ν„°
  2. λͺ¨λ“ˆ 내에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ ν•„ν„°
  3. 컨트둀러 λ ˆλ²¨μ— μž‘λ™ν•˜λŠ” ν•„ν„°
  4. λ©”μ„œλ“œ(ν•Έλ“€λŸ¬) λ ˆλ²¨μ— μž‘λ™ν•˜λŠ” ν•„ν„°

λͺ¨λ“ˆ 밖에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ ν•„ν„°

λͺ¨λ“ˆ 밖에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ ν•„ν„° 섀정을 μœ„ν•΄μ„œλŠ” NestJS μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ useGlobalFilters λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•΄μ•Όν•œλ‹€. 이 λ•Œ λ©”μ„œλ“œμ˜ μΈμžλ‘œλŠ” 클래슀λ₯Ό 톡해 μƒμ„±ν•œ μΈμŠ€ν„΄μŠ€κ°€ μœ„μΉ˜ν•΄μ•Ό ν•œλ‹€. ν•΄λ‹Ή 방식은 μ–Έμ œκΉŒμ§€λ‚˜ λͺ¨λ“ˆ 밖에 ν•„ν„°κ°€ μœ„μΉ˜ν•˜κΈ° λ•Œλ¬Έμ—, λͺ¨λ“ˆ 내에 μ‘΄μž¬ν•˜λŠ” μ˜μ‘΄μ„±μ— λŒ€ν•œ μ£Όμž…μ€ λΆˆκ°€λŠ₯ν•˜λ‹€. μ•„λž˜λŠ” κ³΅μ‹λ¬Έμ„œμ˜ μ˜ˆμ‹œμ΄λ‹€.

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // μ „μ—­ ν•„ν„° μ„€μ •
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

λͺ¨λ“ˆ 내에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ ν•„ν„°

ν•„ν„°λ₯Ό λͺ¨λ“ˆ 내에 μœ„μΉ˜μ‹œν‚€λ©΄μ„œλ„, ν•„ν„°λ₯Ό μ „μ—­μœΌλ‘œ μ‚¬μš©ν•  수 μžˆλ‹€. λͺ¨λ“ˆ λ‚΄ ν”„λ‘œλ°”μ΄λ”μ— ν•„ν„°λ₯Ό μ£Όμž… μ‹œ μ΄λŠ” μ „μ—­ ν•„ν„°κ°€ λœλ‹€.

μ–΄λ–€ λͺ¨λ“ˆμ—μ„œ μ£Όμž…ν•˜λ”λΌλ„ μ΄λŠ” μ „μ—­μœΌλ‘œ μž‘λ™ν•˜κΈ° λ•Œλ¬Έμ—, ν•„ν„°κ°€ μž‘μ„±λœ λͺ¨λ“ˆμ—μ„œ λ°”λ‘œ μ£Όμž…μ„ ν•˜λ©΄ λœλ‹€.

ν•΄λ‹Ή 방식을 μ΄μš©ν•  경우 NestJS λͺ¨λ“ˆ λ‚΄μ˜ μ˜μ‘΄μ„±μ„ 필터에 μ£Όμž…ν•˜μ—¬ μ΄μš©ν•  수 μžˆλ‹€λŠ” μž₯점이 μžˆλ‹€. μ˜ˆμ‹œλŠ” μ•„λž˜μ™€ κ°™λ‹€.

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

컨트둀러 λ ˆλ²¨μ—μ„œ μž‘λ™ν•˜λŠ” ν•„ν„°

νŠΉμ • μ»¨νŠΈλ‘€λŸ¬μ— λŒ€ν•΄μ„œλ§Œ ν•„ν„°λ₯Ό μ μš©ν•  μˆ˜λ„ μžˆλ‹€. 이 λ•ŒλŠ” NestJSμ—μ„œ μ œκ³΅ν•˜λŠ” @UseFilters λ°μ½”λ ˆμ΄ν„°λ₯Ό μ΄μš©ν•΄μ•Όν•œλ‹€.

곡식 λ¬Έμ„œμ—μ„œλŠ” ν•΄λ‹Ή 방식을 μ΄μš©ν•  경우, μΈμŠ€ν„΄μŠ€λ₯Ό 인자둜 ν• λ‹Ήν•˜κΈ°λ³΄λ‹€λŠ” 클래슀 자체λ₯Ό ν• λ‹Ήν•˜μ—¬ μΈμŠ€ν„΄μŠ€ν™”μ— λŒ€ν•œ μ±…μž„μ„ NestJS에 μ΄κ΄€ν•˜λ©΄ μ „λ°˜μ μΈ λ©”λͺ¨λ¦¬μ˜ μ‚¬μš©μ„ 쀄일 수 μžˆλ‹€κ³  ν•œλ‹€. λ˜λ„λ‘μ΄λ©΄ 클래슀 자체λ₯Ό ν• λ‹Ήν•˜μž.

μ˜ˆμ‹œλŠ” μ•„λž˜μ™€ κ°™λ‹€.

// NestJS의 λŒ€ν‘œμ μΈ μ˜ˆμ‹œμΈ Cats μ˜ˆμ‹œ
@UseFilters(HttpExceptionFilter)
// @UseFilters(new HttpExceptionFilter()) 도 κ°€λŠ₯ν•˜λ‚˜,
// λ©”λͺ¨λ¦¬ μ ˆμ•½ μΈ‘λ©΄μ—μ„œ μΆ”μ²œν•˜μ§€ μ•ŠλŠ” 방식
export class CatsController {}

λ©”μ„œλ“œ(ν•Έλ“€λŸ¬) λ ˆλ²¨μ—μ„œ μž‘λ™ν•˜λŠ” ν•„ν„°

νŠΉμ • 컨트둀러의 λ©”μ„œλ“œμ— λŒ€ν•΄μ„œλ§Œ ν•„ν„°λ₯Ό μ μš©ν•  μˆ˜λ„ μžˆλ‹€. ν•΄λ‹Ή 방식 λ˜ν•œ μ•žμ„œ μ–ΈκΈ‰ν•œ @UseFilters λ°μ½”λ ˆμ΄ν„°λ₯Ό μ΄μš©ν•΄μ•Όν•˜λ©°, 컨트둀러 μžμ²΄μ— μ μš©ν•˜λŠ” 것과 μ‚¬μš©λ²•μ΄ λ‹€λ₯΄μ§€ μ•Šλ‹€.

μ˜ˆμ‹œλŠ” μ•„λž˜μ™€ κ°™λ‹€.

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

 

Reference

https://docs.nestjs.com/exception-filters

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

 

'Backend > NestJS' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€

NestJS Request Lifecycle (4) - Pipe  (0) 2022.12.29
NestJS Request Lifecycle (3) - Interceptor  (0) 2022.12.26
NestJS Request Lifecycle (2) - Guard  (0) 2022.12.23
NestJS Request Lifecycle (1) - Middleware  (0) 2022.12.22
NestJS Request Lifecycle (0) - κ°œμš”  (0) 2022.12.22