NestJS Middleware란?
NestJS의 Middleware는 라우트 핸들러 실행 전에 요청/응답 객체에 접근하는 함수다. Express/Fastify 미들웨어와 동일한 개념이며, NestJS 요청 라이프사이클에서 가장 먼저 실행된다. Guard, Interceptor, Pipe보다 앞서 동작하므로, 인증 토큰 파싱, 요청 로깅, CORS, 요청 본문 변환 등 저수준 전처리에 적합하다.
요청 라이프사이클에서의 위치
| 순서 | 단계 | 특징 |
|---|---|---|
| 1 | Middleware | req/res 직접 접근, next() 호출 |
| 2 | Guard | ExecutionContext, 인가 결정 |
| 3 | Interceptor (Before) | RxJS Observable, AOP |
| 4 | Pipe | 유효성 검증, 변환 |
| 5 | Handler | 컨트롤러 메서드 |
Middleware는 Express/Fastify의 req, res, next에 직접 접근하므로, ExecutionContext가 필요 없는 범용적인 전처리에 사용한다.
클래스 Middleware: DI 지원
NestJS의 DI 컨테이너를 활용할 수 있는 클래스 기반 미들웨어:
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
private readonly logger = new Logger('HTTP');
use(req: Request, res: Response, next: NextFunction) {
const { method, originalUrl, ip } = req;
const userAgent = req.get('user-agent') || '';
const start = Date.now();
res.on('finish', () => {
const { statusCode } = res;
const duration = Date.now() - start;
this.logger.log(
`${method} ${originalUrl} ${statusCode} ${duration}ms - ${ip} ${userAgent}`,
);
});
next();
}
}
res.on('finish')를 활용하면 응답 완료 후 상태 코드와 처리 시간을 기록할 수 있다. @Injectable() 데코레이터로 다른 서비스를 주입받을 수 있다는 것이 함수형 미들웨어와의 핵심 차이다.
함수형 Middleware: 간단한 처리
DI가 필요 없는 간단한 미들웨어는 함수로 작성한다:
import { Request, Response, NextFunction } from 'express';
// 요청 ID 부여
export function requestIdMiddleware(req: Request, res: Response, next: NextFunction) {
const requestId = req.headers['x-request-id'] as string
|| crypto.randomUUID();
req['requestId'] = requestId;
res.setHeader('X-Request-Id', requestId);
next();
}
// JSON 응답 시간 헤더
export function responseTimeMiddleware(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
res.setHeader('X-Response-Time', `${Date.now() - start}ms`);
});
next();
}
Middleware 등록: Module의 configure
미들웨어는 모듈의 configure 메서드에서 등록한다:
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
@Module({ ... })
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
// 모든 라우트에 적용
.apply(RequestLoggerMiddleware, requestIdMiddleware)
.forRoutes('*')
// 특정 경로+메서드에만 적용
.apply(AuthTokenMiddleware)
.exclude(
{ path: 'health', method: RequestMethod.GET },
{ path: 'auth/login', method: RequestMethod.POST },
)
.forRoutes('*')
// 특정 컨트롤러에만 적용
.apply(AdminAuditMiddleware)
.forRoutes(AdminController);
}
}
apply()에 여러 미들웨어를 전달하면 순서대로 실행된다. exclude()로 특정 경로를 제외할 수 있고, forRoutes()에 컨트롤러 클래스를 넘길 수도 있다.
글로벌 Middleware: main.ts
모든 라우트에 무조건 적용할 미들웨어는 main.ts에서 등록한다:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 글로벌 미들웨어 (함수형만 가능 — DI 없음)
app.use(requestIdMiddleware);
app.use(helmet());
app.use(compression());
// CORS 설정
app.enableCors({
origin: ['https://app.example.com'],
credentials: true,
});
await app.listen(3000);
}
bootstrap();
주의: app.use()로 등록하는 글로벌 미들웨어는 DI를 사용할 수 없다. DI가 필요한 글로벌 미들웨어는 AppModule.configure()에서 forRoutes('*')로 등록해야 한다.
실전 패턴 1: 인증 토큰 파싱
Guard에서 인가를 처리하기 전에, Middleware에서 토큰을 파싱해 req.user에 세팅하는 패턴:
@Injectable()
export class AuthTokenMiddleware implements NestMiddleware {
constructor(private readonly jwtService: JwtService) {}
use(req: Request, res: Response, next: NextFunction) {
const token = this.extractToken(req);
if (token) {
try {
const payload = this.jwtService.verify(token);
req['user'] = payload;
} catch {
// 토큰 검증 실패 → req.user는 undefined
// Guard에서 인가 결정을 내림
}
}
next(); // 토큰이 없어도 다음으로 진행 (Guard가 처리)
}
private extractToken(req: Request): string | null {
const auth = req.headers.authorization;
if (auth?.startsWith('Bearer ')) {
return auth.slice(7);
}
return req.cookies?.['access_token'] || null;
}
}
Middleware는 토큰 파싱만 담당하고, 인가 결정은 NestJS Guard에서 처리한다. 관심사를 분리하면 Public API와 Protected API를 유연하게 처리할 수 있다.
실전 패턴 2: 요청 본문 로깅 + 감사
@Injectable()
export class AuditMiddleware implements NestMiddleware {
constructor(private readonly auditService: AuditService) {}
use(req: Request, res: Response, next: NextFunction) {
// 읽기 요청은 건너뜀
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const auditData = {
method: req.method,
path: req.originalUrl,
userId: req['user']?.sub,
ip: req.ip,
body: this.sanitizeBody(req.body),
timestamp: new Date(),
};
// 비동기로 감사 로그 저장 (요청 블로킹 없음)
this.auditService.log(auditData).catch(() => {});
next();
}
private sanitizeBody(body: any): any {
if (!body) return null;
const sanitized = { ...body };
// 민감 필드 마스킹
const sensitiveFields = ['password', 'token', 'secret', 'creditCard'];
for (const field of sensitiveFields) {
if (sanitized[field]) sanitized[field] = '***';
}
return sanitized;
}
}
실전 패턴 3: Rate Limiting
@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
private readonly store = new Map<string, { count: number; resetAt: number }>();
constructor(
@Inject('RATE_LIMIT_OPTIONS')
private readonly options: { windowMs: number; max: number },
) {}
use(req: Request, res: Response, next: NextFunction) {
const key = req.ip || 'unknown';
const now = Date.now();
const record = this.store.get(key);
if (!record || now > record.resetAt) {
this.store.set(key, { count: 1, resetAt: now + this.options.windowMs });
return next();
}
if (record.count >= this.options.max) {
res.status(429).json({
statusCode: 429,
message: 'Too Many Requests',
retryAfter: Math.ceil((record.resetAt - now) / 1000),
});
return;
}
record.count++;
next();
}
}
Middleware vs Guard vs Interceptor
| 비교 | Middleware | Guard | Interceptor |
|---|---|---|---|
| 접근 대상 | req, res, next | ExecutionContext | ExecutionContext + Observable |
| 핸들러 정보 | ❌ 모름 | ✅ Reflector 사용 가능 | ✅ Reflector 사용 가능 |
| 응답 변환 | res 직접 조작 | ❌ | ✅ RxJS map/tap |
| 적용 범위 | 경로 기반 | 컨트롤러/핸들러 기반 | 컨트롤러/핸들러 기반 |
| 적합한 용도 | 요청 전처리, 로깅 | 인가 결정 | 응답 변환, 캐싱 |
Middleware는 어떤 핸들러가 실행될지 모른다는 점이 핵심 차이다. 커스텀 데코레이터(@Public(), @Roles())에 따라 동작을 바꿔야 하는 로직은 Guard나 Interceptor에서 처리해야 한다.
정리
| 용도 | 권장 위치 |
|---|---|
| 요청 ID, 헤더 추가 | 함수형 Middleware (main.ts) |
| 토큰 파싱, req.user 세팅 | 클래스 Middleware (DI) |
| HTTP 로깅, 감사 | 클래스 Middleware |
| Rate Limiting | 클래스 Middleware |
| CORS, Helmet, Compression | app.use() (main.ts) |
| 인가, 역할 기반 접근 | Guard (Middleware ❌) |
Middleware는 NestJS 요청 파이프라인의 첫 번째 관문이다. Express/Fastify 레벨의 저수준 처리에 집중하고, NestJS의 데코레이터 메타데이터가 필요한 로직은 Guard와 Interceptor에 위임하는 것이 올바른 설계다.