(NestJS-기초강의) 10. Request Lifecycle (Feat. Guard, Interceptor, Pipe, Filters)

NestJS 애플리케이션은 Request Lifecycle이라는 일련의 단계를 통해 Request을 처리하고 응답을 생성합니다.

Middleware에 대해서는 (NestJS-기초강의) 9. 미들웨어 (Middleware) 에서 소개했지만 다른 Class에 대해서 당장 모두 상세히 알아야 할 필요는 없으므로 Request Lifecycle을 설명하며 간단하게 설명하도록 하겠습니다.

Overview

Middleware, Pipes, Guards, Interceptor 등의 사용으로 Global, Controller Level 및 Route Level 수준의 컴포넌트가 사용되면서 특정 코드가 어디에서 실행되는지 파악하기 어려울 수 있습니다.

일반적으로 Reuqest는 Middleware -> Gaurds -> Interceptor(Pre-request) -> Pipes -> Controller -> Service -> Interceptor(Post-request)-> Exception filter를 거쳐 응답을 처리하는 구조입니다.

Request Lifecycle (출처: https://chuongtrh.github.io/post/request-lifecycle-in-nestjs/)

Middleware

공식 문서: https://docs.nestjs.com/middleware

Request과 Response 사이에 위치하여, 애플리케이션에 들어오는 Request을 가로채고 변형할 수 있는 기능을 제공합니다.

Request 에 대한 추가적인 Logging이나 보안 검사 등을 수행할 수 있습니다.

NestJS에서 Middleware는 Global, Controller 수준, Route 수준에 적용할 수 있습니다.

자세한 내용은 (NestJS-기초강의) 9. 미들웨어 (Middleware) 에서 확인할 수 있습니다.

Guards

공식문서: https://docs.nestjs.com/guards

Request을 처리하는 동안에 Request을 필터링하고 가로채는 데 사용됩니다.

이는 Middleware와 유사하게 동작하지만, 주로 Request를 허용하거나 차단하는 데 사용됩니다.

NestJS 애플리케이션에서 Guards도 마찬가지로 Global, Controller 수준, Route 수준에 바인딩되어 특정 Request가 특정 조건을 충족하는지 확인하거나 검사할 수 있습니다.

예시)

Guards는 @Gaurd() Decorator를 사용하여 적용합니다.

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // Get request's context
    const request = context.switchToHttp().getRequest();
    
    // Check if it is approved
    const isAuthorized = ... 

    // Return result
    return isAuthorized;
  }
}

Interceptor

공식문서: https://docs.nestjs.com/interceptors

Request와 Response을 가로채고 변형시키는 데 사용되는 중요한 기능입니다.

Interceptor는 Request이 컨트롤러에 도달하기 전과 후에 실행되며, RxJS Observables를 반환하여 비동기적으로 동작할 수 있습니다.

Interceptor는 Pre와 Post로 구분되어 작동합니다. 이러한 구분은 Interceptor가 Request의 처리 전(pre)과 후(post)에 실행되는 시점을 나타냅니다.

Pre-Interceptor는 Request이 컨트롤러로 전달되기 전에 실행됩니다. 이 시점에서 Request의 가로채기, 수정 또는 허용 여부를 확인하고 Request을 변형할 수 있습니다. 주로 인증, 권한 부여, 로깅 등의 작업을 수행합니다.

Post-Interceptor는 Request이 Controller에서 반환되고 클라이언트에게 Response되기 전에 실행됩니다. 이 시점에서는 Request에 대한 추가 작업이 가능합니다. 주로 Response의 가로채기, 수정 또는 로깅과 같은 작업을 수행하며, 클라이언트에게 반환되는 Response을 변경할 수 있습니다.

간단한 예시로, Pre-Interceptor에서는 Request가 들어온 시간을 기록하거나 권한을 확인하는 등의 작업을 할 수 있으며, Post-Interceptor에서는 Request이 완료된 시간을 기록하거나 응답을 가공하여 반환할 수 있습니다.

예시)

NestInterceptor를 구현하여 intercept() method를 통해 Repuest를 가로채서 처리합니다.

CallHandler를 통해 다음 Handler를 호출하여 Request를 처리합니다.

Interceptor는 Request가 들어오면 ‘Accept Request…’를 로깅하고 완료되면 ‘Complete to handle the request.’를 로깅합니다. 이가 바로 Pre-interceptor / Post-interceptor에 해당합니다.

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<any> {
    console.log('Accept Request...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`Complete to handle the request. ${Date.now() - now}ms`)),
      );
  }
}

