NestJS 미들웨어, 가드, 인터셉터 활용하기

NestJS 요청 처리 파이프라인

NestJS는 요청을 처리하는 여러 단계를 제공합니다:

  1. Middleware - 라우팅 전 요청/응답 처리
  2. Guard - 인증/인가 확인
  3. Interceptor (Before) - 요청 전처리
  4. Pipe - 데이터 변환 및 검증
  5. Controller - 요청 처리
  6. Interceptor (After) - 응답 후처리
  7. Exception Filter - 예외 처리

미들웨어 (Middleware)

Express의 미들웨어와 동일하게 동작하며, 라우트 핸들러 전에 실행됩니다.

함수형 미들웨어

// logger.middleware.ts
import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request: ${req.method} ${req.url}`);
next();
}

클래스형 미들웨어

// logger.middleware.ts
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();
}
}

미들웨어 적용

// app.module.ts
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'); // /users 경로에만 적용
}

// 또는
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'users', method: RequestMethod.GET }); // GET /users에만
}

// 제외할 경로 지정
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'users', method: RequestMethod.POST },
'users/(.*)',
)
.forRoutes(UsersController);
}

여러 미들웨어 적용

configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware, AuthMiddleware, CorsMiddleware)
.forRoutes('*');
}

실전 예제: 인증 미들웨어

// auth.middleware.ts
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) {
// JWT 검증 로직
return { userId: 1, email: 'user@example.com' };
}
}

가드 (Guard)

가드는 인증과 인가를 처리하는 데 사용됩니다. CanActivate 인터페이스를 구현합니다.

기본 가드 생성

nest g guard auth

인증 가드

// auth.guard.ts
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';
}
}

역할 기반 가드

// roles.guard.ts
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));
}
}

커스텀 데코레이터

// roles.decorator.ts
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';
}

// 전역 가드
// main.ts
app.useGlobalGuards(new AuthGuard());

실전 예제: JWT 가드

// jwt-auth.guard.ts
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

로깅 인터셉터

// logging.interceptor.ts
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`);
}),
);
}
}

응답 변환 인터셉터

// transform.interceptor.ts
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',
})),
);
}
}

에러 처리 인터셉터

// errors.interceptor.ts
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())),
);
}
}

캐싱 인터셉터

// cache.interceptor.ts
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);
}),
);
}
}

타임아웃 인터셉터

// timeout.interceptor.ts
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 [];
}

// 전역 인터셉터
// main.ts
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}`;
}
}

커스텀 파이프

// validation.pipe.ts
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
// create-user.dto.ts
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;
}

// 전역 ValidationPipe 설정
// main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTO에 없는 속성 제거
forbidNonWhitelisted: true, // DTO에 없는 속성이 있으면 에러
transform: true, // 자동 타입 변환
}));

예외 필터 (Exception Filter)

예외를 처리하고 사용자 정의 응답을 반환합니다.

기본 예외 필터

// http-exception.filter.ts
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,
});
}
}

모든 예외 처리

// all-exceptions.filter.ts
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 {}

// 전역 필터
// main.ts
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 문서화하기
  • 테스트 작성하기
  • 배포 준비하기
Share