NestJS Middleware: 클래스 vs 함수형

Middleware의 역할: Guard보다 먼저, 가장 원시적인 요청 가공

NestJS에서 Middleware는 라우트 핸들러에 도달하기 전 가장 먼저 실행되는 계층이다. Express/Fastify의 미들웨어와 동일한 개념으로, requestresponse 객체에 직접 접근하며 next()를 호출해 다음 단계로 넘긴다. NestJS 공식 문서는 Middleware를 “라우트 핸들러 이전에 호출되는 함수”로 정의하며, 다음 작업에 적합하다고 설명한다.

  • 요청/응답 객체 변경
  • 요청-응답 사이클 종료
  • next()로 다음 미들웨어 호출
  • 로깅, 상관관계 ID 주입, CORS, 요청 본문 파싱

Guard, Interceptor, Pipe와의 차이: Middleware는 ExecutionContext에 접근할 수 없다. 즉, 다음에 어떤 핸들러나 컨트롤러가 실행될지 모른다. 이것이 Middleware를 인가(authorization)가 아닌 범용 요청 가공에 사용해야 하는 이유다.

NestJS 요청 파이프라인에서 Middleware 위치

순서 계층 ExecutionContext DI 지원
1 Middleware ❌ 없음 ✅ (클래스 방식)
2 Guard
3 Interceptor (before)
4 Pipe
5 Handler

클래스 Middleware: NestMiddleware 인터페이스

클래스 기반 Middleware는 NestMiddleware 인터페이스를 구현하며, NestJS의 DI(의존성 주입)를 완전히 활용할 수 있다.

// 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) {
    const start = Date.now();
    
    // 응답 완료 시 로그
    res.on('finish', () => {
      const duration = Date.now() - start;
      console.log(
        `${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`
      );
    });

    next();
  }
}

DI를 활용하는 클래스 Middleware

// correlation-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class CorrelationIdMiddleware implements NestMiddleware {
  // 생성자에서 서비스 주입 가능
  // constructor(private readonly configService: ConfigService) {}

  use(req: Request, res: Response, next: NextFunction) {
    const correlationId = req.headers['x-correlation-id'] as string || uuidv4();
    
    // 요청 객체에 주입 — 이후 Guard, Interceptor, Handler에서 사용 가능
    req['correlationId'] = correlationId;
    
    // 응답 헤더에도 포함
    res.setHeader('x-correlation-id', correlationId);
    
    next();
  }
}

함수형 Middleware: 간단한 경우

DI가 필요 없는 단순한 Middleware는 함수로 작성할 수 있다. 공식 문서는 “의존성이 필요 없으면 함수형 Middleware를 사용하라”고 권장한다.

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

export function simpleLogger(req: Request, res: Response, next: NextFunction) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
}
항목 클래스 Middleware 함수형 Middleware
DI 지원 ✅ 생성자 주입
인터페이스 NestMiddleware 없음 (순수 함수)
등록 방법 apply(ClassName) apply(functionName)
적합한 경우 서비스 의존, 복잡한 로직 단순 로깅, 헤더 추가

Middleware 등록: NestModule의 configure 메서드

NestJS에서 Middleware는 @Module() 데코레이터가 아닌, 모듈 클래스가 NestModule 인터페이스를 구현하고 configure() 메서드에서 등록한다.

// app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
import { CorrelationIdMiddleware } from './correlation-id.middleware';
import { simpleLogger } from './simple-logger.middleware';
import { UsersModule } from './users/users.module';
import { OrdersModule } from './orders/orders.module';

@Module({
  imports: [UsersModule, OrdersModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      // 클래스 Middleware: 특정 컨트롤러에 적용
      .apply(CorrelationIdMiddleware, LoggerMiddleware)
      .forRoutes('*')    // 모든 라우트

      // 함수형 Middleware
      // .apply(simpleLogger)
      // .forRoutes('users');
  }
}

forRoutes의 다양한 지정 방법

import { RequestMethod } from '@nestjs/common';
import { UsersController } from './users/users.controller';

// 1. 문자열 경로
consumer.apply(LoggerMiddleware).forRoutes('users');

// 2. 경로 + HTTP 메서드
consumer.apply(LoggerMiddleware).forRoutes(
  { path: 'users', method: RequestMethod.GET },
  { path: 'users', method: RequestMethod.POST },
);

// 3. 컨트롤러 클래스 (해당 컨트롤러의 모든 라우트)
consumer.apply(LoggerMiddleware).forRoutes(UsersController);

// 4. 와일드카드 패턴
consumer.apply(LoggerMiddleware).forRoutes({ 
  path: 'users/(.*)', 
  method: RequestMethod.ALL 
});

// 5. 모든 라우트
consumer.apply(LoggerMiddleware).forRoutes('*');

라우트 제외: exclude 메서드

특정 라우트를 Middleware 적용 대상에서 제외할 수 있다. exclude()forRoutes()와 함께 사용한다.

consumer
  .apply(AuthTokenMiddleware)
  .exclude(
    { path: 'health', method: RequestMethod.GET },
    { path: 'auth/login', method: RequestMethod.POST },
    { path: 'auth/register', method: RequestMethod.POST },
  )
  .forRoutes('*');

주의: 공식 문서에 따르면 exclude()는 Fastify 어댑터 사용 시 와일드카드 패턴을 지원하지 않는다. Express에서는 path-to-regexp 패턴이 동작한다.

복수 Middleware 체이닝: 실행 순서

apply()에 여러 Middleware를 전달하면 왼쪽에서 오른쪽으로 순서대로 실행된다. 또한 configure()에서 consumer를 여러 번 호출할 수 있다.

configure(consumer: MiddlewareConsumer) {
  // 체이닝: Correlation → Logger → RateLimit 순서로 실행
  consumer
    .apply(CorrelationIdMiddleware, LoggerMiddleware, RateLimitMiddleware)
    .forRoutes('*');

  // 별도 등록: 특정 라우트에만 추가 Middleware
  consumer
    .apply(BodyParserMiddleware)
    .forRoutes({ path: 'webhooks', method: RequestMethod.POST });
}

글로벌 Middleware: main.ts에서 등록

Express/Fastify의 app.use()를 직접 사용해 글로벌 Middleware를 등록할 수도 있다. 단, 이 방식은 NestJS의 DI를 사용할 수 없으므로 함수형 Middleware만 가능하다.

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import helmet from 'helmet';
import compression from 'compression';

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

  // Express 글로벌 Middleware (DI 불가)
  app.use(helmet());
  app.use(compression());

  await app.listen(3000);
}
bootstrap();
등록 위치 DI 지원 라우트 필터링 적합한 Middleware
NestModule.configure() ✅ forRoutes/exclude 비즈니스 로직, 서비스 의존
main.ts app.use() ❌ (전체 적용) helmet, compression, cors 등 라이브러리

