NestJS 요청 처리 파이프라인 NestJS는 요청을 처리하는 여러 단계를 제공합니다:
Middleware - 라우팅 전 요청/응답 처리
Guard - 인증/인가 확인
Interceptor (Before) - 요청 전처리
Pipe - 데이터 변환 및 검증
Controller - 요청 처리
Interceptor (After) - 응답 후처리
Exception Filter - 예외 처리
미들웨어 (Middleware) Express의 미들웨어와 동일하게 동작하며, 라우트 핸들러 전에 실행됩니다.
함수형 미들웨어 import { Request , Response , NextFunction } from 'express' ;export function logger (req: Request, res: Response, next: NextFunction ) { console .log (`Request: ${req.method} ${req.url} ` ); next (); }
클래스형 미들웨어 import { Injectable , NestMiddleware } from '@nestjs/common' ;import { Request , Response , NextFunction } from 'express' ;@Injectable ()export class LoggerMiddleware implements NestMiddleware { use (req: Request, res: Response, next: NextFunction ) { console .log (`[${new Date ().toISOString()} ] ${req.method} ${req.url} ` ); next (); } }
미들웨어 적용 import { Module , NestModule , MiddlewareConsumer } from '@nestjs/common' ;import { LoggerMiddleware } from './common/middleware/logger.middleware' ;@Module ({ imports : [], }) export class AppModule implements NestModule { configure (consumer: MiddlewareConsumer ) { consumer .apply (LoggerMiddleware ) .forRoutes ('*' ); } }
특정 라우트에만 적용 configure (consumer: MiddlewareConsumer ) { consumer .apply (LoggerMiddleware ) .forRoutes ('users' ); } configure (consumer: MiddlewareConsumer ) { consumer .apply (LoggerMiddleware ) .forRoutes ({ path : 'users' , method : RequestMethod .GET }); } configure (consumer: MiddlewareConsumer ) { consumer .apply (LoggerMiddleware ) .exclude ( { path : 'users' , method : RequestMethod .POST }, 'users/(.*)' , ) .forRoutes (UsersController ); }
여러 미들웨어 적용 configure (consumer: MiddlewareConsumer ) { consumer .apply (LoggerMiddleware , AuthMiddleware , CorsMiddleware ) .forRoutes ('*' ); }
실전 예제: 인증 미들웨어 import { Injectable , NestMiddleware , UnauthorizedException } from '@nestjs/common' ;import { Request , Response , NextFunction } from 'express' ;@Injectable ()export class AuthMiddleware implements NestMiddleware { use (req: Request, res: Response, next: NextFunction ) { const token = req.headers .authorization ; if (!token) { throw new UnauthorizedException ('No token provided' ); } try { const decoded = this .verifyToken (token); req['user' ] = decoded; next (); } catch (error) { throw new UnauthorizedException ('Invalid token' ); } } private verifyToken (token: string ) { return { userId : 1 , email : 'user@example.com' }; } }
가드 (Guard) 가드는 인증과 인가를 처리하는 데 사용됩니다. CanActivate 인터페이스를 구현합니다.
기본 가드 생성
인증 가드 import { Injectable , CanActivate , ExecutionContext , UnauthorizedException } from '@nestjs/common' ;import { Observable } from 'rxjs' ;@Injectable ()export class AuthGuard implements CanActivate { canActivate ( context : ExecutionContext , ): boolean | Promise <boolean > | Observable <boolean > { const request = context.switchToHttp ().getRequest (); const token = request.headers .authorization ; if (!token) { throw new UnauthorizedException ('No token provided' ); } return this .validateToken (token); } private validateToken (token : string ): boolean { return token === 'valid-token' ; } }
역할 기반 가드 import { Injectable , CanActivate , ExecutionContext } from '@nestjs/common' ;import { Reflector } from '@nestjs/core' ;@Injectable ()export class RolesGuard implements CanActivate { constructor (private reflector: Reflector ) {} canActivate (context : ExecutionContext ): boolean { const requiredRoles = this .reflector .get <string []>('roles' , context.getHandler ()); if (!requiredRoles) { return true ; } const request = context.switchToHttp ().getRequest (); const user = request.user ; return requiredRoles.some (role => user.roles ?.includes (role)); } }
커스텀 데코레이터 import { SetMetadata } from '@nestjs/common' ;export const Roles = (...roles: string [] ) => SetMetadata ('roles' , roles);
가드 사용 @Controller ('users' )@UseGuards (AuthGuard )export class UsersController {}@Get ()@UseGuards (AuthGuard , RolesGuard )@Roles ('admin' )findAll ( ) { return 'This action returns all users' ; } app.useGlobalGuards (new AuthGuard ());
실전 예제: JWT 가드 import { Injectable , ExecutionContext , UnauthorizedException } from '@nestjs/common' ;import { AuthGuard } from '@nestjs/passport' ;@Injectable ()export class JwtAuthGuard extends AuthGuard ('jwt' ) { canActivate (context: ExecutionContext ) { return super .canActivate (context); } handleRequest (err, user, info ) { if (err || !user) { throw err || new UnauthorizedException (); } return user; } }
인터셉터 (Interceptor) 인터셉터는 요청/응답을 가로채서 변환하거나 추가 로직을 실행합니다.
기본 인터셉터 nest g interceptor logging
로깅 인터셉터 import { Injectable , NestInterceptor , ExecutionContext , CallHandler , } from '@nestjs/common' ; import { Observable } from 'rxjs' ;import { tap } from 'rxjs/operators' ;@Injectable ()export class LoggingInterceptor implements NestInterceptor { intercept (context : ExecutionContext , next : CallHandler ): Observable <any > { const now = Date .now (); const request = context.switchToHttp ().getRequest (); console .log (`Before: ${request.method} ${request.url} ` ); return next.handle ().pipe ( tap (() => { console .log (`After: ${Date .now() - now} ms` ); }), ); } }
응답 변환 인터셉터 import { Injectable , NestInterceptor , ExecutionContext , CallHandler , } from '@nestjs/common' ; import { Observable } from 'rxjs' ;import { map } from 'rxjs/operators' ;export interface Response <T> { data : T; statusCode : number ; message : string ; } @Injectable ()export class TransformInterceptor <T> implements NestInterceptor <T, Response <T>> { intercept ( context : ExecutionContext , next : CallHandler , ): Observable <Response <T>> { return next.handle ().pipe ( map (data => ({ data, statusCode : context.switchToHttp ().getResponse ().statusCode , message : 'Success' , })), ); } }
에러 처리 인터셉터 import { Injectable , NestInterceptor , ExecutionContext , CallHandler , BadGatewayException , } from '@nestjs/common' ; import { Observable , throwError } from 'rxjs' ;import { catchError } from 'rxjs/operators' ;@Injectable ()export class ErrorsInterceptor implements NestInterceptor { intercept (context : ExecutionContext , next : CallHandler ): Observable <any > { return next.handle ().pipe ( catchError (err => throwError (() => new BadGatewayException ())), ); } }
캐싱 인터셉터 import { Injectable , NestInterceptor , ExecutionContext , CallHandler , } from '@nestjs/common' ; import { Observable , of } from 'rxjs' ;import { tap } from 'rxjs/operators' ;@Injectable ()export class CacheInterceptor implements NestInterceptor { private cache = new Map (); intercept (context : ExecutionContext , next : CallHandler ): Observable <any > { const request = context.switchToHttp ().getRequest (); const key = request.url ; if (this .cache .has (key)) { console .log ('Returning cached response' ); return of (this .cache .get (key)); } return next.handle ().pipe ( tap (response => { console .log ('Caching response' ); this .cache .set (key, response); }), ); } }
타임아웃 인터셉터 import { Injectable , NestInterceptor , ExecutionContext , CallHandler , RequestTimeoutException , } from '@nestjs/common' ; import { Observable , throwError, TimeoutError } from 'rxjs' ;import { catchError, timeout } from 'rxjs/operators' ;@Injectable ()export class TimeoutInterceptor implements NestInterceptor { intercept (context : ExecutionContext , next : CallHandler ): Observable <any > { return next.handle ().pipe ( timeout (5000 ), catchError (err => { if (err instanceof TimeoutError ) { return throwError (() => new RequestTimeoutException ()); } return throwError (() => err); }), ); } }
인터셉터 사용 @UseInterceptors (LoggingInterceptor )@Controller ('users' )export class UsersController {}@UseInterceptors (TransformInterceptor )@Get ()findAll ( ) { return []; } app.useGlobalInterceptors (new LoggingInterceptor ());
파이프 (Pipe) 파이프는 데이터 변환과 검증을 담당합니다.
내장 파이프 import { Controller , Get , Param , ParseIntPipe , ParseBoolPipe , ParseArrayPipe , ParseUUIDPipe , } from '@nestjs/common' ; @Controller ('users' )export class UsersController { @Get (':id' ) findOne (@Param ('id' , ParseIntPipe) id: number ) { return `User ${id} ` ; } @Get (':uuid' ) findByUUID (@Param ('uuid' , ParseUUIDPipe) uuid: string ) { return `User ${uuid} ` ; } }
커스텀 파이프 import { PipeTransform , Injectable , ArgumentMetadata , BadRequestException } from '@nestjs/common' ;@Injectable ()export class ValidationPipe implements PipeTransform { transform (value: any , metadata: ArgumentMetadata ) { if (!value) { throw new BadRequestException ('Validation failed' ); } return value; } }
class-validator 사용 npm install class-validator class-transformer
import { IsEmail , IsNotEmpty , MinLength , IsInt , Min , Max } from 'class-validator' ;export class CreateUserDto { @IsNotEmpty () @MinLength (3 ) name : string ; @IsEmail () email : string ; @IsInt () @Min (0 ) @Max (120 ) age : number ; } app.useGlobalPipes (new ValidationPipe ({ whitelist : true , forbidNonWhitelisted : true , transform : true , }));
예외 필터 (Exception Filter) 예외를 처리하고 사용자 정의 응답을 반환합니다.
기본 예외 필터 import { ExceptionFilter , Catch , ArgumentsHost , HttpException , HttpStatus , } from '@nestjs/common' ; import { Request , Response } from 'express' ;@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 , message : exception.message , }); } }
모든 예외 처리 import { ExceptionFilter , Catch , ArgumentsHost , HttpException , HttpStatus , } from '@nestjs/common' ; @Catch ()export class AllExceptionsFilter implements ExceptionFilter { catch (exception : unknown , host : ArgumentsHost ) { const ctx = host.switchToHttp (); const response = ctx.getResponse (); const request = ctx.getRequest (); const status = exception instanceof HttpException ? exception.getStatus () : HttpStatus .INTERNAL_SERVER_ERROR ; const message = exception instanceof HttpException ? exception.message : 'Internal server error' ; response.status (status).json ({ statusCode : status, timestamp : new Date ().toISOString (), path : request.url , message, }); } }
필터 사용 @Get ()@UseFilters (HttpExceptionFilter )findAll ( ) {}@UseFilters (HttpExceptionFilter )@Controller ('users' )export class UsersController {}app.useGlobalFilters (new AllExceptionsFilter ());
실전 예제: 종합 활용 app.module.ts import { Module , NestModule , MiddlewareConsumer } from '@nestjs/common' ;import { APP_GUARD , APP_INTERCEPTOR , APP_FILTER } from '@nestjs/core' ;import { LoggerMiddleware } from './common/middleware/logger.middleware' ;import { AuthGuard } from './common/guards/auth.guard' ;import { LoggingInterceptor } from './common/interceptors/logging.interceptor' ;import { AllExceptionsFilter } from './common/filters/all-exceptions.filter' ;@Module ({ providers : [ { provide : APP_GUARD , useClass : AuthGuard , }, { provide : APP_INTERCEPTOR , useClass : LoggingInterceptor , }, { provide : APP_FILTER , useClass : AllExceptionsFilter , }, ], }) export class AppModule implements NestModule { configure (consumer: MiddlewareConsumer ) { consumer.apply (LoggerMiddleware ).forRoutes ('*' ); } }
users.controller.ts import { Controller , Get , Post , Body , Param , UseGuards , UseInterceptors , ParseIntPipe , ValidationPipe , } from '@nestjs/common' ; import { UsersService } from './users.service' ;import { CreateUserDto } from './dto/create-user.dto' ;import { JwtAuthGuard } from '../common/guards/jwt-auth.guard' ;import { RolesGuard } from '../common/guards/roles.guard' ;import { Roles } from '../common/decorators/roles.decorator' ;import { TransformInterceptor } from '../common/interceptors/transform.interceptor' ;@Controller ('users' )@UseGuards (JwtAuthGuard , RolesGuard )@UseInterceptors (TransformInterceptor )export class UsersController { constructor (private readonly usersService: UsersService ) {} @Get () @Roles ('admin' , 'user' ) findAll ( ) { return this .usersService .findAll (); } @Get (':id' ) findOne (@Param ('id' , ParseIntPipe) id: number ) { return this .usersService .findOne (id); } @Post () @Roles ('admin' ) create (@Body (ValidationPipe) createUserDto: CreateUserDto ) { return this .usersService .create (createUserDto); } }
정리
Middleware : 요청/응답 전처리, 로깅, CORS 등
Guard : 인증/인가 검사
Interceptor : 요청/응답 변환, 로깅, 캐싱, 타임아웃
Pipe : 데이터 변환 및 검증
Exception Filter : 예외 처리 및 에러 응답 커스터마이징
실행 순서: Middleware → Guard → Interceptor(Before) → Pipe → Controller → Interceptor(After) → Filter
다음 단계 이 개념들을 마스터했다면:
JWT 인증 구현하기
Swagger로 API 문서화하기
테스트 작성하기
배포 준비하기