Pipes

공식문서: https://docs.nestjs.com/pipes

데이터의 유효성 검사, 변환 및 가공을 수행하는 기능을 담당하는데 데이터 처리에 좀 더 중점을 둡니다.

Handler Method가 호출되기 직전에 pipes를 삽입하고 piepes는 method에 대한 인수를 수신하여 작동합니다.

Nest에는 다음의 기본 내장된 파이프가 있습니다.

  • ValidationPipe (유효성 검사 파이프): 입력 데이터의 유효성을 검사하고, 데이터가 유효하지 않을 경우 예외를 발생시키거나 적절한 오류 응답을 반환합니다. 주로 DTO(Data Transfer Object)의 유효성을 검사하는 데 사용됩니다.
  • ParseIntPipe (정수 파이프): 문자열을 정수로 변환하고, 변환할 수 없는 경우 예외를 발생시킵니다. 주로 문자열을 정수로 변환하여 처리하는 경우에 사용됩니다.
  • ParseFloatPipe (부동 소수점 파이프): 문자열을 부동 소수점 숫자로 변환합니다. 문자열을 부동 소수점 숫자로 변환할 수 없는 경우 예외를 발생시킵니다.
  • ParseBoolPipe (부울 파이프): 문자열을 부울 값(true/false)으로 변환합니다. 문자열이 부울 값으로 변환할 수 없는 경우 예외를 발생시킵니다.
  • ParseArrayPipe (배열 파이프): 문자열을 배열로 변환합니다. 문자열이 배열로 변환될 수 없는 경우 예외를 발생시킵니다.
  • ParseUUIDPipe (UUID 파이프): 문자열을 UUID 형식으로 변환합니다. 문자열이 올바른 UUID 형식이 아닌 경우 예외를 발생시킵니다.
  • ParseEnumPipe (열거형 파이프): 문자열을 지정된 열거형으로 변환합니다. 문자열이 유효한 열거형 값이 아닌 경우 예외를 발생시킵니다.
  • DefaultValuePipe (기본값 파이프): 파라미터가 없거나 값이 undefined인 경우, 지정된 기본값으로 설정합니다.
  • ParseFilePipe (파일 파이프): Request에서 파일을 파싱하고 처리하는 데 사용됩니다. Request에 파일이 포함되어 있지 않거나 파일 형식이 올바르지 않은 경우 예외를 발생시킵니다.

예제)

다음 예제에서 id로 전달되는 argument는 숫자이거나 숫자가 아니라면 예외가 throw 됩니다.

@Get('find/:index')
async findOne(@Param('index', ParseIntPipe) index: number) {
  return this.usersService.findOne(index);
}

Filter

주로 전역적으로(Globally) Request과 Response를 변환하거나, 특정한 엔드포인트에 적용되는 로직을 추가하는 데 사용됩니다.

일반적으로 다음 목적으로 사용됩니다.

  1. Exception Filters (예외 필터): 예외가 발생했을 때 해당 예외를 가로채고 적절한 응답을 생성합니다.
  2. HTTP Exception Filters (HTTP 예외 필터): 특정 HTTP 상태 코드에 대한 예외를 처리합니다.
  3. Custom Filters (사용자 정의 필터): 특정 조건에 맞춰 Request 또는 Response를 가로채고 조작합니다.

예제)

Http 요청이 왔을 때, ExceptionFilter를 구현하여 예외를 처리하는 AllExceptionFilter를 정의합니다.

/core/filters/exception.filter.ts

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

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const context = host.switchToHttp();
    const response = context.getResponse<Response>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        message: exception.message,
        timestamp: new Date().toISOString(),
      });
  }
}

전역 혹은 Controller 레벨에서 위에서 정의한 AllExceptionFilter를 적용합니다.

본 예제에서는 전역에서 등록하였습니다.

/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

import { AllExceptionFilter } from './core/filters/exception.filter';
import { LoggerService } from './core/logger/logger.service';
import { LoggingInterceptor } from './core/interceptors/logger.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalFilters(new AllExceptionFilter(new LoggerService()));

  app.useGlobalInterceptors(new LoggingInterceptor(new LoggerService()));
  await app.listen(3000);
}
bootstrap();

참조 링크

Leave a Comment