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를 활용하되, 단순 로직은 함수형으로 간결하게 유지하세요.