실전 패턴: 요청 시간 측정 + 느린 요청 경고

// slow-request.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class SlowRequestMiddleware implements NestMiddleware {
  private readonly logger = new Logger(SlowRequestMiddleware.name);
  private readonly threshold: number;

  constructor(private configService: ConfigService) {
    this.threshold = this.configService.get<number>('SLOW_REQUEST_MS', 3000);
  }

  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      if (duration > this.threshold) {
        this.logger.warn(
          `Slow request: ${req.method} ${req.originalUrl} took ${duration}ms (threshold: ${this.threshold}ms)`
        );
      }
    });

    next();
  }
}

Middleware vs Guard/Interceptor: 올바른 선택 기준

작업 올바른 계층 이유
요청 로깅, 상관관계 ID Middleware 핸들러 정보 불필요, 모든 요청에 적용
helmet, compression, cors Middleware (글로벌) Express 생태계 라이브러리
JWT 검증, 역할 확인 Guard ExecutionContext로 핸들러 메타데이터 필요
응답 변환, 캐싱 Interceptor 핸들러 전후 모두 개입, RxJS Observable
DTO 유효성 검사 Pipe 파라미터 단위 변환/검증

실전 체크리스트: Middleware 설계 6단계

  1. DI 필요 여부 결정 — 서비스 주입이 필요하면 클래스, 아니면 함수형
  2. 적용 범위 설정 — 모든 라우트면 forRoutes('*'), 특정 컨트롤러면 클래스 참조
  3. 제외 라우트 명시 — 헬스체크, 메트릭 엔드포인트는 exclude()로 제외
  4. 실행 순서 의식apply(A, B, C)는 A→B→C 순서, 보안→로깅→비즈니스 순으로 배치
  5. next() 호출 보장 — next()를 빠뜨리면 요청이 영원히 멈춘다
  6. Express 라이브러리는 main.ts에서 — helmet, compression 등은 app.use()로 글로벌 등록

흔한 실수 4가지와 방지법

실수 1: next()를 호출하지 않아 요청이 멈춤

증상: 특정 요청이 타임아웃된다. Middleware에서 조건 분기 중 하나의 경로에서 next()를 빠뜨렸다.

방지: 모든 코드 경로에서 next()가 호출되거나 res.end()/res.json()으로 응답을 완료하는지 확인한다. 요청을 중단하려면 next() 대신 응답을 직접 보내야 한다.

실수 2: Middleware에서 인가 로직을 구현

증상: Middleware에서 JWT를 검증하고 역할을 확인하는데, 특정 핸들러에만 적용하기 어렵고 메타데이터(@Roles 등)를 읽을 수 없다.

방지: 인가는 Guard의 책임이다. Middleware는 ExecutionContext가 없으므로 핸들러 메타데이터에 접근할 수 없다. 토큰 파싱까지만 Middleware에서 하고, 검증/인가는 Guard에서 한다.

실수 3: app.use()로 등록한 Middleware에서 DI를 기대

증상: app.use(new MyMiddleware())에서 생성자의 서비스가 undefined.

방지: app.use()는 NestJS IoC 컨테이너 바깥이다. DI가 필요하면 반드시 모듈의 configure()에서 등록한다.

실수 4: 모듈별 Middleware가 다른 모듈 라우트에 적용되지 않음

증상: UsersModuleconfigure()에서 등록한 Middleware가 OrdersController에 적용되지 않는다.

방지: Middleware는 등록한 모듈의 범위 내에서만 적용된다. 모든 모듈에 적용하려면 AppModule(루트 모듈)의 configure()에서 등록하거나 app.use()로 글로벌 등록한다.

마무리

NestJS Middleware는 요청 파이프라인의 가장 앞단에서 req/res를 가공하는 범용 계층이다. ExecutionContext 없이 동작하므로 인가보다는 로깅, 상관관계 ID, 보안 헤더 같은 횡단 관심사에 적합하다. 클래스 방식은 DI를, 함수 방식은 단순함을 제공하며, forRoutes()exclude()로 정밀한 적용 범위를 제어할 수 있다. 이 글의 모든 내용은 NestJS 공식 문서(Middleware)를 근거로 한다.

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