NestJS 라이프사이클 (2/3): 가드, 파이프, 인터셉터! 요청이 Controller를 통과하는 7단계

이전 포스팅에서 onModuleInit()으로 초기 설정이 끝났으며
Nest.js에서 모듈 초기화가 끝났으면 다음 단계로 넘어가게 됩니다!

(모듈 초기화 과정)
https://gapal.tistory.com/71

 

Nest.js 라이프사이클 완전 정복 (1/3)- 모듈 초기화 단계(Initialization Phase)

Nest.JS를 사용하다보면 서비스나 모듈이 언제 생성되고 언제 실행되는지 모를때가 있습니다. 그런 경험이 있다면 Nest.js의 라이프 사이클을 잘 모르기 때문일거라 생각합니다.그렇다면 과연 Nest.j

gapal.tistory.com

 

※ 실행단계 (Running Phase)

NestJS에서 실행 단계(Running Phase)란 app.listen()이 호출된 후 서버가 정상적으로 클라이언트 요청을 받고 응답하는 기간 전체를 의미합니다. 이 단계의 핵심은 안정적이고 일관적인 요청 처리이며, 이를 위해 NestJS는 다음과 같은 기능들을 일련의 순서에 따라 체계적으로 작동시킵니다.

요청이 들어와 응답이 나가기까지 NestJS 내부를 통과하는 순서를 이해하는 것이 실행 단계 마스터의 지름길입니다.

3.요청 라이프사이클: 7단계의 여정

들어온 요청은 컨트롤러(핸들러)에 도달하기 전후로 여러 단계를 거치며 검증, 변환, 조작됩니다.

1.  HTTP 미들웨어 (Middleware)

  • 작동 시점: 가장 먼저. Express나 Fastify 기반의 전통적인 HTTP 미들웨어 계층입니다.
  • 역할: CORS 설정, 세션 관리, 기본적인 로깅 및 요청 바디 파싱(Body Parsing)과 같이 서버 전역의 공통적인 HTTP 작업을 처리합니다.

2.  가드 (Guards)

  • 작동 시점: 라우트 핸들러(Controller 메서드) 실행 직전.
  • 역할: 인가(Authorization) 및 접근 제어를 담당합니다. 요청을 처리할 권한이 있는지 확인하며, 권한이 없는 요청은 핸들러에 도달하기 전에 즉시 차단합니다.

3.  인터셉터 (Pre-Controller Interceptors)

  • 작동 시점: 가드 통과 후, 핸들러 실행 직전.
  • 역할: 핸들러가 실행되기 전에 요청 객체를 조작하거나 로깅(Logging), 시간 측정 등 사전 작업을 수행합니다.

4.  파이프 (Pipes)

  • 작동 시점: 라우트 핸들러 호출 직전.
  • 역할: 핸들러의 인자(Arguments)에 대한 유효성 검사(Validation)를 수행하고, 데이터를 원하는 형식으로 변환(Transformation)합니다. 예를 들어, 문자열 ID를 숫자로 변환하거나, DTO의 필드가 비어있지 않은지 검사합니다.

5. 컨트롤러/핸들러 (Controller/Handler)

  • 작동 시점: 파이프 통과 후, 비즈니스 로직 실행 시작.
  • 역할: 요청을 받아 적절한 서비스(Service) 레이어로 전달하고, 서비스에서 처리된 결과를 받아옵니다.

6.  서비스 (Service/Provider)

  • 작동 시점: 핸들러의 호출에 의해 실행.
  • 역할: 실제 비즈니스 로직을 담고 있으며, 데이터베이스 접근, 복잡한 계산 등을 수행하는 NestJS의 핵심 레이어입니다.

7.  인터셉터 (Post-Controller Interceptors) & 필터 (Exception Filters)

  • 인터셉터 작동 시점: 서비스의 처리 결과가 응답되기 직전.
    • 역할: 서비스 결과를 가로채서 최종적으로 응답을 포매팅(Formatting)하거나, 캐싱 처리 등을 수행합니다.
  • 예외 필터 작동 시점: 위 과정 중 어디서든 예외(Exception)가 발생했을 때.
    • 역할: 발생한 예외를 잡아 클라이언트에게 일관되고 구조화된 오류 응답으로 변환하여 전달합니다
  • 미들웨어/가드 적용: app.useGlobal... 명령어는 프레임워크가 애플리케이션 인스턴스에 기능을 '등록'하는 실행 명령입니다. 이는 특정 시점에 자동으로 호출되는 훅이 아니라, 개발자가 명시적으로 호출하는 설정 코드입니다.
  • 앱 리스닝 시작: app.listen() 함수는 Node.js 서버를 실제로 구동하는 최종 명령(Final Command)입니다. 이 또한 자동 호출되는 훅이 아니라, 서버 가동을 위한 필수적인 명령입니다.

4.코드 예제 실행

data.service
import {
  Injectable,
  OnApplicationBootstrap,
  OnModuleInit,
} from "@nestjs/common";

@Injectable()
export class DataService implements OnModuleInit, OnApplicationBootstrap {
  onModuleInit() {
    console.log("[DataService] onModuleInit 호출됨");
  }

  onApplicationBootstrap() {
    console.log("[DataService] onApplicationBootstrap 호출됨");
  }

