미들웨어, 인터셉터, 파이프, 가드
요청 처리 파이프라인의 실행 순서를 따라 미들웨어·가드·파이프·인터셉터의 책임과 적용 지점을 구분합니다.
NestJS 애플리케이션은 요청을 받아 응답을 보내기까지 여러 단계를 거치며, 그 사이에 요청 데이터의 변환, 유효성 검사, 권한 확인 같은 부가 작업이 일어납니다. NestJS는 이런 작업을 위해 미들웨어(middleware), 가드(guard), 파이프(pipe), 인터셉터(interceptor)를 제공하고, 이들이 정해진 순서로 요청 처리 흐름을 이룹니다.
이 글에서는 각 구성 요소가 무엇을 맡고, 언제 실행되며, 어디에 적용하는지를 차례로 살펴봅니다. 요청은 모든 단계를 반드시 끝까지 지나가는 것이 아니라, 가드나 파이프에서 멈추거나 인터셉터를 거슬러 나오며 응답이 바뀔 수 있다는 점을 함께 확인합니다.
요청은 순서대로 진입하고 인터셉터를 역순으로 빠져나온다
정상 진입·역순 interceptor 반환·조기 응답·예외 filter로 제어와 요청 값이 변한다.
Nest 요청 경로 · 진행과 예외
정상 요청은 핸들러로 진행하고, 거부·오류는 예외 처리로 빠집니다
미들웨어 → 가드 → 인터셉터 전처리 → 파이프 → 핸들러 순서입니다. 본문의 AuthGuard는 user 존재만 확인합니다. PositiveIntPipe는 원시 문자열 전체가 ASCII 숫자인지 검사한 뒤 양의 안전한 정수로 변환합니다.
- 미들웨어
원시 HTTP 요청을 관찰하고 다음 단계로 넘깁니다.
- 가드
false면 ForbiddenException이 발생하고 예외 처리로 기본 403을 응답합니다. 핸들러는 실행되지 않으며, 401은 별도 UnauthorizedException을 던져야 합니다.
- 인터셉터 전처리
핸들러 호출을 감싸고 추적·캐시·응답 변환을 시작합니다.
- 파이프와 핸들러
PositiveIntPipe가 문자열 '12'를 숫자 12로 바꾸면 핸들러가 실행됩니다. '12abc', '0', '-1', 'abc'는 예외 처리로 빠지고 핸들러는 실행되지 않습니다.
- 되감기 또는 필터
정상 결과는 인터셉터를 역순으로 나가고, 잡히지 않은 예외는 가장 가까운 필터가 응답으로 매핑합니다.
가드의 false도 예외 filter에서 처리됩니다. 그림의 필터 범위는 route → controller → global 우선순위이며 연쇄 실행이 아닙니다. 본문 Pipe에서는 문자열 '12' → 숫자 12 → 핸들러로 진행하고, '12abc', '0', '-1', 'abc'는 BadRequestException으로 핸들러 전에 멈춥니다. 기본 예외 응답은 400입니다. 앞쪽 0은 허용해 '01'은 1이 되며, 원시 문자열 입력과 양의 안전한 정수 범위를 전제로 합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 미들웨어, 가드, 인터셉터 전처리, 파이프, 핸들러, 인터셉터 후처리와 응답의 정상 경로 및 조기 응답과 예외 필터 경로를 보여준다.
- 정상 경로는 미들웨어, 가드, 실행 전 인터셉터, 파이프, 핸들러, 반환 인터셉터와 응답 순서다. 가드 false는 기본 403, 파이프의 BadRequestException은 기본 400을 예외 처리 경계로 전달한다. 핸들러의 다른 미처리 예외와 인터셉터 오류 처리 범위는 아래 설명을 따른다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 29 | 정상 경로는 미들웨어, 가드, 실행 전 인터셉터, 파이프, 핸들러, 반환 인터셉터와 응답 순서다. 가드 false는 기본 403, 파이프의 BadRequestException은 기본 400을 예외 처리 경계로 전달한다. 핸들러의 다른 미처리 예외와 인터셉터 오류 처리 범위는 아래 설명을 따른다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 정상 진입 middleware·guard 뒤 interceptor 전처리·pipe를 실행합니다. | handler: 아직 전 · 조기 종료: 별도 분기 | 준비된 요청만 업무에 들어갑니다. |
| handler 결과 handler의 정상 반환과 interceptor가 처리하지 않은 예외의 다음 경계를 구분합니다. | 정상: 반환 · 미처리 예외: 가장 가까운 일치 filter 경계 | 업무 결과가 출력 경로를 정합니다. |
| 역순 반환·오류 정상 결과는 interceptor를 역순으로 나오며 처리되지 않은 예외만 일치하는 filter로 응답합니다. | 정상: 역순 후처리 · 미처리 예외: 가장 가까운 일치 filter | interceptor가 오류를 처리할 수 있는 범위와 미처리 예외의 filter 응답 범위를 구분합니다. |
요청은 순서대로 진입하고 인터셉터를 역순으로 빠져나온다
정상 진입·역순 interceptor 반환·조기 응답·예외 filter로 제어와 요청 값이 변한다.
Nest 요청 경로 · 진행과 예외
정상 요청은 핸들러로 진행하고, 거부·오류는 예외 처리로 빠집니다
미들웨어 → 가드 → 인터셉터 전처리 → 파이프 → 핸들러 순서입니다. 본문의 AuthGuard는 user 존재만 확인합니다. PositiveIntPipe는 원시 문자열 전체가 ASCII 숫자인지 검사한 뒤 양의 안전한 정수로 변환합니다.
- 미들웨어
원시 HTTP 요청을 관찰하고 다음 단계로 넘깁니다.
- 가드
false면 ForbiddenException이 발생하고 예외 처리로 기본 403을 응답합니다. 핸들러는 실행되지 않으며, 401은 별도 UnauthorizedException을 던져야 합니다.
- 인터셉터 전처리
핸들러 호출을 감싸고 추적·캐시·응답 변환을 시작합니다.
- 파이프와 핸들러
PositiveIntPipe가 문자열 '12'를 숫자 12로 바꾸면 핸들러가 실행됩니다. '12abc', '0', '-1', 'abc'는 예외 처리로 빠지고 핸들러는 실행되지 않습니다.
- 되감기 또는 필터
정상 결과는 인터셉터를 역순으로 나가고, 잡히지 않은 예외는 가장 가까운 필터가 응답으로 매핑합니다.
가드의 false도 예외 filter에서 처리됩니다. 그림의 필터 범위는 route → controller → global 우선순위이며 연쇄 실행이 아닙니다. 본문 Pipe에서는 문자열 '12' → 숫자 12 → 핸들러로 진행하고, '12abc', '0', '-1', 'abc'는 BadRequestException으로 핸들러 전에 멈춥니다. 기본 예외 응답은 400입니다. 앞쪽 0은 허용해 '01'은 1이 되며, 원시 문자열 입력과 양의 안전한 정수 범위를 전제로 합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 미들웨어, 가드, 인터셉터 전처리, 파이프, 핸들러, 인터셉터 후처리와 응답의 정상 경로 및 조기 응답과 예외 필터 경로를 보여준다.
- 정상 경로는 미들웨어, 가드, 실행 전 인터셉터, 파이프, 핸들러, 반환 인터셉터와 응답 순서다. 가드 false는 기본 403, 파이프의 BadRequestException은 기본 400을 예외 처리 경계로 전달한다. 핸들러의 다른 미처리 예외와 인터셉터 오류 처리 범위는 아래 설명을 따른다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 29 | 정상 경로는 미들웨어, 가드, 실행 전 인터셉터, 파이프, 핸들러, 반환 인터셉터와 응답 순서다. 가드 false는 기본 403, 파이프의 BadRequestException은 기본 400을 예외 처리 경계로 전달한다. 핸들러의 다른 미처리 예외와 인터셉터 오류 처리 범위는 아래 설명을 따른다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 정상 진입 middleware·guard 뒤 interceptor 전처리·pipe를 실행합니다. | handler: 아직 전 · 조기 종료: 별도 분기 | 준비된 요청만 업무에 들어갑니다. |
| handler 결과 handler의 정상 반환과 interceptor가 처리하지 않은 예외의 다음 경계를 구분합니다. | 정상: 반환 · 미처리 예외: 가장 가까운 일치 filter 경계 | 업무 결과가 출력 경로를 정합니다. |
| 역순 반환·오류 정상 결과는 interceptor를 역순으로 나오며 처리되지 않은 예외만 일치하는 filter로 응답합니다. | 정상: 역순 후처리 · 미처리 예외: 가장 가까운 일치 filter | interceptor가 오류를 처리할 수 있는 범위와 미처리 예외의 filter 응답 범위를 구분합니다. |
그림의 정상 경로는 미들웨어, 가드, 실행 전 인터셉터, 파이프, 핸들러, 반환 인터셉터를 거쳐 응답으로 나갑니다. 가드가 false를 돌려주면 기본 403으로, 파이프가 BadRequestException을 던지면 기본 400으로 예외 처리에 넘어가며, 두 경우 모두 핸들러는 실행되지 않습니다. 잡히지 않은 예외는 라우트, 컨트롤러, 전역 순서로 가장 가까운 예외 필터가 응답으로 바꾸며, 필터가 연달아 실행되지는 않습니다. 아래 칸들은 이 글의 AuthGuard가 사용자 객체의 존재만 확인하고, PositiveIntPipe가 원시 문자열 전체가 숫자인지 검사한 뒤 양의 안전한 정수로 바꾼다는 점을 정리합니다.
미들웨어
NestJS는 기본 HTTP 서버로 Express를 씁니다. 미들웨어는 HTTP 요청을 받아 다음 단계로 넘기거나 직접 응답을 끝낼 수 있는 함수이며, Nest의 요청 수명 주기에서 가드와 라우트 핸들러보다 먼저 실행됩니다. req, res, next 객체에 직접 접근할 수 있으므로, 모든 요청에 공통으로 적용하는 로깅, 세션 기반 인증, CORS 처리 같은 일에 알맞습니다.
다만 next() 아래에 쓴 코드가 비동기 핸들러의 응답 완료를 기다리지는 않습니다. 실행 전후를 감싸는 처리가 필요하다면 인터셉터를 쓰고, 실제 응답 완료를 관찰하려면 응답 객체의 이벤트를 따로 씁니다.
미들웨어는 클래스나 함수로 작성할 수 있으며, 클래스로 만들 때는 NestMiddleware 인터페이스를 구현합니다.
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() 메서드에서 적용합니다. NestJS 11부터 Express 5의 경로 규칙을 따르므로, 와일드카드에는 *splat처럼 이름을 붙입니다. 'abcd/*splat'은 abcd/ 뒤에 한 글자 이상이 있는 경로와 맞고, 중괄호로 감싼 '{*splat}'은 빈 경로까지 포함하므로 루트를 포함한 모든 경로에 적용됩니다.
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('{*splat}'); // 루트를 포함한 모든 경로에 적용합니다.
// .forRoutes({ path: 'cats', method: RequestMethod.GET }); // 특정 경로와 메서드에만 적용
}
}apply()에는 적용할 미들웨어를, forRoutes()에는 적용할 경로를 넘깁니다. 이렇게 등록하면 루트를 포함한 모든 요청마다 터미널에 Request... GET /처럼 메서드와 원래 URL이 한 줄씩 찍힙니다. 다만 모듈, 컨트롤러, 서비스의 기본 개념의 main.ts처럼 api 전역 접두사를 설정했다면 미들웨어의 경로에도 접두사가 붙으므로, GET /api에서 Request... GET /api가 찍히고 접두사 밖의 GET /에서는 찍히지 않습니다. 주석 처리한 줄은 경로와 HTTP 메서드를 객체로 함께 지정하는 형태이며, 주석을 풀어 쓰려면 RequestMethod도 @nestjs/common에서 가져와야 합니다.
가드
가드는 라우트 핸들러가 실행되기 전에 요청을 가로채, 권한이나 인증 정보 같은 조건을 만족하는지 확인합니다. 클럽 입구에서 신분증을 확인하는 경비원과 비슷합니다. 미들웨어보다 늦게, 인터셉터와 파이프보다 먼저 실행되며, 역할 기반 접근 제어나 토큰 검증 같은 인증·인가 로직에 알맞습니다.
가드는 CanActivate 인터페이스를 구현하고 canActivate()에서 불리언 값을 돌려줍니다. 조건을 만족하면 요청이 다음 단계로 진행되고, false를 돌려주면 Nest가 ForbiddenException을 던져 기본 403 응답이 됩니다. 401이 필요한 인증 실패라면 UnauthorizedException처럼 의도한 예외를 직접 던져야 하며, 예외 필터를 설치했다면 최종 응답 형식은 그 필터를 따릅니다.
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; // 가짜 인증 로직
}
}context.switchToHttp().getRequest()는 실행 문맥에서 HTTP 요청 객체를 꺼내는 호출입니다. 가드는 HTTP뿐 아니라 WebSocket이나 마이크로서비스에도 쓰일 수 있으므로, 요청 객체를 바로 받지 않고 ExecutionContext를 거쳐 꺼냅니다. 반환 타입에 Promise와 Observable이 함께 적힌 것은 데이터베이스 조회처럼 비동기로 판단하는 가드도 허용하기 때문입니다. Observable은 RxJS 라이브러리가 제공하는, 시간에 따라 여러 값을 차례로 내보낼 수 있는 객체이며 TypeScript 교재의 RxJS와 반응형 프로그래밍에서 다룹니다. 가드는 컨트롤러나 메서드에 @UseGuards() 데코레이터로 적용합니다.
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/guards/auth.guard'; // 가드 임포트
@Controller('users')
export class UsersController {
@UseGuards(AuthGuard) // findAll 메서드에 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).';
}
}이 예제는 두 메서드에 각각 가드를 붙였으며, 클래스 선언 위에 @UseGuards(AuthGuard)를 붙이면 그 컨트롤러의 모든 핸들러에 적용됩니다.
위 가드는 !!request.user로 사용자 객체가 있는지만 검사하는 학습용 코드입니다. 빈 객체 {}도 참으로 평가되므로 통과합니다. 앞의 로깅 미들웨어는 request.user를 만들지 않으므로, 사용자를 준비하는 단계가 없으면 요청은 거부됩니다. 클라이언트가 보낸 body.user나 role: 'admin'을 그대로 복사해 넣는 것은 인증이 아닙니다. 실제 인증에서는 서버가 세션이나 토큰을 검증해 사용자를 확정한 뒤 접근 정책을 검사해야 하며, 토큰 문자열을 해석하는 것만으로 서명과 만료 검증이 끝나지도 않습니다.
가드는 파이프보다 먼저 실행되므로, 가드에서 경로의 ID를 읽더라도 그 값은 아직 아래 PositiveIntPipe가 만든 숫자가 아닙니다. 가드의 접근 판단과 파이프의 값 변환은 나눠서 생각합니다. 이 글의 Request·Response 예제는 Express 기반 HTTP 어댑터를 전제로 하며, Fastify로 바꾸면 요청·응답 타입과 API도 함께 바꿔야 합니다.
더 깊이: 미들웨어는 왜 다음 핸들러를 모를까
미들웨어는 Nest가 요청을 처리할 라우트 핸들러를 고르기 전에 실행됩니다. 그래서 가드 문서가 설명하듯 미들웨어는 next()를 호출한 뒤 어떤 핸들러가 실행될지 모르고, 가드는 ExecutionContext를 받아 다음에 실행될 핸들러와 컨트롤러를 정확히 압니다. 핸들러에 붙은 메타데이터를 읽어 '이 경로는 관리자만'처럼 판단하는 일을 미들웨어가 아니라 가드에 두는 이유가 여기에 있습니다.
같은 이유로 미들웨어에서 던진 예외는 전역 예외 필터만 잡습니다. app.useGlobalFilters()나 APP_FILTER로 등록한 필터는 동작하지만, 컨트롤러나 메서드에 붙인 필터는 아직 고른 핸들러가 없으므로 실행되지 않고, 미들웨어 클래스에 @UseFilters()를 붙이는 것도 지원하지 않습니다. 비동기 미들웨어는 use()를 async로 선언하거나 Promise를 돌려줘야 거부된 Promise가 Nest의 예외 처리 계층으로 넘어가며, 콜백 방식으로 쓴 미들웨어는 next(err)로 오류를 넘깁니다.
main.ts에서 app.use()로 등록한 미들웨어는 이 규칙 밖에 있습니다. 그런 미들웨어는 Nest의 미들웨어 모듈이 아니라 Express나 Fastify가 직접 처리하므로, 오류를 Nest의 예외 처리 계층에 맡기려면 모듈의 configure()에서 MiddlewareConsumer로 등록하는 편이 낫습니다.
파이프
파이프는 들어오는 요청 데이터를 변환(transformation)하거나 유효성을 검사(validation)합니다. 가드 다음, 핸들러에 인자가 전달되기 직전에 실행되며, 문자열을 숫자로 바꾸거나 DTO의 유효성을 검사하는 데 씁니다. 파이프는 PipeTransform 인터페이스를 구현하고 transform() 메서드를 정의하며, NestJS는 ValidationPipe와 ParseIntPipe 같은 내장 파이프도 제공합니다.
예를 들어 /items/12의 12는 경로에서 꺼낸 문자열입니다. 핸들러의 타입을 id: number로 적는 것만으로는 이 문자열이 숫자로 바뀌지 않습니다. 숫자 ID를 받으려면 어떤 문자열을 허용할지 정하고, 파이프에서 검사한 뒤 변환해야 합니다.
아래 PositiveIntPipe는 문자열 전체가 ASCII 숫자 0부터 9로만 이루어져 있고, 변환한 값이 양의 안전한 정수(safe integer)일 때만 통과시킵니다. 안전한 정수 범위에서는 자바스크립트의 number가 정수를 정확하게 표현하고 이웃한 정수와 구별할 수 있으며, 여기서는 1부터 Number.MAX_SAFE_INTEGER인 9007199254740991까지 허용합니다.
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class PositiveIntPipe implements PipeTransform<string, number> {
transform(value: string): number {
if (typeof value !== 'string' || !/^[0-9]+$/.test(value)) {
throw new BadRequestException('ID must be a positive safe integer.');
}
const id = Number(value);
if (!Number.isSafeInteger(id) || id < 1) {
throw new BadRequestException('ID must be a positive safe integer.');
}
return id;
}
}parseInt('12abc', 10)은 앞의 숫자만 읽어 12를 돌려주므로, 이 파이프는 Number()로 바꾸기 전에 문자열 전체를 검사해 그런 입력을 막습니다. 공백, 부호, 소수점, 지수 표기가 들어간 문자열은 거부합니다. 앞쪽의 0은 허용하므로 '01'은 1이 되지만, '0'이나 '000'은 양수가 아니라서 거부되고, '9007199254740992'처럼 안전한 정수 범위를 넘는 값도 거부됩니다.
이 파이프는 원시 문자열을 받는다고 전제하며, 이미 변환된 number가 들어오면 거부합니다. 전역 파이프가 먼저 실행되므로, 모듈, 컨트롤러, 서비스의 기본 개념의 new ValidationPipe()처럼 변환 옵션을 켜지 않은 구성을 전제로 합니다. ValidationPipe({ transform: true })로 숫자 매개변수를 먼저 변환하는 프로젝트라면 이 파이프의 입력도 숫자가 될 수 있으므로, 입력 타입과 파이프의 적용 방식을 함께 맞춥니다.
파이프는 @UsePipes()로 컨트롤러나 메서드에 적용할 수 있지만, 여기서는 ID 매개변수만 검사하도록 @Param()에 직접 연결합니다.
import { Controller, Get, Param } from '@nestjs/common';
import { PositiveIntPipe } from '../common/pipes/positive-int.pipe';
@Controller('items')
export class ItemsController {
@Get(':id')
// Pipe가 반환한 양의 안전한 정수를 받습니다.
findOne(@Param('id', PositiveIntPipe) id: number): string {
return `Item with ID: ${id}`;
}
}파일을 만든 뒤 src/app.module.ts에 import { ItemsController } from './items/items.controller';를 추가하고, @Module()의 controllers 배열을 [AppController, ItemsController]로 바꿉니다. 다른 컨트롤러, imports, providers, 미들웨어 설정은 그대로 둡니다.
전역 접두사로 api를 설정했다면 요청 경로는 GET /api/items/12이고, 접두사가 없는 구성에서는 GET /items/12입니다. 정상 경로에서는 문자열 '12'가 파이프를 거쳐 숫자 12로 바뀌고, 핸들러가 Item with ID: 12를 돌려줍니다. 다음 표는 이 파이프에 원시 문자열이 전달됐을 때의 예상 결과입니다. 앞의 가드 등이 요청을 먼저 막지 않고, 사용자 정의 예외 필터가 상태 코드를 바꾸지 않는다면 BadRequestException은 기본 400으로 응답됩니다.
원시 id 문자열 | 파이프의 결과 | 핸들러 실행 |
|---|---|---|
'12' | 숫자 12를 반환 | 실행되어 Item with ID: 12 반환 |
'12abc' | 숫자가 아닌 문자가 있어 BadRequestException | 실행 안 됨 |
'0' | 양수가 아니라서 BadRequestException | 실행 안 됨 |
'-1' | 부호가 있어 BadRequestException | 실행 안 됨 |
'abc' | 숫자가 아니라서 BadRequestException | 실행 안 됨 |
404가 나오면 먼저 컨트롤러 등록과 실제 전역 접두사를 확인합니다. 위 입력에서 예상과 다른 결과가 나오면 @Param('id', PositiveIntPipe)가 적용됐는지와 앞서 실행되는 전역 파이프의 변환 설정을 확인합니다. 파이프가 거부한 요청은 핸들러 안까지 들어가지 않으므로, 400을 볼 때는 핸들러보다 원시 문자열과 거부 규칙부터 살펴봅니다.
이 검사는 ID의 형식과 숫자 범위만 확인합니다. 그 ID가 실제로 있는지, 요청한 사용자가 접근할 수 있는지는 서비스의 조회와 별도의 인증·인가 규칙에서 확인해야 하며, 위 핸들러는 실제 항목을 조회하지 않고 문자열만 돌려주는 예제입니다.
인터셉터
인터셉터는 관점 지향 프로그래밍(AOP)의 개념을 구현한 기능으로, 핸들러 실행의 앞뒤에 로직을 끼워 넣거나, 응답 데이터를 바꾸거나, 예외를 처리합니다. AOP는 로깅이나 시간 측정처럼 여러 기능에 반복되는 부가 처리를 업무 코드와 떼어 한곳에 모으는 방법이며, Spring 교재의 횡단 관심과 AOP에서 자세히 다룹니다. 요청 쪽의 전처리는 가드 뒤, 파이프 앞에서 실행되며, next.handle()로 파이프와 핸들러를 감싼 뒤 반환 스트림에서 응답 후처리나 예외 처리를 합니다. RxJS의 Observable로 비동기 스트림을 다루므로 로깅, 캐싱, 응답 변환, 예외 처리, 트랜잭션 관리에 두루 쓰입니다.
인터셉터는 NestInterceptor 인터페이스를 구현하고 intercept() 메서드를 정의합니다.
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,
})),
);
}
}next.handle()은 핸들러를 실행하는 Observable을 돌려주며, 인터셉터가 이를 호출하지 않으면 핸들러는 아예 실행되지 않습니다. 위 코드는 pipe(map(...))로 그 결과 값만 바꾸므로, 핸들러가 실행되는 방식은 건드리지 않습니다. 인터셉터는 @UseInterceptors() 데코레이터로 적용하며, 아래처럼 클래스에 붙이면 그 컨트롤러의 모든 핸들러 응답이 같은 모양으로 감싸집니다.
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!" }이 map()은 핸들러가 정상적으로 내보낸 값을 { statusCode, message, data }로 바꿉니다. statusCode는 현재 응답 객체에서 읽으므로, 정상 GET은 보통 200, 상태를 따로 정하지 않은 정상 POST는 Nest의 기본값 201이 됩니다. 파이프나 서비스가 예외를 던지면 정상 값이 없으므로 이 map()이 성공 응답을 만들지 않으며, 오류를 바꾸려면 catchError() 같은 별도의 처리가 필요합니다.
전역 G, 컨트롤러 C, 메서드 R 인터셉터가 모두 정상 흐름을 감싸면 전처리는 G, C, R 순서로, 반환 쪽 후처리는 R, C, G 순서로 실행됩니다. 가장 안쪽부터 결과를 되돌려 주는 구조입니다. 이 방식은 핸들러가 값을 return하는 응답에 적용됩니다. @Res()로 응답을 직접 끝내는 방식은 일반적인 반환값 매핑과 함께 쓸 수 없으며, 응답 객체를 보조로만 쓰려면 passthrough 옵션의 조건을 확인합니다. 요청 수명 주기 문서와 인터셉터 문서에서 이 순서와 응답 매핑 조건을 확인할 수 있습니다.
실행 순서 요약
NestJS의 요청 처리는 다음 순서로 진행됩니다. 먼저 미들웨어가 공통 전처리를 하고, 가드가 라우트 핸들러에 접근할 수 있는지 판단합니다. 이어서 인터셉터의 전처리가 핸들러 실행을 감싸기 시작하고, 파이프가 핸들러의 인자를 검사하거나 바꿉니다. 그다음 라우트 핸들러가 서비스 호출 같은 실제 업무를 수행하고, 마지막으로 인터셉터의 후처리가 반환값을 바꾸거나 예외를 처리합니다.
각 단계는 서로 다른 관심사를 맡습니다. 실행 순서, 값의 변환을 누가 맡는지, 어디서 요청을 막는지, 오류가 어떤 응답으로 바뀌는지를 기준으로 알맞은 구성 요소를 고르면 애플리케이션을 유지보수하고 넓혀 가기 쉬워집니다.
정리
- 미들웨어는 가장 먼저 실행되는 공통 전처리이며, NestJS 11부터는
{*splat}처럼 이름 있는 와일드카드로 경로를 지정합니다. - 가드는 접근을 판단해
false면 기본 403으로 요청을 멈추고, 401이 필요하면UnauthorizedException을 직접 던집니다. - 파이프는 핸들러 직전에 인자를 검사하고 바꾸며, 형식 검사는 데이터의 존재나 접근 권한까지 확인하지 않습니다.
- 인터셉터는
next.handle()로 핸들러를 감싸며, 후처리는 전처리와 반대 순서로 풀립니다.
연습 문제
1. 어느 단계에서 멈추는가
같은 핸들러에 AuthGuard, 실행 전후 인터셉터, PositiveIntPipe를 등록했습니다. 전역 파이프는 값을 먼저 바꾸지 않고, 인터셉터는 오류를 잡지 않으며, 사용자 정의 필터는 없습니다. request.user가 없고 원시 ID가 '12'인 요청과, request.user가 있고 원시 ID가 '12abc'인 요청에서 각각 핸들러가 실행되는지와 기본 응답 상태를 쓰세요.
해설 보기
첫 요청은 가드가 false를 돌려주므로 기본 403입니다. 인터셉터, 파이프, 핸들러는 실행되지 않습니다.
두 번째 요청은 가드를 통과해 인터셉터의 전처리가 실행되고, 파이프가 문자열 형식 때문에 BadRequestException을 던져 기본 400이 됩니다. 핸들러는 실행되지 않으며, 인터셉터의 정상 값 변환용 map()도 정상 반환값이 없으므로 실행되지 않습니다. 오류를 처리하는 catchError()나 정리하는 finalize()를 따로 작성했다면 그 동작은 구분해서 봐야 합니다.
2. 통과했다는 말의 범위
전역 파이프의 선행 변환은 없고, AuthGuard와 PositiveIntPipe, TransformInterceptor만 씁니다. request.user = {}이고 원시 ID가 '0012'이면 가드와 파이프의 결과는 무엇이며, 이것으로 실제 로그인한 사용자라고 판단해도 되나요? 별도로 서버가 검증한 사용자가 있고 ID가 '12'인데 ID 12의 항목이 DB에 없다면, 파이프가 404를 만드나요?
해설 보기
빈 객체도 참으로 평가되어 가드를 통과하고, 파이프는 ASCII 숫자 문자열을 안전한 양의 정수 12로 바꿉니다. 그러나 가드는 그 객체가 어디서 왔는지나 인증 상태를 검증하지 않았으므로, 로그인했다는 뜻이 되지 않습니다.
파이프는 형식과 범위만 검사하므로 숫자 12를 그대로 넘기며 404를 만들지 않습니다. 문자열만 돌려주는 예제 핸들러는 존재 여부를 조회하지 않습니다. 실제로 조회하는 핸들러라면 저장소의 결과를 확인하고, 없을 때 NotFoundException을 던지는 처리가 따로 필요합니다.
3. 범위를 넘는 ID와 응답 변환
사용자 정보가 있고 ID가 '9007199254740992'인 요청은 숫자 문자로만 되어 있다는 이유로 핸들러까지 가나요? 별도로 정상 핸들러가 'created'를 돌려주고 현재 응답의 statusCode가 201일 때, TransformInterceptor가 내보낼 값과 G·C·R 세 인터셉터의 후처리 순서를 쓰세요.
해설 보기
변환한 값이 자바스크립트의 안전한 정수 범위를 벗어나므로 파이프가 BadRequestException을 던져 기본 400이 됩니다. 핸들러와 정상 값의 map()은 실행되지 않습니다.
정상 경우의 응답은 { statusCode: 201, message: 'Success', data: 'created' }입니다. 후처리는 R, C, G 순서로 되돌아오며, 정상 값의 변환이 모두 끝난 뒤 HTTP 응답이 전송됩니다.
다음 글 예외 처리와 필터에서는 가드와 파이프, 서비스가 던진 예외가 어떻게 HTTP 응답으로 바뀌는지와, 예외 필터로 그 응답을 제어하는 방법을 다룹니다.