본문으로 건너뛰기

안동민 개발노트

본문 시작

미들웨어, 인터셉터, 파이프, 가드

요청 처리 파이프라인의 실행 순서를 따라 미들웨어·가드·파이프·인터셉터의 책임과 적용 지점을 구분합니다.

NestJS 애플리케이션은 클라이언트 요청을 받아 응답을 보내기까지 여러 단계를 거칩니다.

이 과정에서 요청 데이터 변환, 유효성 검사, 권한 확인 같은 부가 작업이 수행됩니다.

NestJS는 이를 효율적으로 처리하도록 미들웨어(Middleware), 인터셉터(Interceptor), 파이프(Pipe), 가드(Guard)를 제공합니다.

이들은 공장 컨베이어 벨트처럼 순서대로 동작하며 요청 처리 파이프라인을 구성합니다.

이 절에서는 각 요소가 무엇이며, 어떤 역할을 수행하고, 언제 사용해야 하는지 자세히 알아보겠습니다.

NestJS 요청 처리 단계

요청은 컨트롤러로 바로 들어가지 않는다. 공통 처리, 접근 판단, 입력 검증, 응답 변환이 정해진 위치에서 차례로 개입한다.

  1. Middleware

    요청 전체에 공통으로 필요한 로깅, CORS, 세션을 먼저 처리한다.

  2. Guard

    사용자가 이 라우트에 접근 가능한지 판단한다.

  3. Interceptor 전

    핸들러 앞뒤를 감싸는 작업을 시작한다.

  4. Pipe

    컨트롤러 매개변수 직전에 변환과 검증을 수행한다.

  5. Controller

    라우트 핸들러가 서비스를 호출한다.

  6. Interceptor 후

    반환값을 응답 규격으로 감싸거나 변환한다.

  7. 모든 요청 공통

    미들웨어

  8. 접근 허용 판단

  9. 입력 DTO 검증

    파이프

  10. 응답 래핑과 캐싱

    인터셉터


미들웨어: Express.js와의 연결 고리

NestJS는 내부적으로 HTTP 서버 프레임워크인 Express.js를 기본값으로 사용합니다.

미들웨어는 Express.js의 미들웨어 개념을 NestJS에서도 사용할 수 있도록 제공하는 기능이며, HTTP 요청이 라우트 핸들러에 도달하기 전 또는 후에 실행되는 함수입니다.

주요 특징 및 사용 사례
  • 가장 먼저 실행: 요청 파이프라인에서 가장 먼저 실행되는 단계 중 하나입니다.
  • 요청 객체 접근: req, res, next 객체에 직접 접근하여 요청 헤더, 바디, 응답 등을 조작할 수 있습니다.
  • 인증 및 로깅: 모든 요청에 대해 공통적으로 적용되는 로깅, 인증/인가(세션 기반), CORS 처리 등에 적합합니다.
작성 및 적용 방법

미들웨어는 클래스 기반 또는 함수 기반으로 작성할 수 있습니다.

