코딩일상

[nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[Middleware] 본문

개발 공부/nest.js

[nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[Middleware]

solutionMan 2025. 12. 14. 14:03
반응형


1. Middleware의 본질

Middleware는 요청-응답 사이클에서 라우트 핸들러가 실행되기 전에 호출되는 함수입니다. Express의 middleware와 동일한 개념이며, NestJS는 Express 위에 구축되어 있어 Express middleware를 그대로 사용할 수 있습니다.

공항 보안 검색대를 생각해보세요:

승객(Request) → 보안검색(Middleware 1) → 세관검사(Middleware 2) → 탑승구(Controller)
                    ↓                        ↓
              위험물 차단              서류 확인

각 middleware는 다음 작업을 수행할 수 있습니다:

  • 요청/응답 객체에 접근 및 수정
  • 요청-응답 사이클 종료
  • 스택의 다음 middleware 호출 (next())
  • 다음 middleware를 호출하지 않으면 요청이 중단됨
  •  

2. Middleware 구현 방법

2.1 함수형 Middleware (Function Middleware)

가장 간단한 형태

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

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // 반드시 호출해야 다음 단계로 진행
}

실제 사용 예시 - API 호출 시간 측정

// performance.middleware.ts
export function performanceLogger(req: Request, res: Response, next: NextFunction) {
  const startTime = Date.now();
  
  // 응답이 완료되면 실행
  res.on('finish', () => {
    const duration = Date.now() - startTime;
    console.log(`${req.method} ${req.url} - ${duration}ms - ${res.statusCode}`);
  });
  
  next();
}

2.2 클래스 기반 Middleware (Class Middleware)

의존성 주입이 필요한 경우

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

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private jwtService: JwtService) {} // DI 가능!

  async use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers.authorization?.split(' ')[1];
    
    if (!token) {
      return res.status(401).json({ message: 'Token required' });
    }

    try {
      const payload = await this.jwtService.verifyAsync(token);
      req['user'] = payload; // Request 객체에 사용자 정보 추가
      next();
    } catch (error) {
      return res.status(401).json({ message: 'Invalid token' });
    }
  }
}

실제 사용 예시 - Request ID 생성기

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

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

3. Middleware 적용 방법

3.1 모듈 단위 적용

// app.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { AuthMiddleware } from './common/middleware/auth.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 1. 특정 경로에만 적용
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');

    // 2. HTTP 메서드 지정
    consumer
      .apply(AuthMiddleware)
      .forRoutes({ path: 'cats', method: RequestMethod.POST });

    // 3. 여러 middleware 체인
    consumer
      .apply(LoggerMiddleware, AuthMiddleware)
      .forRoutes(CatsController);

    // 4. 특정 경로 제외
    consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'cats', method: RequestMethod.GET },
        { path: 'cats/(.*)', method: RequestMethod.POST },
        'cats/(.*)', // 와일드카드 지원
      )
      .forRoutes(CatsController);

    // 5. 모든 경로에 적용
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');
  }
}

3.2 실제 프로젝트 구조 예시

// app.module.ts
@Module({
  imports: [
    AuthModule,
    UsersModule,
    VehiclesModule,
    ChargingModule,
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 1. 모든 요청에 로깅
    consumer
      .apply(RequestLoggerMiddleware)
      .forRoutes('*');

    // 2. API 경로만 인증 체크
    consumer
      .apply(JwtAuthMiddleware)
      .exclude(
        { path: 'auth/login', method: RequestMethod.POST },
        { path: 'auth/register', method: RequestMethod.POST },
        { path: 'health', method: RequestMethod.GET },
      )
      .forRoutes({ path: 'api/*', method: RequestMethod.ALL });

    // 3. 관리자 경로만 관리자 권한 체크
    consumer
      .apply(AdminAuthMiddleware)
      .forRoutes({ path: 'admin/*', method: RequestMethod.ALL });

    // 4. Rate limiting for public APIs
    consumer
      .apply(RateLimitMiddleware)
      .forRoutes(
        { path: 'api/public/*', method: RequestMethod.ALL },
      );
  }
}

4. 고급 패턴 및 실전 예시

4.1 Request Context 관리

// context.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { ClsService } from 'nestjs-cls'; // 또는 AsyncLocalStorage 사용

@Injectable()
export class ContextMiddleware implements NestMiddleware {
  constructor(private cls: ClsService) {}

  use(req: Request, res: Response, next: NextFunction) {
    // 요청 전체에서 사용 가능한 context 설정
    this.cls.run(() => {
      this.cls.set('requestId', req['id']);
      this.cls.set('userId', req['user']?.id);
      this.cls.set('timestamp', new Date());
      next();
    });
  }
}

4.2 CORS 커스텀 Middleware

// cors.middleware.ts
@Injectable()
export class CustomCorsMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const allowedOrigins = [
      'https://pmgrow.com',
      'https://admin.pmgrow.com',
    ];
    
    const origin = req.headers.origin;
    
    if (allowedOrigins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
      res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
      res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    }

    // Preflight request 처리
    if (req.method === 'OPTIONS') {
      return res.sendStatus(204);
    }

    next();
  }
}

