NestJS Request Lifecycle 심화

NestJS Request Lifecycle 전체 흐름

NestJS에서 HTTP 요청이 들어오면 7단계의 파이프라인을 순서대로 통과한다. 각 단계에서 요청을 변환하거나, 차단하거나, 후처리할 수 있다. 이 순서를 정확히 이해해야 미들웨어·가드·인터셉터·파이프를 올바르게 배치할 수 있다.

전체 흐름은 다음과 같다:

  1. Middleware → 2. Guard → 3. Interceptor (before) → 4. Pipe → 5. Route Handler → 6. Interceptor (after) → 7. Exception Filter

1. Middleware: 가장 먼저 실행

Middleware는 Express/Fastify 레벨에서 동작하며, NestJS의 DI 시스템에 진입하기 전에 실행된다. 글로벌 미들웨어 → 모듈 미들웨어 순서로 실행된다.

// 실행 순서: Global Middleware → Module Middleware
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`[Middleware] ${req.method} ${req.url}`);
    // 여기서 next()를 호출하지 않으면 요청이 중단됨
    next();
  }
}

// Module에서 등록
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');
  }
}

용도: 로깅, CORS, 쿠키 파싱, 요청 ID 부여 등 NestJS 기능에 의존하지 않는 범용 처리.

2. Guard: 접근 제어 게이트

GuardcanActivate()true를 반환해야 다음 단계로 진행한다. Global Guard → Controller Guard → Method Guard 순서로 실행된다.

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization;

    if (!token) {
      // false 반환 시 ForbiddenException 자동 발생
      // → Exception Filter로 직행
      return false;
    }
    return true;
  }
}

// 실행 순서 확인
@UseGuards(ControllerGuard)   // 2번째
@Controller('orders')
export class OrderController {
  @UseGuards(MethodGuard)     // 3번째 (Global Guard가 1번째)
  @Get()
  findAll() { ... }
}

핵심: Guard에서 false를 반환하거나 예외를 던지면, Interceptor·Pipe·Handler는 실행되지 않고 바로 Exception Filter로 넘어간다.

3. Interceptor (before): 요청 전처리

Interceptor는 RxJS Observable 기반으로, 핸들러 실행 전후에 모두 개입할 수 있다. next.handle() 호출 전이 “before”, 후가 “after”다.

@Injectable()
export class TimingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // ── before (Handler 실행 전) ──
    const start = Date.now();
    console.log('[Interceptor] Before handler');

    return next.handle().pipe(
      // ── after (Handler 실행 후) ──
      tap(() => {
        console.log(`[Interceptor] After handler: ${Date.now() - start}ms`);
      }),
    );
  }
}

Global Interceptor → Controller Interceptor → Method Interceptor 순서로 실행된다.

4. Pipe: 변환과 검증

Pipe는 핸들러의 파라미터에 대해 동작한다. 변환(transform)과 검증(validation)을 담당한다.

@Controller('orders')
export class OrderController {
  @Post()
  create(
    // Global Pipe → Parameter Pipe 순서
    @Body(new ValidationPipe({ whitelist: true }))
    dto: CreateOrderDto,

    @Param('id', ParseIntPipe)  // '123' → 123 변환
    id: number,
  ) {
    // Pipe 통과 후 dto는 검증 완료, id는 number 타입 보장
    return this.orderService.create(id, dto);
  }
}

핵심: Pipe에서 ValidationError가 발생하면, Handler는 실행되지 않고 Exception Filter로 넘어간다. 이때 Interceptor의 “after” 로직(Observable)도 실행되지 않는다.

5. Route Handler: 비즈니스 로직

모든 전처리를 통과한 후 실제 컨트롤러 메서드가 실행된다.

6. Interceptor (after): 응답 후처리

Handler가 반환한 값은 Interceptor의 next.handle() Observable을 통해 흐른다. 여기서 응답을 변환하거나 캐싱할 수 있다:

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      // Handler 반환값을 표준 응답 포맷으로 래핑
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

주의: after 단계의 실행 순서는 before의 역순이다. Method Interceptor → Controller Interceptor → Global Interceptor 순서로 실행된다 (양파 모델).

7. Exception Filter: 예외 최종 처리

Exception Filter는 파이프라인 어디에서든 발생한 예외를 잡는다. Method Filter → Controller Filter → Global Filter 순서로 매칭을 시도하며, 첫 번째로 매칭되는 필터가 처리한다.

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus();

    response.status(status).json({
      success: false,
      statusCode: status,
      message: exception.message,
      timestamp: new Date().toISOString(),
    });
  }
}

실행 순서 종합 정리

순서 컴포넌트 스코프 실행 순서 예외 시
1 Middleware Global → Module Express 에러 핸들러
2 Guard Global → Controller → Method Exception Filter
3 Interceptor (before) Global → Controller → Method Exception Filter
4 Pipe Global → Parameter Exception Filter
5 Handler Exception Filter
6 Interceptor (after) Method → Controller → Global Exception Filter
7 Exception Filter Method → Controller → Global

흔한 실수와 해결법

1. Middleware에서 DI가 안 되는 이유

// ❌ 함수형 미들웨어는 DI 불가
export function logger(req, res, next) {
  // this.service 접근 불가
  next();
}

// ✅ 클래스 미들웨어는 DI 가능
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  constructor(private readonly configService: ConfigService) {}
  use(req, res, next) {
    // this.configService 사용 가능
    next();
  }
}

2. Guard vs Middleware: 어디서 인증할까?

Guard를 사용하라. Guard는 ExecutionContext에 접근할 수 있어 Reflector로 메타데이터(@Public(), @Roles())를 읽을 수 있다. Middleware는 어떤 핸들러가 실행될지 모른다.

3. Interceptor에서 예외 발생 시 흐름

// Interceptor before에서 예외 → Handler 실행 안 됨
// Interceptor after(pipe)에서 예외 → Exception Filter로 전달
return next.handle().pipe(
  catchError(err => {
    // Handler에서 발생한 예외를 여기서 잡을 수 있음
    // 잡지 않으면 Exception Filter로 전달
    return throwError(() => new InternalServerErrorException());
  }),
);

정리

NestJS Request Lifecycle은 Middleware → Guard → Interceptor(before) → Pipe → Handler → Interceptor(after) → Exception Filter 순서로 동작한다. 각 컴포넌트는 고유한 역할과 실행 스코프가 있으며, 예외 발생 시 즉시 Exception Filter로 분기한다. 이 흐름을 정확히 이해하면, 인증·검증·변환·로깅·에러 처리를 올바른 위치에 배치하여 유지보수성 높은 아키텍처를 구축할 수 있다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux