NestJS

6. NestJS 미들웨어: 요청 처리 파이프라인 이해하기

오늘의하늘은 2025. 3. 30. 15:32

웹 애플리케이션 개발에서 미들웨어는 요청과 응답 사이에 위치하여 다양한 작업을 수행하는 함수입니다. NestJS에서는 미들웨어를 통해 HTTP 요청 처리 과정에 개입하여 로깅, 인증, 데이터 변환 등의 작업을 효과적으로 처리할 수 있습니다. 이번 글에서는 NestJS의 미들웨어 개념과 활용 방법을 자세히 알아보겠습니다.

목차

  1. 미들웨어란 무엇인가?
  2. NestJS에서 미들웨어 작성하기
  3. 미들웨어 적용 방법
  4. 함수형 미들웨어
  5. 글로벌 미들웨어
  6. 미들웨어 소비자
  7. 여러 미들웨어 연결하기
  8. 라우트 와일드카드와 미들웨어
  9. 서드파티 미들웨어 활용하기
  10. 요청 처리 파이프라인의 전체 흐름
  11. 실전 예제
  12. 정리

미들웨어란 무엇인가?

미들웨어는 라우트 핸들러가 요청을 처리하기 전에 실행되는 함수입니다. 미들웨어는 요청 객체(request), 응답 객체(response), 그리고 애플리케이션의 요청-응답 주기에서 다음 미들웨어 함수에 대한 접근 권한을 가집니다.

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

  • 코드 실행
  • 요청 및 응답 객체 변경
  • 요청-응답 주기 종료
  • 스택의 다음 미들웨어 함수 호출
  • 현재 미들웨어 함수가 요청-응답 주기를 종료하지 않으면 next()를 호출하여 다음 미들웨어 함수에 제어권을 전달

NestJS의 미들웨어는 기본적으로 Express의 미들웨어와 동일하며, Express 문서에서 설명하는 미들웨어 기능을 모두 사용할 수 있습니다.

NestJS에서 미들웨어 작성하기

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

  1. 클래스를 사용하여 작성하는 방법 (@Injectable() 데코레이터와 NestMiddleware 인터페이스 구현)
  2. 함수를 사용하여 작성하는 방법 (간단한 함수로 구현)

먼저, 클래스를 사용하여 미들웨어를 작성하는 방법을 살펴보겠습니다:

// src/common/middleware/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) {
    console.log(`Request... Method: ${req.method}, URL: ${req.url}`);
    console.log(`Request headers:`, req.headers);
    console.log(`Request body:`, req.body);
    
    // 요청 시작 시간 기록
    const startTime = Date.now();
    
    // 응답이 완료된 후 실행될 리스너 추가
    res.on('finish', () => {
      const endTime = Date.now();
      const responseTime = endTime - startTime;
      console.log(`Response... Status: ${res.statusCode}, Time: ${responseTime}ms`);
    });
    
    next(); // 다음 미들웨어 또는 라우트 핸들러로 제어권 전달
  }
}

이 미들웨어는 요청이 들어올 때마다 요청 메서드, URL, 헤더, 본문을 로깅하고, 응답이 완료된 후에는 상태 코드와 응답 시간을 로깅합니다.

미들웨어 적용 방법

작성한 미들웨어를 적용하려면 모듈의 configure 메서드를 구현해야 합니다. 이 메서드는 NestModule 인터페이스를 구현할 때 필요합니다:

// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { LoggerMiddleware } from './common/middleware/logger.middleware';

@Module({
  imports: [UsersModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 'users' 경로에 LoggerMiddleware 적용
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('users');
  }
}

위 코드에서 .forRoutes('users')는 'users' 경로로 들어오는 모든 요청에 LoggerMiddleware를 적용한다는 의미입니다.

더 구체적으로 경로와 HTTP 메서드를 지정할 수도 있습니다:

import { RequestMethod } from '@nestjs/common';

// ...

configure(consumer: MiddlewareConsumer) {
  consumer
    .apply(LoggerMiddleware)
    .forRoutes({ path: 'users', method: RequestMethod.GET });
}

또는 컨트롤러 클래스를 직접 지정할 수도 있습니다:

import { UsersController } from './users/users.controller';

// ...

configure(consumer: MiddlewareConsumer) {
  consumer
    .apply(LoggerMiddleware)
    .forRoutes(UsersController);
}

함수형 미들웨어

간단한 기능만 필요한 경우 함수형 미들웨어를 사용할 수 있습니다:

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

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

함수형 미들웨어는 다음과 같이 적용합니다:

// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { logger } from './common/middleware/logger.middleware';

@Module({
  // ...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(logger)
      .forRoutes('users');
  }
}

함수형 미들웨어는 의존성 주입(DI)이 필요 없는 간단한 경우에 유용합니다. 클래스 미들웨어는 의존성 주입을 활용해야 하는 복잡한 경우에 더 적합합니다.

글로벌 미들웨어

애플리케이션의 모든 경로에 미들웨어를 적용하려면 INestApplication 인스턴스의 use() 메서드를 사용합니다:

// src/main.ts
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);
  
  // 모든 경로에 logger 미들웨어 적용
  app.use(logger);
  
  await app.listen(3000);
}
bootstrap();