4.3 Request Body 검증 및 변환

// sanitize.middleware.ts
@Injectable()
export class SanitizeMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    if (req.body) {
      // XSS 방지: HTML 태그 제거
      this.sanitizeObject(req.body);
    }
    next();
  }

  private sanitizeObject(obj: any) {
    for (const key in obj) {
      if (typeof obj[key] === 'string') {
        // 간단한 HTML 태그 제거 (실제로는 DOMPurify 등 사용 권장)
        obj[key] = obj[key].replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
        obj[key] = obj[key].trim();
      } else if (typeof obj[key] === 'object') {
        this.sanitizeObject(obj[key]);
      }
    }
  }
}

4.4 Rate Limiting Middleware

// rate-limit.middleware.ts
import { Injectable, NestMiddleware, HttpException, HttpStatus } from '@nestjs/common';
import { RedisService } from '../redis/redis.service';

@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
  constructor(private redisService: RedisService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const ip = req.ip;
    const key = `rate-limit:${ip}`;
    const limit = 100; // 100 requests
    const window = 60; // per 60 seconds

    const current = await this.redisService.get(key);
    
    if (!current) {
      await this.redisService.setex(key, window, '1');
      return next();
    }

    const count = parseInt(current);
    
    if (count >= limit) {
      throw new HttpException(
        'Too many requests',
        HttpStatus.TOO_MANY_REQUESTS,
      );
    }

    await this.redisService.incr(key);
    next();
  }
}

4.5 데이터베이스 트랜잭션 컨텍스트

// transaction.middleware.ts
@Injectable()
export class TransactionMiddleware implements NestMiddleware {
  constructor(private dataSource: DataSource) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    req['queryRunner'] = queryRunner;

    res.on('finish', async () => {
      if (res.statusCode >= 200 && res.statusCode < 300) {
        await queryRunner.commitTransaction();
      } else {
        await queryRunner.rollbackTransaction();
      }
      await queryRunner.release();
    });

    next();
  }
}

5. 글로벌 Middleware

함수형 미들웨어를 전역으로 적용

// 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);
  
  // 글로벌 미들웨어 적용
  app.use(logger);
  
  // 여러 개 적용 가능
  app.use(cors());
  app.use(helmet());
  app.use(compression());
  
  await app.listen(3000);
}
bootstrap();

주의사항:

  • app.use()로 적용된 미들웨어는 의존성 주입 사용 불가
  • 의존성이 필요하면 모듈의 configure() 메서드에서 적용

6. Middleware vs Guards vs Interceptors

특징 Middleware Guards Interceptors
실행 시점 라우트 핸들러 이전 미들웨어 이후, 핸들러 이전 핸들러 전후
접근 가능 Request, Response ExecutionContext ExecutionContext, 반환값
용도 로깅, CORS, 파싱 인증/인가 변환, 캐싱, 예외 처리
DI 제한적 (app.use 시) 완전 지원 완전 지원
다음 호출 next() 필수 true/false 반환 handle() 호출

실행 순서

Request
  ↓
Middleware (global)
  ↓
Middleware (module)
  ↓
Guards (global → controller → route)
  ↓
Interceptors (before)
  ↓
Pipes
  ↓
Route Handler
  ↓
Interceptors (after)
  ↓
Exception Filters
  ↓
Response

7. 실전 프로젝트 예시 - 완전한 구현

프로젝트 구조

src/
├── common/
│   └── middleware/
│       ├── logger.middleware.ts
│       ├── auth.middleware.ts
│       ├── rate-limit.middleware.ts
│       └── request-context.middleware.ts
├── app.module.ts
└── main.ts

