미들웨어, 인터셉터, 파이프, 가드
요청 처리 파이프라인의 실행 순서를 따라 미들웨어·가드·파이프·인터셉터의 책임과 적용 지점을 구분합니다.
NestJS 애플리케이션은 클라이언트 요청을 받아 응답을 보내기까지 여러 단계를 거칩니다.
이 과정에서 요청 데이터 변환, 유효성 검사, 권한 확인 같은 부가 작업이 수행됩니다.
NestJS는 이를 효율적으로 처리하도록 미들웨어(Middleware), 인터셉터(Interceptor), 파이프(Pipe), 가드(Guard)를 제공합니다.
이들은 공장 컨베이어 벨트처럼 순서대로 동작하며 요청 처리 파이프라인을 구성합니다.
이 절에서는 각 요소가 무엇이며, 어떤 역할을 수행하고, 언제 사용해야 하는지 자세히 알아보겠습니다.
요청은 순서대로 진입하고 인터셉터를 역순으로 빠져나온다
정상 진입·역순 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 응답 범위를 구분합니다. |
미들웨어: Express.js와의 연결 고리
NestJS는 내부적으로 HTTP 서버 프레임워크인 Express.js를 기본값으로 사용합니다.
미들웨어는 HTTP 요청을 받아 다음 처리 단계로 넘기거나 직접 응답을 끝낼 수 있는 함수입니다. Nest의 요청 수명주기에서는 가드와 라우트 핸들러보다 먼저 실행됩니다. next() 아래에 쓴 코드가 비동기 핸들러의 응답 완료를 기다리는 것은 아니므로, 실행 전후 처리를 감싸려면 인터셉터를 사용하고 실제 응답 완료 관찰은 응답 객체의 이벤트와 구분합니다.
- 가장 먼저 실행: 요청 파이프라인에서 가장 먼저 실행되는 단계 중 하나입니다.
- 요청 객체 접근:
req,res,next객체에 직접 접근하여 요청 헤더, 바디, 응답 등을 조작할 수 있습니다. - 인증 및 로깅: 모든 요청에 대해 공통적으로 적용되는 로깅, 인증/인가(세션 기반), CORS 처리 등에 적합합니다.
미들웨어는 클래스 기반 또는 함수 기반으로 작성할 수 있습니다.
클래스 기반 미들웨어를 사용하려면 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() 메서드를 사용하여 적용합니다.
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 }); // 특정 경로와 메서드에 적용
}
}가드: 권한 및 인증 관리
가드는 특정 라우트 핸들러가 실행되기 전에 요청을 가로채서 특정 조건(주로 권한이나 인증 정보)을 만족하는지 확인하는 역할을 합니다.
마치 클럽 입구에서 신분증을 검사하는 경비원과 같습니다.
조건이 만족되면 요청은 다음 단계로 진행됩니다. 가드가 false를 반환하면 Nest는 ForbiddenException을 발생시켜 기본 403 Forbidden으로 응답합니다. 401 Unauthorized가 필요한 인증 실패는 UnauthorizedException처럼 의도한 예외를 명시해야 합니다. 예외 필터가 설치되어 있다면 최종 응답 형식은 그 정책에 따릅니다.
- 실행 시점: 미들웨어보다 늦게, 하지만 인터셉터와 파이프보다 먼저 실행됩니다.
- 권한 부여: 사용자 역할 기반 접근 제어(RBAC), JWT 토큰 유효성 검증 등 인증(Authentication) 및 인가(Authorization) 로직에 최적화되어 있습니다.
CanActivate인터페이스:CanActivate인터페이스를 구현하고canActivate()메서드에서 boolean 값을 반환합니다.
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() 데코레이터를 사용하여 적용합니다.
request.user가 있다는 것과 인증이 끝났다는 것은 다릅니다
위 가드는 사용자 정보의 존재만 검사하는 학습용 조각입니다. {}도 참으로 평가되므로 통과합니다. 앞의 로깅 미들웨어는 request.user를 만들지 않으며, 클라이언트가 보낸 body.user나 role: 'admin'을 복사해 넣어서는 인증 근거가 되지 않습니다. 실제 인증 단계에서는 서버가 세션이나 토큰을 검증하고, 그 결과로 사용자를 확정한 뒤 접근 정책을 검사해야 합니다. 토큰 문자열을 해석하는 것만으로 서명·만료 검증이 끝나지도 않습니다.
가드는 파이프보다 먼저 실행되므로, 가드에서 경로 ID를 읽더라도 그 값은 아직 PositiveIntPipe가 만든 숫자가 아닙니다. 가드의 접근 판단과 파이프의 값 변환을 분리하세요. 이 절의 Request·Response 사용 예제는 Express 기반 HTTP 어댑터를 전제로 하며, Fastify로 바꾸면 요청·응답 타입과 API도 맞춰야 합니다.
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는 사용자 객체가 있는지만 확인합니다. 헤더의 토큰을 검증하거나 사용자를 새로 만드는 코드는 아니므로, 사용자 준비 단계가 없으면 요청은 거부됩니다.
파이프: 데이터 변환 및 유효성 검사
파이프는 들어오는 요청 데이터의 변환(Transformation) 또는 유효성 검사(Validation)를 수행하는 역할을 합니다.
컨트롤러의 라우트 핸들러로 데이터가 전달되기 전에 데이터를 원하는 형식으로 가공하거나, 정의된 규칙에 따라 데이터가 올바른지 확인하는 데 사용됩니다.
주요 특징 및 사용 사례- 실행 시점: 가드 다음에 실행되며, 요청 핸들러 메서드에 전달되기 직전에 실행됩니다.
- 데이터 처리:
string을number로 변환하거나, DTO(Data Transfer Object) 유효성 검사를 수행합니다. PipeTransform인터페이스:PipeTransform인터페이스를 구현하고transform()메서드를 오버라이드합니다.
NestJS는 ValidationPipe, ParseIntPipe 등 유용한 빌트인 파이프를 제공합니다.
예를 들어 /items/12의 12는 라우트에서 꺼낸 문자열입니다. 핸들러의 타입을 id: number로 적는 것만으로는 이 문자열이 숫자로 바뀌지 않습니다. 숫자 ID를 받으려면 어떤 문자열을 허용할지 정하고, Pipe에서 검사한 뒤 변환해야 합니다.
아래 PositiveIntPipe는 문자열 전체가 ASCII 숫자 0부터 9로만 이루어져 있고, 변환한 값이 양의 안전한 정수일 때 통과시킵니다. 안전한 정수 범위에서는 JavaScript의 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를 반환합니다. 이 예제는 그런 입력을 ID로 받아들이지 않으므로, Number()로 바꾸기 전에 문자열 전체를 검사합니다. 공백, 부호, 소수점, 지수 표기가 들어간 문자열은 거부합니다. 앞쪽의 0은 허용하므로 '01'은 숫자 1이 되지만, '0'이나 '000'은 양수가 아니어서 거부됩니다. '9007199254740992'처럼 안전한 정수 범위를 넘는 값도 거부합니다.
이 Pipe는 원시 문자열을 받는 예제입니다. 문자열 대신 이미 변환된 number가 들어와도 거부합니다. 전역 Pipe가 먼저 실행되므로, 애플리케이션 시작 설정의 new ValidationPipe()처럼 변환 옵션을 켜지 않은 구성을 전제로 합니다. 기존 프로젝트에서 ValidationPipe({ transform: true })로 숫자 매개변수를 먼저 변환한다면 이 Pipe의 입력도 숫자가 될 수 있습니다. 그 경우에는 입력형과 Pipe 적용 방식을 함께 조정해야 합니다.
파이프는 @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와 미들웨어 설정은 그대로 유지합니다.
앞 장의 main.ts처럼 app.setGlobalPrefix('api')를 사용한다면 요청 경로는 GET /api/items/12입니다. 전역 접두사가 없는 구성에서는 GET /items/12가 됩니다. 정상 경로에서는 문자열 '12'가 Pipe를 거쳐 숫자 12로 바뀌고, 핸들러가 Item with ID: 12를 반환합니다.
다음 표는 이 Pipe에 원시 문자열이 전달됐을 때의 예상 결과입니다. 앞의 가드 등이 요청을 먼저 차단하지 않고, 사용자 정의 예외 필터가 상태 코드를 바꾸지 않는 경우 BadRequestException은 기본 HTTP 400으로 응답됩니다.
원시 id 문자열 | Pipe의 결과 | 핸들러 실행 |
|---|---|---|
'12' | 숫자 12 반환 | 실행, Item with ID: 12 반환 |
'12abc' | 숫자 이외의 문자가 있어 BadRequestException | 미실행 |
'0' | 양수가 아니어서 BadRequestException | 미실행 |
'-1' | 부호가 있어 BadRequestException | 미실행 |
'abc' | 숫자가 아니어서 BadRequestException | 미실행 |
404가 나오면 먼저 컨트롤러 등록과 실제 전역 접두사를 확인합니다. 위 입력에서 예상과 다른 결과가 나오면 @Param('id', PositiveIntPipe)의 적용 여부와 선행 전역 Pipe의 변환 설정을 확인합니다. Pipe가 거부한 요청은 핸들러 내부까지 들어가지 않으므로, 400을 확인할 때는 핸들러의 처리보다 원시 문자열과 거부 규칙부터 살펴봅니다.
이 검사는 ID의 형식과 숫자 범위만 확인합니다. 해당 ID가 실제로 존재하는지, 요청한 사용자가 접근할 수 있는지는 서비스의 조회와 별도의 인증·인가 규칙에서 확인해야 합니다. 위 핸들러는 문자열을 반환하는 예제이며, 실제 항목 조회를 구현하지 않습니다.
인터셉터: 응답 변환 및 부가 로직
인터셉터는 AOP(Aspect-Oriented Programming) 개념을 구현한 기능으로, 요청 전/후에 추가적인 로직을 삽입하거나, 응답 데이터를 변환하거나, 예외를 처리하는 등의 역할을 수행합니다.
함수 호출 전후에 특정 작업을 수행하는 로직을 삽입하는 것과 유사합니다.
주요 특징 및 사용 사례- 실행 시점: 요청 쪽 전처리는 가드 뒤·파이프 앞에서 실행됩니다. 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,
})),
);
}
}인터셉터는 @UseInterceptors() 데코레이터를 사용하여 적용합니다.
정상 반환 값만 감싸는 이유와 여러 인터셉터의 순서
이 map()은 핸들러가 정상적으로 내보낸 값을 { statusCode, message, data }로 바꿉니다. statusCode는 현재 응답 객체에서 읽으므로 정상 GET은 보통 200, 별도 상태 설정이 없는 정상 POST는 Nest 기본값 201을 반영할 수 있습니다. 파이프나 서비스가 예외를 던지면 정상 값이 없어서 이 map()으로 성공 응답을 만들지 않습니다. 오류를 변환하려면 catchError() 등 별도 처리가 필요합니다.
전역 G, 컨트롤러 C, 메서드 R 인터셉터가 모두 정상 흐름을 감싸면 전처리는 G → C → R, 반환 쪽 후처리는 R → C → G 순서입니다. 가장 안쪽부터 결과를 되돌려 주는 구조입니다. 이 예제처럼 핸들러가 값을 return하는 응답 방식에 적용하세요. @Res()로 응답을 직접 끝내는 방식은 일반적인 반환 값 매핑과 함께 쓸 수 없으며, 응답 객체만 보조적으로 쓰려면 passthrough 방식의 조건을 확인해야 합니다. 공식 요청 생명주기와 인터셉터 문서에서 이 순서와 응답 매핑 조건을 확인할 수 있습니다.
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!" }요청 처리 파이프라인 요약 및 실행 순서
미들웨어, 인터셉터, 파이프, 가드를 비교할 때 실행 순서, 변환 책임, 차단 조건, 오류 매핑 기준을 확인합니다.
NestJS의 요청 처리 파이프라인은 다음과 같은 순서로 실행됩니다.
미들웨어 (Middleware): 가장 먼저 실행되어 공통적인 전처리(로깅, 인증 등)를 수행합니다.
가드 (Guards): 미들웨어 다음으로 실행되어 라우트 핸들러에 대한 접근 권한을 확인합니다.
인터셉터 (Interceptors) - 전(Pre-handler): 라우트 핸들러 실행 전에 추가 로직(예: 요청 로깅, 캐싱)을 수행합니다.
파이프 (Pipes): 라우트 핸들러의 매개변수가 유효한지 검사하거나 변환합니다.
라우트 핸들러 (Controller Method): 컨트롤러의 실제 비즈니스 로직(서비스 호출 등)이 실행됩니다.
인터셉터 (Interceptors) - 후(Post-handler): 라우트 핸들러의 응답이 반환된 후 최종 응답 변환, 예외 처리 등을 수행합니다.
요청은 모든 단계를 반드시 끝까지 지나가는 것이 아니라, 가드와 파이프에서 멈추거나 인터셉터를 되돌아 나오며 응답이 변환될 수 있습니다.
실행되는 단계를 예상해 보기
같은 핸들러에 AuthGuard, 실행 전후 인터셉터, PositiveIntPipe를 등록했다고 가정합니다. 전역 파이프는 값을 먼저 변환하지 않고, 인터셉터는 오류를 잡지 않으며, 사용자 정의 필터는 없다고 합니다. 다음 두 요청에서 핸들러가 실행되는지와 기본 응답 상태를 먼저 적어 보세요.
request.user가 없고 원시 ID는'12'입니다.request.user가 있고 원시 ID는'12abc'입니다.
어느 경계에서 멈추는가
첫 요청은 가드가 false를 반환하므로 기본 403입니다. 인터셉터와 파이프, 핸들러는 실행하지 않습니다. 두 번째 요청은 가드를 통과해 인터셉터 전처리가 실행되고, 파이프가 문자열 형식 때문에 BadRequestException을 던져 기본 400이 됩니다. 핸들러는 실행하지 않습니다. 인터셉터의 정상 값 변환용 map()도 정상 반환 값이 없으므로 실행되지 않습니다. 오류를 처리하는 catchError()나 정리하는 finalize()를 별도로 작성했다면 그 동작은 구분해야 합니다.
이러한 파이프라인의 각 요소는 서로 다른 관심사를 분리하고, 애플리케이션의 유지보수성과 확장성을 크게 향상시킵니다.
필요한 지점에 맞는 요소를 조합하면 NestJS 애플리케이션의 유지보수성과 확장성을 높일 수 있습니다.
다음 절에서는 NestJS에서 예외를 효과적으로 처리하는 방법에 대해 알아보겠습니다.
연습: 통과했다는 말의 범위
전역 파이프의 선행 변환은 없고, AuthGuard와 PositiveIntPipe, 위 TransformInterceptor만 사용한다고 가정합니다. 인터셉터는 오류를 잡지 않고 사용자 정의 필터도 없습니다.
request.user = {}이고 원시 ID가'0012'이면 가드와 파이프의 결과는 무엇인가요? 이 결과만으로 실제 로그인한 사용자라고 판단해도 될까요?- 서버에서 검증한 사용자가 있고 ID가
'12'입니다. ID 12의 항목이 DB에 없더라도 파이프가404를 만들까요? 앞의ItemsController처럼 문자열만 반환하는 핸들러와 실제 조회 핸들러를 구분해 보세요. - 사용자 정보가 있고 ID가
'9007199254740992'입니다. 숫자 문자로만 되어 있다는 이유로 핸들러까지 갈까요? - 정상 핸들러가
'created'를 반환하며 현재 응답의statusCode는201입니다. 인터셉터가 내보낼 값과 G·C·R 후처리 순서를 적어 보세요.
존재 검사·형식 검사·응답 변환 풀이
- 빈 객체는 참으로 평가되어 가드가 통과합니다. 파이프는 ASCII 숫자 문자열을 안전한 양의 정수
12로 바꿉니다. 그러나 가드는 객체의 출처나 인증 상태를 검증하지 않았으므로 로그인 증거가 되지 않습니다. - 파이프는 형식과 범위만 검사하므로 숫자
12를 넘깁니다. 문자열만 반환하는 예제는 존재 여부를 조회하지 않습니다. 실제 조회 핸들러는 저장소 결과를 확인하고, 없을 때NotFoundException을 던지는 등 별도의 처리가 필요합니다. - 변환 결과가 JavaScript 안전한 정수 범위를 벗어나므로 파이프가
BadRequestException을 던져 기본400응답이 됩니다. 핸들러와 정상 값map()은 실행되지 않습니다. { statusCode: 201, message: 'Success', data: 'created' }입니다. 후처리는R → C → G로 되돌아옵니다. 정상 값의 변환이 완료된 뒤 HTTP 응답이 전송됩니다.