NestJS 미들웨어 가이드

미들웨어는 라우트 핸들러가 요청을 처리하기 전에 실행되는 함수입니다. NestJS의 미들웨어는 Express의 미들웨어와 동일한 개념을 따르며, 요청 및 응답 객체에 접근하여 다양한 작업을 수행할 수 있습니다.

미들웨어의 기본 개념

미들웨어는 다음과 같은 작업을 수행할 수 있습니다:

  1. 코드 실행
  2. 요청 및 응답 객체 변경
  3. 요청-응답 주기 종료
  4. 다음 미들웨어 함수 호출
  5. 특정 조건에 따라 라우트 핸들러 건너뛰기

NestJS에서 미들웨어 구현 방법

NestJS에서는 미들웨어를 두 가지 방식으로 구현할 수 있습니다:

미들웨어 구현 방법

NestJS에서 미들웨어를 구현하는 방법에는 두 가지가 있습니다:

1. 함수형 미들웨어

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
}

2. 클래스형 미들웨어 (NestMiddleware 인터페이스 구현)

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) {
    console.log('Request...');
    next();
  }
}

미들웨어 적용 방법

미들웨어는 모듈 클래스의 configure() 메서드를 사용하여 적용합니다. 이를 위해 모듈은 NestModule 인터페이스를 구현해야 합니다.

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
 
@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

미들웨어 적용 대상 지정 방법

경로 기반 적용

consumer
  .apply(LoggerMiddleware)
  .forRoutes('cats');

컨트롤러 기반 적용

consumer
  .apply(LoggerMiddleware)
  .forRoutes(CatsController);

HTTP 메서드 기반 적용

import { RequestMethod } from '@nestjs/common';
 
consumer
  .apply(LoggerMiddleware)
  .forRoutes({ path: 'cats', method: RequestMethod.GET });

여러 미들웨어 적용

consumer
  .apply(cors(), helmet(), LoggerMiddleware)
  .forRoutes(CatsController);

특정 경로 제외

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/(.*)',
  )
  .forRoutes(CatsController);

전역 미들웨어

전체 애플리케이션에 미들웨어를 적용하려면 main.ts 파일에서 use() 메서드를 사용합니다:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './common/middleware/logger.middleware';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(logger);
  await app.listen(3000);
}
bootstrap();

미들웨어의 실제 사용 사례