통합 예시

// logger.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';

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

  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl, ip } = req;
    const userAgent = req.get('user-agent') || '';
    const startTime = Date.now();

    res.on('finish', () => {
      const { statusCode } = res;
      const contentLength = res.get('content-length');
      const duration = Date.now() - startTime;

      this.logger.log(
        `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip} - ${duration}ms`
      );
    });

    next();
  }
}
// auth.middleware.ts
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../../users/users.service';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(
    private jwtService: JwtService,
    private usersService: UsersService,
  ) {}

  async use(req: Request, res: Response, next: NextFunction) {
    try {
      const token = this.extractTokenFromHeader(req);
      
      if (!token) {
        throw new UnauthorizedException('Token not provided');
      }

      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET,
      });

      // 사용자 정보 조회 (선택사항)
      const user = await this.usersService.findOne(payload.sub);
      
      if (!user) {
        throw new UnauthorizedException('User not found');
      }

      // Request 객체에 사용자 정보 첨부
      req['user'] = user;
      
      next();
    } catch (error) {
      throw new UnauthorizedException('Invalid token');
    }
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}
// app.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { AuthMiddleware } from './common/middleware/auth.middleware';
import { RequestContextMiddleware } from './common/middleware/request-context.middleware';
import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';

@Module({
  imports: [
    UsersModule,
    VehiclesModule,
    ChargingModule,
    AuthModule,
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 1. 모든 요청 로깅
    consumer
      .apply(LoggerMiddleware, RequestContextMiddleware)
      .forRoutes('*');

    // 2. 공개 API Rate Limiting
    consumer
      .apply(RateLimitMiddleware)
      .forRoutes(
        { path: 'auth/login', method: RequestMethod.POST },
        { path: 'auth/register', method: RequestMethod.POST },
      );

    // 3. 보호된 라우트 인증
    consumer
      .apply(AuthMiddleware)
      .exclude(
        // 제외할 경로들
        { path: 'auth/login', method: RequestMethod.POST },
        { path: 'auth/register', method: RequestMethod.POST },
        { path: 'health', method: RequestMethod.GET },
        'docs/(.*)', // API 문서
      )
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

8. 주의사항 및 Best Practices

8.1 반드시 next() 호출하기

// ❌ 나쁜 예
export class BadMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Processing...');
    // next()를 호출하지 않으면 요청이 멈춤!
  }
}

// ✅ 좋은 예
export class GoodMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Processing...');
    next(); // 반드시 호출
  }
}

8.2 에러 처리

@Injectable()
export class ErrorHandlingMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    try {
      // 처리 로직
      next();
    } catch (error) {
      // NestJS의 예외 필터로 전달
      next(error);
    }
  }
}

8.3 비동기 작업

@Injectable()
export class AsyncMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    try {
      await someAsyncOperation();
      next();
    } catch (error) {
      next(error);
    }
  }
}

8.4 성능 고려사항

// ❌ 나쁜 예: 모든 요청마다 DB 조회
@Injectable()
export class SlowMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    const config = await this.configRepository.findOne(); // 매번 DB 조회
    req['config'] = config;
    next();
  }
}

// ✅ 좋은 예: 캐싱 활용
@Injectable()
export class FastMiddleware implements NestMiddleware {
  private configCache: any;
  
  async use(req: Request, res: Response, next: NextFunction) {
    if (!this.configCache) {
      this.configCache = await this.configRepository.findOne();
    }
    req['config'] = this.configCache;
    next();
  }
}

9. 요약 및 언제 사용할까?

상기에 대한 더 자세한 이유는 다음 포스팅에서 만들어가 보겠다.

Middleware를 사용해야 하는 경우

  1. ✅ 모든 요청에 대한 로깅
  2. ✅ CORS 설정
  3. ✅ Request/Response 객체 직접 조작
  4. ✅ 라우트 핸들러 실행 전 전처리
  5. ✅ Body 파싱, 압축, 보안 헤더 설정

Guards를 사용해야 하는 경우

  1. ✅ 인증/인가 (권한 체크)
  2. ✅ 특정 조건에서 라우트 접근 차단
  3. ✅ ExecutionContext 필요시

Interceptors를 사용해야 하는 경우

  1. ✅ 응답 데이터 변환
  2. ✅ 캐싱
  3. ✅ 성능 측정
  4. ✅ 응답 후 추가 작업

 

반응형
Comments