클래스 기반 미들웨어를 사용하려면 NestMiddleware 인터페이스를 구현해야 합니다.

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... ${req.method} ${req.originalUrl}`);
    next(); // 다음 미들웨어 또는 라우트 핸들러로 제어를 넘깁니다.
  }
}

미들웨어는 모듈에서 .configure() 메서드를 사용하여 적용합니다.

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

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware) // LoggerMiddleware를 적용합니다.
      .forRoutes('*'); // 모든 경로에 적용합니다. 특정 경로만 지정할 수도 있습니다.
      // .forRoutes({ path: 'cats', method: RequestMethod.GET }); // 특정 경로와 메서드에 적용
  }
}

가드: 권한 및 인증 관리

가드는 특정 라우트 핸들러가 실행되기 전에 요청을 가로채서 특정 조건(주로 권한이나 인증 정보)을 만족하는지 확인하는 역할을 합니다.

마치 클럽 입구에서 신분증을 검사하는 경비원과 같습니다.

조건이 만족되면 요청은 다음 단계로 진행되지만, 그렇지 않으면 즉시 HTTP 응답(예: 401 Unauthorized, 403 Forbidden)을 반환합니다.

주요 특징 및 사용 사례
  • 실행 시점: 미들웨어보다 늦게, 하지만 인터셉터와 파이프보다 먼저 실행됩니다.
  • 권한 부여: 사용자 역할 기반 접근 제어(RBAC), JWT 토큰 유효성 검증 등 인증(Authentication)인가(Authorization)로직에 최적화되어 있습니다.
  • CanActivate 인터페이스: CanActivate 인터페이스를 구현하고 canActivate() 메서드에서 boolean 값을 반환합니다.
작성 및 적용 방법
src/auth/guards/auth.guard.ts
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();
    // 실제 로직: JWT 토큰 유효성 검사, 사용자 역할 확인 등
    // 예시: request.user 객체에 사용자가 있다면 true 반환
    return !!request.user; // 가짜 인증 로직
  }
}

가드는 컨트롤러나 메서드 레벨에서 @UseGuards() 데코레이터를 사용하여 적용합니다.

src/users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/guards/auth.guard'; // 가드 임포트

@Controller('users')
export class UsersController {
  @UseGuards(AuthGuard) // 이 컨트롤러의 모든 라우트에 AuthGuard 적용
  @Get()
  findAll(): string {
    return 'This is a protected resource (all users).';
  }

  @UseGuards(AuthGuard) // 특정 라우트에만 AuthGuard 적용
  @Get(':id')
  findOne(): string {
    return 'This is a protected resource (single user).';
  }
}

파이프: 데이터 변환 및 유효성 검사

파이프는 들어오는 요청 데이터를 변환(Transformation)하거나 유효성 검사(Validation)를 수행하는 역할을 합니다.

컨트롤러의 라우트 핸들러로 데이터가 전달되기 전에 데이터를 원하는 형식으로 가공하거나, 정의된 규칙에 따라 데이터가 올바른지 확인하는 데 사용됩니다.

주요 특징 및 사용 사례
  • 실행 시점: 가드 다음에 실행되며, 요청 핸들러 메서드에 전달되기 직전에 실행됩니다.
  • 데이터 처리: stringnumber로 변환하거나, DTO(Data Transfer Object) 유효성 검사를 수행합니다.
  • PipeTransform 인터페이스: PipeTransform 인터페이스를 구현하고 transform() 메서드를 오버라이드합니다.
작성 및 적용 방법

NestJS는 ValidationPipe, ParseIntPipe 등 유용한 빌트인 파이프를 제공합니다.

커스텀 파이프도 쉽게 만들 수 있습니다.

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

@Injectable()
export class PositiveIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10); // 문자열을 숫자로 변환
    if (isNaN(val) || val < 0) { // 변환 실패 또는 음수인 경우 예외 발생
      throw new BadRequestException('Validation failed: ID must be a positive integer.');
    }
    return val;
  }
}

파이프는 @UsePipes() 데코레이터를 사용하거나, 메서드 매개변수 레벨에 직접 적용할 수 있습니다.

src/items/items.controller.ts
import { Controller, Get, Param, UsePipes } from '@nestjs/common';
import { PositiveIntPipe } from '../common/pipes/positive-int.pipe';

@Controller('items')
export class ItemsController {
  // @UsePipes(PositiveIntPipe) // 컨트롤러 레벨에 적용
  @Get(':id')
  // 매개변수 레벨에 파이프 적용. id가 숫자이고 양수인지 검증합니다.
  findOne(@Param('id', PositiveIntPipe) id: number): string {
    return `Item with ID: ${id}`;
  }
}

인터셉터: 응답 변환 및 부가 로직

인터셉터는 AOP(Aspect-Oriented Programming) 개념을 구현한 기능으로, 요청 전/후에 추가적인 로직을 삽입하거나, 응답 데이터를 변환하거나, 예외를 처리하는 등의 역할을 수행합니다.

함수 호출 전후에 특정 작업을 수행하는 로직을 삽입하는 것과 유사합니다.

주요 특징 및 사용 사례
  • 실행 시점: 파이프 다음에 실행되며, 라우트 핸들러가 실행된 후 응답이 클라이언트에 전달되기 직전에 실행됩니다.
  • 반응형 프로그래밍: RxJS의 Observable을 사용하여 비동기 스트림을 조작하고 변환할 수 있습니다.
  • 로깅, 캐싱, 응답 변환, 예외 처리, 트랜잭션 관리 등에 활용됩니다.
작성 및 적용 방법

인터셉터는 NestInterceptor 인터페이스를 구현하고 intercept() 메서드를 오버라이드합니다.

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

export interface Response<T> {
  statusCode: number;
  message: string;
  data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => ({ // 컨트롤러가 반환한 데이터를 감싸서 응답합니다.
        statusCode: context.switchToHttp().getResponse().statusCode,
        message: 'Success',
        data: data,
      })),
    );
  }
}

인터셉터는 @UseInterceptors() 데코레이터를 사용하여 적용합니다.

src/app.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';

@Controller()
@UseInterceptors(TransformInterceptor) // 이 컨트롤러의 모든 응답을 변환
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello(); // "Hello World!" 문자열 반환
  }
}
// 결과 응답: { statusCode: 200, message: "Success", data: "Hello World!" }

요청 처리 파이프라인 요약 및 실행 순서

미들웨어, 인터셉터, 파이프, 가드를 비교할 때 실행 순서, 변환 책임, 차단 조건, 오류 매핑 기준을 확인합니다.

Nest 요청은 실행 전후 정책과 예외 경로를 함께 지난다

Middleware·Guard·Interceptor·Pipe는 위치와 변경 대상, 실패 시점이 달라 요청의 직선 경로를 감싼다.

  1. Middleware

    라우팅 전 req·res에 공통 문맥을 붙인다.

  2. Guard

    사용자·역할·권한으로 실행 여부를 판단한다.

  3. Interceptor Before

    시간 측정·캐시·요청 전 문맥을 연다.

  4. Pipe

    params·query·body를 변환하고 DTO를 검증한다.

  5. Controller / Service

    요청 계약과 업무 규칙을 실행한다.

  6. Interceptor After

    반환값을 표준 응답으로 매핑한다.

구성요소주로 만지는 것대표 실패
Guard사용자·역할·권한401 / 403
PipeDTO·params·query·body400
Interceptor전후 문맥·반환값응답 변환·예외 매핑

NestJS의 요청 처리 파이프라인은 다음과 같은 순서로 실행됩니다.

미들웨어 (Middleware): 가장 먼저 실행되어 공통적인 전처리(로깅, 인증 등)를 수행합니다.

가드 (Guards): 미들웨어 다음으로 실행되어 라우트 핸들러에 대한 접근 권한을 확인합니다.

인터셉터 (Interceptors) - 전(Pre-handler): 라우트 핸들러 실행 전에 추가 로직(예: 요청 로깅, 캐싱)을 수행합니다.

파이프 (Pipes): 라우트 핸들러의 매개변수가 유효한지 검사하거나 변환합니다.

라우트 핸들러 (Controller Method): 컨트롤러의 실제 비즈니스 로직(서비스 호출 등)이 실행됩니다.

인터셉터 (Interceptors) - 후(Post-handler): 라우트 핸들러의 응답이 반환된 후 최종 응답 변환, 예외 처리 등을 수행합니다.

NestJS 관심사 분리

같은 요청 안에서도 공통 처리, 보안, 검증, 비즈니스, 응답 포맷은 다른 책임이다. 위치를 분리해야 코드 변경 범위가 작아진다.

  1. 공통 요청 문맥

    깅, CORS, 세션처럼 라우트와 무관한 처리는 Middleware에 둔다.

  2. 접근 가능성

    토큰, 역할, 소유자 확인처럼 실행 여부를 결정하면 Guard에 둔다.

  3. 입력 품질

    문자열을 숫자로 바꾸거나 DTO를 검증하면 Pipe에 둔다.

  4. 전후 처리

    캐싱, 시간 측정, 응답 래핑처럼 핸들러 앞뒤가 필요하면 Interceptor에 둔다.

  5. 업무 규칙

    주문 생성, 사용자 저장 같은 도메인 판단은 Service에 둔다.

냄새원인옮길 곳
컨트롤러가 토큰을 직접 파싱접근 판단과 요청 처리 혼합Guard
서비스가 DTO 타입 변환 담당입력 형식과 업무 규칙 혼합Pipe
모든 핸들러가 같은 응답 래핑 반복후처리 중복Interceptor

요청은 모든 단계를 반드시 끝까지 지나가는 것이 아니라, 가드와 파이프에서 멈추거나 인터셉터를 되돌아 나오며 응답이 변환될 수 있습니다.

요청은 중간에서 끝나거나 응답으로 되돌아온다

정상 흐름만 보면 파이프라인을 오해하기 쉽다. 가드와 파이프는 요청을 멈출 수 있고, 인터셉터는 응답이 돌아오는 길에도 개입한다.

  1. 1
    Request

    들어오는 방향

  2. 2
    Gate

    드와 파이프가 실패를 조기에 만든다.

  3. 3
    Handler

    통과한 요청만 업무 로직으로 간다.

  4. 4
    Return

    응답은 인터셉터 후처리를 거쳐 나간다.

이러한 파이프라인의 각 요소는 서로 다른 관심사를 분리하고, 애플리케이션의 유지보수성과 확장성을 크게 향상시킵니다.

필요한 지점에 맞는 요소를 조합하면 NestJS 애플리케이션의 유지보수성과 확장성을 높일 수 있습니다.

다음 절에서는 NestJS에서 예외를 효과적으로 처리하는 방법에 대해 알아보겠습니다.