1. 로깅 미들웨어

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private logger = new Logger('HTTP');
 
  use(req: Request, res: Response, next: NextFunction) {
    const { ip, method, originalUrl } = req;
    const userAgent = req.get('user-agent') || '';
 
    res.on('finish', () => {
      const { statusCode } = res;
      const contentLength = res.get('content-length');
 
      this.logger.log(
        `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
      );
    });
 
    next();
  }
}

2. 인증 미들웨어

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private readonly jwtService: JwtService) {}
 
  use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers.authorization?.split(' ')[1];
    
    if (!token) {
      return res.status(401).json({ message: 'No token provided' });
    }
    
    try {
      const decoded = this.jwtService.verify(token);
      req.user = decoded;
      next();
    } catch (error) {
      return res.status(401).json({ message: 'Invalid token' });
    }
  }
}

3. CORS 미들웨어

import * as cors from 'cors';
 
// main.ts
app.use(cors());

요청 처리 프로세스에서 미들웨어의 역할

위 다이어그램은 NestJS의 요청 처리 프로세스와 그 안에서 미들웨어의 위치를 보여줍니다. 미들웨어는 요청 처리 파이프라인의 가장 앞부분에 위치하며, 다음과 같은 중요한 역할을 수행합니다:

  1. 요청 전처리: 클라이언트로부터 받은 요청을 라우트 핸들러가 처리하기 전에 전처리합니다.
  2. 요청 필터링: 특정 조건에 따라 요청을 필터링하거나 차단할 수 있습니다.
  3. 공통 기능 제공: 로깅, 인증, CORS 처리 등 여러 라우트에서 공통적으로 필요한 기능을 제공합니다.
  4. 요청 변형: 요청 객체를 수정하거나 확장하여 추가 정보를 제공할 수 있습니다.
  5. 응답 수정: 응답 객체에 접근하여 헤더를 추가하거나 다른 수정을 할 수 있습니다.

요청 처리 프로세스 단계별 흐름

NestJS의 요청 처리 프로세스는 다음과 같은 순서로 진행됩니다:

  1. HTTP 요청 수신: 클라이언트로부터 요청이 들어옵니다.
  2. 미들웨어 실행: 적용된 모든 글로벌 미들웨어와 라우트별 미들웨어가 순차적으로 실행됩니다.
    • 이 단계에서 로깅, 인증, 요청 본문 파싱(JSON, URL 인코딩 등) 등이 처리됩니다.
  3. 가드 실행: 특정 라우트에 대한 접근 권한을 확인합니다.
  4. 인터셉터(전처리): 요청 객체를 변환하거나 추가 로직을 실행합니다.
  5. 파이프: 입력 데이터 유효성 검사 및 변환을 수행합니다.
  6. 컨트롤러/라우트 핸들러: 비즈니스 로직을 실행하고 응답을 생성합니다.
  7. 인터셉터(후처리): 응답을 수정하거나 추가 로직을 실행합니다.
  8. HTTP 응답 반환: 클라이언트에게 응답을 전송합니다.

미들웨어와 다른 NestJS 컴포넌트의 차이점

구성 요소실행 시점주요 용도
미들웨어라우트 핸들러 이전로깅, 인증, 요청 파싱
가드미들웨어 이후, 라우트 핸들러 이전권한 검사, 역할 기반 접근 제어
인터셉터가드 이후, 라우트 핸들러 전후요청/응답 변환, 캐싱, 로깅
파이프인터셉터와 라우트 핸들러 사이데이터 유효성 검사 및 변환
예외 필터전체 프로세스에서 예외 발생 시예외 처리 및 응답 형식 지정

실제 미들웨어 활용 시나리오

1. API 요청 로깅

@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
  private logger = new Logger('API');
 
  use(req: Request, res: Response, next: NextFunction) {
    const startAt = process.hrtime();
    const { method, originalUrl } = req;
    
    res.on('finish', () => {
      const [seconds, nanoseconds] = process.hrtime(startAt);
      const duration = seconds * 1000 + nanoseconds / 1000000;
      this.logger.log(`${method} ${originalUrl} ${res.statusCode} - ${duration.toFixed(2)}ms`);
    });
    
    next();
  }
}

2. 사용자 인증 및 정보 추가

@Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
  constructor(
    private readonly authService: AuthService,
    private readonly userService: UserService,
  ) {}
 
  async use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers.authorization?.split(' ')[1];
    
    if (token) {
      try {
        const decoded = this.authService.verifyToken(token);
        const user = await this.userService.findById(decoded.userId);
        
        if (user) {
          req.user = user;  // 요청 객체에 사용자 정보 추가
        }
      } catch (error) {
        // 토큰 검증 실패 - 사용자 정보는 추가하지 않음
      }
    }
    
    next();  // 인증 여부와 관계없이 다음 단계로 진행 (가드에서 처리)
  }
}

3. 요청 속도 제한 (Rate Limiting)

@Injectable()
export class RateLimiterMiddleware implements NestMiddleware {
  private readonly limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15분
    max: 100, // IP당 15분간 최대 100개 요청
    standardHeaders: true,
    legacyHeaders: false,
  });
 
  use(req: Request, res: Response, next: NextFunction) {
    this.limiter(req, res, next);
  }
}

미들웨어 실행 순서 제어하기

미들웨어는 등록된 순서대로 실행됩니다. 따라서 실행 순서가 중요한 경우에는 순서를 명시적으로 지정해야 합니다:

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(CorsMiddleware, HelmetMiddleware)  // CORS, 보안 헤더 먼저 처리
      .forRoutes('*')
      .apply(LoggerMiddleware, AuthMiddleware)  // 로깅, 인증은 그 다음 처리
      .forRoutes('auth', 'users', 'products');
  }
}

미들웨어 실행 순서

  1. 전역 미들웨어
  2. 모듈 바인딩 미들웨어
  3. 가드
  4. 인터셉터 (컨트롤러 이전)
  5. 파이프
  6. 컨트롤러 핸들러
  7. 인터셉터 (컨트롤러 이후)
  8. 예외 필터

실제 활용 사례

  • 인증/인가 미들웨어
  • 로깅 미들웨어
  • CORS 처리
  • 요청 본문 파싱
  • 세션 관리
  • 성능 모니터링

미들웨어는 NestJS 애플리케이션에서 횡단 관심사(cross-cutting concerns)를 효과적으로 처리하는 강력한 메커니즘을 제공합니다.