Middleware의 역할: Guard보다 먼저, 가장 원시적인 요청 가공
NestJS에서 Middleware는 라우트 핸들러에 도달하기 전 가장 먼저 실행되는 계층이다. Express/Fastify의 미들웨어와 동일한 개념으로, request와 response 객체에 직접 접근하며 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단계
- DI 필요 여부 결정 — 서비스 주입이 필요하면 클래스, 아니면 함수형
- 적용 범위 설정 — 모든 라우트면
forRoutes('*'), 특정 컨트롤러면 클래스 참조 - 제외 라우트 명시 — 헬스체크, 메트릭 엔드포인트는
exclude()로 제외 - 실행 순서 의식 —
apply(A, B, C)는 A→B→C 순서, 보안→로깅→비즈니스 순으로 배치 - next() 호출 보장 — next()를 빠뜨리면 요청이 영원히 멈춘다
- 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가 다른 모듈 라우트에 적용되지 않음
증상: UsersModule의 configure()에서 등록한 Middleware가 OrdersController에 적용되지 않는다.
방지: Middleware는 등록한 모듈의 범위 내에서만 적용된다. 모든 모듈에 적용하려면 AppModule(루트 모듈)의 configure()에서 등록하거나 app.use()로 글로벌 등록한다.
마무리
NestJS Middleware는 요청 파이프라인의 가장 앞단에서 req/res를 가공하는 범용 계층이다. ExecutionContext 없이 동작하므로 인가보다는 로깅, 상관관계 ID, 보안 헤더 같은 횡단 관심사에 적합하다. 클래스 방식은 DI를, 함수 방식은 단순함을 제공하며, forRoutes()와 exclude()로 정밀한 적용 범위를 제어할 수 있다. 이 글의 모든 내용은 NestJS 공식 문서(Middleware)를 근거로 한다.