  getData(): string[] {
    return ["item1", "item2", "item3"];
  }
}
app.module.ts
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { DataService } from "./data.service";
import { LoggerMiddleware } from "./logger.middleware";

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, DataService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    console.log("[AppModule] configure 호출됨 - 미들웨어 설정 중...");
    consumer.apply(LoggerMiddleware).forRoutes("*"); // 모든 경로에 적용
  }
}
app.service
import {
  Injectable,
  OnApplicationBootstrap,
  OnModuleInit,
} from "@nestjs/common";

@Injectable()
export class AppService implements OnModuleInit, OnApplicationBootstrap {
  onModuleInit() {
    console.log("[AppService] onModuleInit");
  }

  onApplicationBootstrap() {
    console.log("[AppService] onApplicationBootstrap");
  }

  getHello(): string {
    console.log("--- [6. 서비스] 비즈니스 로직 수행 ---");
    return "Hello World!";
  }
}
auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    console.log("--- [2. 가드] 권한 체크 (NestJS 레벨 진입) ---");
    return true;
  }
}
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("--- [1. 미들웨어] 요청 도착 (Express 레벨) ---");
    next();
  }
}
http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    console.log('--- [예외 필터] 에러 발생! 응답 가로채기 ---');
   
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        message: '예외 필터가 처리함',
        timestamp: new Date().toISOString(),
      });
  }
}


logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('--- [3. 인터셉터 (전)] 컨트롤러 실행 전 ---');
   
    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`--- [7. 인터셉터 (후)] 응답 나가기 전 (소요시간: ${Date.now() - now}ms) ---`)),
      );
  }
}


validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class MyValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    console.log('--- [4. 파이프] 데이터 검증 및 변환 ---');
    return value;
  }
}
main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { AuthGuard } from "./auth.guard";
import { LoggingInterceptor } from "./logging.interceptor";
import { MyValidationPipe } from "./validation.pipe";
import { HttpExceptionFilter } from "./http-exception.filter";

async function bootstrap() {
  console.log("--- [Bootstrap] 앱 생성 시작 ---");
  const app = await NestFactory.create(AppModule);

  // 전역 설정 등록
  app.useGlobalGuards(new AuthGuard());
  app.useGlobalInterceptors(new LoggingInterceptor());
  app.useGlobalPipes(new MyValidationPipe());
  app.useGlobalFilters(new HttpExceptionFilter());

  console.log(
    "--- [Bootstrap] 전역 설정(가드, 인터셉터, 파이프, 필터) 등록 완료 ---"
  );

  await app.listen(3000);
  console.log("--- [Bootstrap] 서버 실행 중 (Port: 3000) ---");
}

bootstrap();

위와 같이 코드를 구성했을 때 사이클 별로 로그를 찍었을 때 결과물을 보겠습니다.

5.결과 출력

main.ts에서 가드,인터셉터,파이프 ,필터와 같은 전역 설정이 먼저 된후에 app이 listen()으로 정상적으로 실행되며 nest.js의 실행사이클이 모두 마무리 되는 것을 볼 수 있습니다.

근데 여기서 실행단계인데 왜 초기화단계인 moduleInit()보다 미들웨어 등록이 앞서는지 궁금할수 있습니다.

💡 왜 Global 등록이 먼저 실행될까요?

onModuleInit 등의 훅은 의존성 주입(DI)이 필요한 컴포넌트(서비스, 프로바이더)에 속해 있습니다. 즉, DI 컨테이너가 해당 모듈과 컴포넌트를 모두 인스턴스화해야 비로소 훅이 호출될 수 있습니다.

반면에 전역 가드/미들웨어 등록은 애플리케이션이 요청을 처리할 준비(라우팅 환경)를 시작하는 초기 설정이며, 반드시 다른 서비스의 인스턴스가 생성될 필요 없이 서버 인스턴스에 곧바로 적용됩니다.

따라서 코드를 통해 확인하신 것처럼, 전역 환경 설정모듈 내부의 초기화 훅보다 먼저 실행되는 것이 NestJS의 설계 원칙입니다.

◆ 마무리

NestJS 실행 단계의 핵심은 요청 라이프사이클을 이해하고, 각 기능(가드, 파이프, 인터셉터 등)이 언제, 왜 실행되는지 아는 것입니다. 이 구조적 이해를 바탕으로 여러분은 안정적이고 유지보수가 용이한 엔터프라이즈급 백엔드 애플리케이션을 구축할 수 있습니다.

다음 포스팅에서는 NestJS의 3단계, 앱 종료(Termination) 과정과 관련된 라이프사이클 훅에 대해 다루겠습니다.

(다음 단계보기)

https://gapal.tistory.com/86

 

NestJS 라이프사이클 (3/3): 우아한 종료와 리소스 정리 (Graceful Shutdown & Resource Cleanup)

https://gapal.tistory.com/84 NestJS 라이프사이클 (2/3): 가드, 파이프, 인터셉터! 요청이 Controller를 통과하는 7단계이전 포스팅에서 onModuleInit()으로 초기 설정이 끝났으며 Nest.js에서 모듈 초기화가 끝났으

gapal.tistory.com

 

반응형