app.use()는 함수형 미들웨어만 사용할 수 있다는 제한이 있습니다. 클래스 미들웨어를 글로벌하게 적용하려면 의존성 주입을 위해 별도의 글로벌 모듈을 만들어야 합니다.

미들웨어 소비자

MiddlewareConsumer는 미들웨어를 관리하기 위한 헬퍼 클래스입니다. 이를 사용하면 미들웨어를 특정 경로에 적용하거나 특정 경로에서 제외할 수 있습니다:

configure(consumer: MiddlewareConsumer) {
  consumer
    .apply(LoggerMiddleware)
    .exclude(
      { path: 'users', method: RequestMethod.GET },
      { path: 'users/create', method: RequestMethod.POST },
    )
    .forRoutes(UsersController);
}

이 예제에서는 UsersController의 모든 라우트에 LoggerMiddleware를 적용하되, GET /users와 POST /users/create 경로는 제외합니다.

여러 미들웨어 연결하기

여러 미들웨어를 순차적으로 실행하려면 apply() 메서드에 여러 미들웨어를 전달하면 됩니다:

consumer
  .apply(cors(), helmet(), logger)
  .forRoutes(UsersController);

이 예제에서는 cors, helmet, logger 미들웨어가 순서대로 실행됩니다.

라우트 와일드카드와 미들웨어

미들웨어 라우트 경로에도 와일드카드를 사용할 수 있습니다:

consumer
  .apply(LoggerMiddleware)
  .forRoutes('users*');

이 설정은 'users'로 시작하는 모든 경로(예: /users, /users/123, /users/profile 등)에 LoggerMiddleware를 적용합니다.

서드파티 미들웨어 활용하기

Express 생태계에는 많은 미들웨어가 있으며, NestJS에서도 이러한 미들웨어를 쉽게 사용할 수 있습니다. 다음은 몇 가지 유용한 서드파티 미들웨어와 그 적용 예시입니다:

CORS(Cross-Origin Resource Sharing)

다른 도메인에서의 요청을 허용하기 위한 CORS 설정:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // CORS 활성화
  app.enableCors({
    origin: 'http://example.com', // 특정 도메인만 허용하거나, true로 설정하여 모든 도메인 허용
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
    credentials: true, // 쿠키를 포함한 요청 허용
  });
  
  await app.listen(3000);
}
bootstrap();

Helmet

Helmet은 HTTP 헤더를 설정하여 웹 취약성으로부터 앱을 보호합니다:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // Helmet 미들웨어 적용
  app.use(helmet());
  
  await app.listen(3000);
}
bootstrap();

압축(Compression)

응답 본문을 압축하여 전송 속도를 향상시킵니다:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // Compression 미들웨어 적용
  app.use(compression());
  
  await app.listen(3000);
}
bootstrap();

쿠키 파서(Cookie Parser)

쿠키를 파싱하여 req.cookies 객체로 만듭니다:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // CookieParser 미들웨어 적용
  app.use(cookieParser());
  
  await app.listen(3000);
}
bootstrap();

세션(Session)

사용자 세션을 관리합니다:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // Session 미들웨어 적용
  app.use(
    session({
      secret: 'my-secret', // 실제 애플리케이션에서는 환경 변수 등을 통해 안전하게 관리해야 함
      resave: false,
      saveUninitialized: false,
      cookie: { secure: process.env.NODE_ENV === 'production' },
    }),
  );
  
  await app.listen(3000);
}
bootstrap();

요청 처리 파이프라인의 전체 흐름

NestJS에서 HTTP 요청이 처리되는 전체 파이프라인을 이해하는 것이 중요합니다. 다음은 요청이 들어와서 응답이 나가기까지의 일반적인 흐름입니다:

  1. 글로벌 미들웨어: app.use()로 등록된 미들웨어가 먼저 실행됩니다.
  2. 모듈 미들웨어: 특정 모듈과 라우트에 등록된 미들웨어가 실행됩니다.
  3. 가드(Guards): 권한 부여, 인증 등을 처리합니다.
  4. 인터셉터(Interceptors)(요청 전): 요청이 핸들러에 도달하기 전에 실행됩니다.
  5. 파이프(Pipes): 요청 유효성 검사와 변환을 처리합니다.
  6. 컨트롤러: 실제 요청 핸들러가 실행됩니다.
  7. 인터셉터(Interceptors)(응답 후): 응답이 클라이언트로 전송되기 전에 실행됩니다.
  8. 예외 필터(Exception Filters): 처리되지 않은 예외를 잡아 적절한 응답을 보냅니다.

미들웨어는 이 파이프라인의 첫 번째 단계로, 가드, 파이프, 인터셉터 등이 실행되기 전에 요청을 처리할 수 있습니다.

실전 예제

이제 지금까지 배운 내용을 활용하여 실용적인 미들웨어 예제를 몇 가지 살펴보겠습니다.

요청 ID 미들웨어

각 요청에 고유한 ID를 부여하여 로깅과 디버깅을 용이하게 합니다:

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

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const requestId = uuidv4();
    req['requestId'] = requestId;
    res.setHeader('X-Request-Id', requestId);
    next();
  }
}

요청 타임아웃 미들웨어

요청 처리 시간이 지정된 시간을 초과하면 타임아웃 에러를 발생시킵니다:

// src/common/middleware/timeout.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class TimeoutMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // 타임아웃 시간 설정 (5초)
    const timeout = 5000;
    let isTimeout = false;
    
    // 타임아웃 타이머 설정
    const timeoutId = setTimeout(() => {
      isTimeout = true;
      res.status(408).json({ message: 'Request Timeout' });
    }, timeout);
    
    // 원래의 end 메서드를 저장
    const originalEnd = res.end;
    
    // res.end 메서드 오버라이드
    res.end = function(...args) {
      // 이미 타임아웃되지 않았다면 타이머를 클리어하고 원래의 end 호출
      if (!isTimeout) {
        clearTimeout(timeoutId);
        originalEnd.apply(res, args);
      }
      return res;
    };
    
    next();
  }
}

로깅 및 측정 미들웨어

요청 처리 성능을 측정하고 로깅하는 미들웨어:

// src/common/middleware/performance-logger.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class PerformanceLoggerMiddleware implements NestMiddleware {
  private readonly logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    // 요청 시작 시간
    const startTime = Date.now();
    const { method, originalUrl } = req;
    
    // 응답이 전송된 후 로깅
    res.on('finish', () => {
      const { statusCode } = res;
      const contentLength = res.get('content-length') || 0;
      const responseTime = Date.now() - startTime;
      
      this.logger.log(
        `${method} ${originalUrl} ${statusCode} ${contentLength} - ${responseTime}ms`,
      );
      
      // 느린 요청 경고
      if (responseTime > 1000) {
        this.logger.warn(`Slow request detected: ${method} ${originalUrl} - ${responseTime}ms`);
      }
    });
    
    next();
  }
}

IP 제한 미들웨어

특정 IP 주소의 접근을 제한하는 미들웨어:

// src/common/middleware/ip-filter.middleware.ts
import { Injectable, NestMiddleware, ForbiddenException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class IpFilterMiddleware implements NestMiddleware {
  private readonly allowedIps = ['127.0.0.1', '::1']; // 허용된 IP 목록

  use(req: Request, res: Response, next: NextFunction) {
    const clientIp = req.ip || 
                     req.connection.remoteAddress || 
                     req.headers['x-forwarded-for'] as string;
    
    if (!this.allowedIps.includes(clientIp)) {
      throw new ForbiddenException('Access denied');
    }
    
    next();
  }
}

모든 미들웨어 적용하기

위에서 만든 여러 미들웨어를 애플리케이션에 적용해보겠습니다:

// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { RequestIdMiddleware } from './common/middleware/request-id.middleware';
import { TimeoutMiddleware } from './common/middleware/timeout.middleware';
import { PerformanceLoggerMiddleware } from './common/middleware/performance-logger.middleware';
import { IpFilterMiddleware } from './common/middleware/ip-filter.middleware';

@Module({
  imports: [UsersModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 모든 라우트에 RequestId와 PerformanceLogger 미들웨어 적용
    consumer
      .apply(RequestIdMiddleware, PerformanceLoggerMiddleware)
      .forRoutes('*');
    
    // 관리자 경로에만 IP 필터 미들웨어 적용
    consumer
      .apply(IpFilterMiddleware)
      .forRoutes('admin');
    
    // 파일 업로드 경로에만 타임아웃 미들웨어 적용
    consumer
      .apply(TimeoutMiddleware)
      .forRoutes({ path: 'upload', method: RequestMethod.POST });
  }
}

정리

미들웨어는 NestJS 애플리케이션에서 요청 처리 파이프라인의 중요한 부분을 담당합니다. 이번 글에서는 다음 내용을 다루었습니다:

  • 미들웨어의 개념과 역할
  • 클래스 미들웨어와 함수형 미들웨어의 작성 방법
  • 미들웨어를 특정 경로에 적용하는 방법
  • 글로벌 미들웨어 설정 방법
  • 여러 미들웨어를 연결하는 방법
  • 서드파티 미들웨어 활용 방법
  • 요청 처리 파이프라인의 전체 흐름
  • 실용적인 미들웨어 예제

미들웨어는 요청 로깅, 인증, 데이터 변환, 에러 처리 등 다양한 크로스커팅 관심사(cross-cutting concerns)를 처리하는 데 매우 유용합니다. 잘 설계된 미들웨어는 코드의 재사용성을 높이고, 컨트롤러와 서비스가 핵심 비즈니스 로직에만 집중할 수 있게 해줍니다.

다음 글에서는 NestJS의 예외 필터(Exception Filters)에 대해 알아보고, 애플리케이션에서 발생하는 오류를 효과적으로 처리하는 방법을 살펴보겠습니다.


 

반응형