TIL - 20260221

juni·2026년 2월 21일

TIL

목록 보기
274/316

0221 NestJS 기초 (4/N): 요청 생명주기 (Guard, Interceptor, Filter)


✅ 1. NestJS 요청 생명주기 (Request Lifecycle) 개요

  • 클라이언트가 NestJS 서버로 HTTP 요청을 보내면, 이 요청은 컨트롤러의 라우트 핸들러에 도달하기 전과 후에 여러 단계를 거치게 됩니다.

  • NestJS는 이 과정을 세밀하게 제어할 수 있도록 5가지 주요 구성 요소를 제공합니다.

  • 실행 순서:

    1. Middleware (미들웨어)
    2. Guard (가드)
    3. Interceptor (인터셉터 - Controller 실행 전)
    4. Pipe (파이프) (어제 학습한 유효성 검증)
    5. Controller (컨트롤러 / 비즈니스 로직)
    6. Interceptor (인터셉터 - Controller 실행 후)
    7. Exception Filter (예외 필터 - 에러 발생 시)

✅ 2. 미들웨어 (Middleware)

  • 개념: Express.js의 미들웨어와 동일한 개념입니다. 라우트 핸들러가 실행되기 가장 먼저 요청(Request)과 응답(Response) 객체에 접근하여 조작할 수 있습니다.
  • 주요 용도:
    • 전역적인 로깅 (어떤 IP에서 어떤 URL로 요청이 왔는지 기록)
    • 요청 헤더 검사 및 조작
  • 특징: NestJS의 실행 컨텍스트(Execution Context)에 접근할 수 없어, 다음에 어떤 컨트롤러가 실행될지 알지 못합니다.

✅ 3. 가드 (Guard)

  • 개념: 들어온 요청이 라우트 핸들러에 의해 처리될지 말지를 결정하는 역할을 합니다. (주로 true 또는 false 반환)
  • 주요 용도: 인증(Authentication)인가(Authorization).
    • "이 사용자가 로그인한 사용자인가?" (인증)
    • "이 사용자가 관리자 권한을 가지고 있는가?" (인가)
  • 왜 미들웨어 대신 가드를 쓰는가?: 미들웨어는 다음에 실행될 핸들러가 무엇인지 알 수 없지만, 가드는 ExecutionContext를 통해 다음에 실행될 컨트롤러와 메서드의 정보에 접근할 수 있어 훨씬 정교한 권한 제어가 가능합니다.

➕ 가드 구현 예시 (CanActivate 인터페이스)

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    // 요청 헤더에서 토큰을 확인하는 로직 (예시)
    const hasToken = request.headers.authorization !== undefined;
    
    // true를 반환하면 요청 통과, false면 403 Forbidden 에러 발생
    return hasToken; 
  }
}

// 컨트롤러에 적용
@UseGuards(AuthGuard)
@Controller('users')
export class UsersController { ... }

✅ 4. 인터셉터 (Interceptor)

  • 개념: 메서드 실행 전/후에 추가 로직을 바인딩할 수 있는 기능입니다. AOP(관점 지향 프로그래밍)에서 영감을 받았습니다. Spring의 Interceptor나 AOP와 유사합니다.
  • 주요 용도:
    • 응답 데이터 변환 (Response Mapping): 컨트롤러가 반환한 데이터를 클라이언트에게 보내기 전에 특정 형식(e.g., { data: ... })으로 일괄 포장합니다.
    • 실행 시간 측정: 요청이 들어온 시간과 응답이 나가는 시간을 계산하여 로깅합니다.
    • 캐싱: 특정 요청에 대해 컨트롤러를 실행하지 않고 캐시된 응답을 바로 반환합니다.
  • 특징: RxJSObservable을 사용하여 비동기 스트림을 강력하게 제어합니다.

➕ 응답 변환 인터셉터 예시

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    // next.handle()은 컨트롤러의 라우트 핸들러를 실행함
    return next.handle().pipe(
      // 컨트롤러가 반환한 데이터(data)를 가로채서 새로운 객체 형태로 변환
      map(data => ({
        success: true,
        data: data,
      })),
    );
  }
}

✅ 5. 예외 필터 (Exception Filter)

  • 개념: 애플리케이션 전역에서 처리되지 않은 예외(Unhandled Exception)가 발생했을 때, 이를 가로채서 클라이언트에게 일관된 형태의 에러 응답을 보내는 역할을 합니다. Spring의 @RestControllerAdvice와 동일한 역할입니다.
  • 주요 용도:
    • 에러 메시지 포맷 통일 (e.g., { statusCode: 400, message: "...", timestamp: "..." })
    • 에러 로깅 중앙화

➕ 예외 필터 구현 예시

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException) // HttpException 타입의 에러만 가로챔
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    // 커스텀 에러 응답 포맷 정의
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.message,
    });
  }
}

📌 요약

  • NestJS는 요청이 들어오고 나가는 과정을 세밀하게 제어할 수 있는 생명주기(Lifecycle)를 제공합니다.
  • Middleware는 라우트 도달 전 전역적인 처리를 담당합니다.
  • GuardExecutionContext를 활용하여 요청의 인증 및 인가(권한 제어)를 수행합니다.
  • Interceptor는 라우트 핸들러 실행 전후에 개입하여 응답 데이터를 변환하거나 로깅 등의 부가 기능을 수행합니다.
  • Exception Filter는 앱 전역에서 발생하는 에러를 가로채어 일관된 에러 응답 포맷을 클라이언트에게 제공합니다.

0개의 댓글