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를 거쳐 응답을 처리하는 구조입니다.
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를 변환하거나, 특정한 엔드포인트에 적용되는 로직을 추가하는 데 사용됩니다.
일반적으로 다음 목적으로 사용됩니다.
- Exception Filters (예외 필터): 예외가 발생했을 때 해당 예외를 가로채고 적절한 응답을 생성합니다.
- HTTP Exception Filters (HTTP 예외 필터): 특정 HTTP 상태 코드에 대한 예외를 처리합니다.
- 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();