NestJS Middleware 실전 패턴

NestJS Middleware란?

NestJS Middleware는 라우트 핸들러가 실행되기 전에 요청·응답 객체에 접근하여 로직을 수행하는 함수 또는 클래스입니다. Express 미들웨어와 동일한 개념이지만, NestJS의 DI(Dependency Injection) 시스템과 완벽하게 통합되어 훨씬 강력합니다.

Guard, Interceptor, Pipe와 달리 Middleware는 실행 컨텍스트(ExecutionContext) 이전 단계에서 동작하므로, 인증 토큰 파싱, 요청 로깅, CORS 처리 같은 횡단 관심사(Cross-cutting concerns)를 다루기에 적합합니다.

클래스 Middleware vs 함수 Middleware

클래스 기반 Middleware

NestMiddleware 인터페이스를 구현하는 방식입니다. DI 컨테이너에서 서비스를 주입받을 수 있어 가장 많이 사용됩니다.

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) {
    const start = Date.now();
    res.on('finish', () => {
      const duration = Date.now() - start;
      console.log(`${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`);
    });
    next();
  }
}

함수 기반 Middleware

DI가 불필요한 단순 로직에는 함수형이 더 간결합니다.

import { Request, Response, NextFunction } from 'express';

export function corsMiddleware(req: Request, res: Response, next: NextFunction) {
  res.setHeader('X-Request-Id', crypto.randomUUID());
  next();
}

Middleware 등록: apply()와 forRoutes()

NestJS에서 Middleware는 모듈의 configure() 메서드를 통해 등록합니다.

import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';

@Module({ controllers: [UserController, OrderController] })
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .exclude({ path: 'health', method: RequestMethod.GET })
      .forRoutes(
        { path: 'users', method: RequestMethod.ALL },
        { path: 'orders/*', method: RequestMethod.ALL },
      );
  }
}
메서드 설명
apply() 적용할 Middleware 클래스 또는 함수 (쉼표로 다중 가능)
forRoutes() 컨트롤러 클래스 또는 path+method 조합으로 적용 대상 지정
exclude() 특정 라우트를 적용 대상에서 제외

실전 패턴 1: 인증 토큰 파싱 Middleware

Guard에서 인증 검증을 하기 전, Middleware 단계에서 토큰을 파싱하여 req.user에 주입하는 패턴입니다.

@Injectable()
export class AuthTokenMiddleware implements NestMiddleware {
  constructor(private readonly jwtService: JwtService) {}

  use(req: Request, res: Response, next: NextFunction) {
    const authHeader = req.headers.authorization;
    if (authHeader?.startsWith('Bearer ')) {
      try {
        const token = authHeader.slice(7);
        const payload = this.jwtService.verify(token);
        (req as any).user = payload;
      } catch {
        // Guard에서 처리하도록 user를 설정하지 않음
      }
    }
    next();
  }
}

이 패턴의 장점은 Guard와 역할이 명확히 분리된다는 것입니다. Middleware는 토큰 파싱만 담당하고, Guard는 파싱된 사용자 정보를 기반으로 접근 허용 여부만 결정합니다.

실전 패턴 2: 요청 상관관계 ID(Correlation ID)

MSA 환경에서 요청 추적을 위해 Correlation ID를 전파하는 Middleware입니다. NestJS ExecutionContext 심화에서 다룬 Guard·Interceptor와 함께 사용하면 전 구간 추적이 가능합니다.

import { AsyncLocalStorage } from 'async_hooks';

export const correlationStorage = new AsyncLocalStorage<string>();

@Injectable()
export class CorrelationMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const correlationId =
      (req.headers['x-correlation-id'] as string) ?? crypto.randomUUID();

    res.setHeader('X-Correlation-Id', correlationId);

    correlationStorage.run(correlationId, () => next());
  }
}

AsyncLocalStorage를 활용하면 서비스 레이어 어디서든 correlationStorage.getStore()로 현재 요청의 ID에 접근할 수 있어, 로거와 HTTP 클라이언트에 자동 전파됩니다.

실전 패턴 3: Rate Limiting Middleware

IP 기반 요청 제한을 Middleware에서 구현하는 패턴입니다.

@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
  private store = new Map<string, { count: number; resetAt: number }>();

  use(req: Request, res: Response, next: NextFunction) {
    const ip = req.ip ?? 'unknown';
    const now = Date.now();
    const window = 60_000; // 1분
    const limit = 100;

    let entry = this.store.get(ip);
    if (!entry || now > entry.resetAt) {
      entry = { count: 0, resetAt: now + window };
      this.store.set(ip, entry);
    }

    entry.count++;
    res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - entry.count));

    if (entry.count > limit) {
      res.status(429).json({ message: 'Too Many Requests' });
      return;
    }
    next();
  }
}

글로벌 Middleware와 실행 순서

main.ts에서 app.use()로 등록하면 모든 라우트에 적용됩니다. 이때 DI는 사용할 수 없으므로 함수형 Middleware만 가능합니다.

// main.ts
import helmet from 'helmet';
import compression from 'compression';

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

  // 글로벌 Middleware (Express 플러그인 포함)
  app.use(helmet());
  app.use(compression());

  await app.listen(3000);
}

NestJS 요청 파이프라인의 실행 순서:

순서 단계 역할
1 Global Middleware app.use()로 등록한 미들웨어
2 Module Middleware configure()로 등록한 미들웨어
3 Guard 접근 권한 검증
4 Interceptor (pre) 요청 전처리
5 Pipe 파라미터 변환·검증
6 Handler 컨트롤러 메서드 실행
7 Interceptor (post) 응답 후처리
8 Exception Filter 에러 처리

Fastify 어댑터에서의 차이점

Fastify를 사용하는 경우, Express 미들웨어와 시그니처가 다릅니다. NestJS 헥사고날 아키텍처 심화에서 다룬 것처럼 어댑터 독립적인 설계가 중요합니다.

// Fastify 환경에서의 Middleware
@Injectable()
export class FastifyLoggerMiddleware implements NestMiddleware {
  use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
    console.log(`[Fastify] ${req.method} ${req.url}`);
    next();
  }
}

Fastify 전용 Hook이 필요하면 onRequest, preHandler 등 Fastify 네이티브 훅을 main.ts에서 직접 등록하는 것이 더 적합합니다.

Middleware vs Guard vs Interceptor 선택 기준

기준 Middleware Guard Interceptor
DI 지원 클래스형만
ExecutionContext
응답 변환 raw level ✅ (RxJS)
적합한 용도 로깅, CORS, 파싱 인가, 역할 검증 캐싱, 변환, 타이밍

정리

NestJS Middleware는 요청 파이프라인의 가장 앞단에서 동작하는 강력한 도구입니다. 토큰 파싱, Correlation ID 전파, Rate Limiting 같은 횡단 관심사에 적합하며, Guard·Interceptor와 역할을 명확히 나누는 것이 핵심입니다. 클래스형으로 DI를 활용하되, 단순 로직은 함수형으로 간결하게 유지하세요.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux