
NestJS의 요청 생명 주기 (Request lifecycle)
NestJS 애플리케이션에서는 클라이언트가 요청을 보내고 응답을 받을 때 까지 여러 컴포넌트가 순차적으로 실행된다.
이 흐름을 요청 생명주기(Request Lifecycle)이라고 부른다.
이 요청이 처리되는 전체 흐름을 이해하면 미들웨어, 파이프, 가드, 인터셉터, 필터 같은 여러 구조가 어떤 순서로 실행되는지 파악할 수 있다. 그리고 코드 디버깅이나 성능 튜닝 및 보안 설정시에 해당 기능이 어디서 실행되는지를 알면 문제 해결이 더 쉬워질 것 이다.
일반적으로 NestJS의 요청 생명주기는 아래와 같다.
- Incoming request
- Middleware
- 2.1. Globally bound middleware
- 2.2. Module bound middleware
- Guards
- 3.1 Global guards
- 3.2 Controller guards
- 3.3 Route guards
- Interceptors (pre-controller)
- 4.1 Global interceptors
- 4.2 Controller interceptors
- 4.3 Route interceptors
- Pipes
- 5.1 Global pipes
- 5.2 Controller pipes
- 5.3 Route pipes
- 5.4 Route parameter pipes
- Controller (method handler)
- Service (if exists)
- Interceptors (post-request)
- 8.1 Route interceptor
- 8.2 Controller interceptor
- 8.3 Global interceptor
- Exception filters
- 9.1 route
- 9.2 controller
- 9.3 global
- Server response
Middleware

미들웨어(Middleware)는 라우트 핸들러(Controller) 이전에 실행되는 함수이다.
일반적으로 미들웨어는 요청과 응답 사이에서 작동하는 중간층(bridge layer) 이다. 요청과 응답 중간에 끼어서 공통적인 처리를 담당하는 층인 것이다. 미들웨어를 사용하게 되면,
- 모든 라우트에 중복되는 기능(로그, 인증 토큰 검사, 헤더 세팅 등)을 공통 로직으로 분리해서 관리할 수 있다.
- 요청 데이터 포맷, 헤더, 바디 등을 컨트롤러에 전달하기 전에 가공하는 전처리를 할수 있다.
- CORS, Rate Limit, Helmet 등 요청 레벨의 보안 정책을 적용할 수 있다.
- 요청마다 traceId를 부여해서 응답 시간, 상태 코드를 기록하여 서비스를 모니터링, 로그 할 수 있게된다.
- nestjs 내부에서 사용하는 express 또는 fastify 기반 서버의 동작 흐름에 개입이 가능하다.
nest-middleware.interface.d.ts
/**
* @see [Middleware](https://docs.nestjs.com/middleware)
*
* @publicApi
*/
export interface NestMiddleware<TRequest = any, TResponse = any> {
use(req: TRequest, res: TResponse, next: (error?: any) => void): any;
}
req, res 그리고 다음 단계로 넘기는 next()에 접근이 가능하다.
req와 res는 Node.js Htttp 객체이고, Nest 내부의 Request,Response 객체와 동일하다.
next()를 호출해야 다음 미들웨어나 컨트롤러로 넘어간다. 요청을 조기 종료하면 next()를 호출하지 않는다. 그래서 이어가고 싶다면 반드시 next() 호출이 필요하다.
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
private readonly logger = new Logger('HTTP');
use(req: any, res: any, next: () => void) {
// traceId 발급 및 시작 시각 기록
const traceId = randomUUID();
req.__traceId = traceId;
// 고해상도 시간 (나노초) → ms로 계산 예정
req.__startAtNs = process.hrtime.bigint();
// 응답 헤더에 traceId 노출 (클라이언트/로그 상호 참조)
res.setHeader('x-trace-id', traceId);
// 응답 종료 시점에 한번만 로그 출력
res.on('finish', () => {
const endNs = process.hrtime.bigint();
const startNs: bigint = req.__startAtNs ?? endNs;
const latencyMs = Number(endNs - startNs) / 1_000_000;
const log = {
method: req.method,
path: req.originalUrl || req.url,
status: res.statusCode,
latencyMs: Number(latencyMs.toFixed(2)),
traceId,
};
// 단일 라인 로그 (후속 단계에서 pino-http로 대체 예정)
this.logger.log(JSON.stringify(log));
});
next();
}
}
위의 코드는 로깅을 하기 위해 전역적으로 사용하기위해 만든 로거이다.
흔히 위의 형태와 같이 @Injectable()과 NestMiddleware의 조합으로 구현하여 의존성을 주입해서 사용하는 클래스 기반 방식으로 사용하게 된다. 간단한 형태라면 함수형으로 사용해도 된다.
단, 이렇게 전역 미들웨어로 사용하기 위해서는 app.module 또는 main에서 등록을 해주어야 한다.
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestLoggerMiddleware).forRoutes('*');
}
}
모듈별로 인터셉터를 만들어 특정 모듈 내에서만 작동하도록 할 수 있다. 이런 경우에는 해당 모듈에만 의존성을 주입하여 적용하면 된다.
주로 사용되는 곳
- 요청 로깅, 트레이싱
- 보안 및 헤더 설정 작업
- 파싱과 전처리
컨트롤러 이전 단게에서 요청과 응답 객체를 작업하거나 공통 정책을 강제할 때 적합하다.
Guards

가드(Guard)는 요청이 컨트롤러나 핸들러에 도달하기 직전에 해당 요청이 실행할 자격이 있는지를 판단하는 보안 필터이다.
즉, 인가(Authorization)를 담당하는 계층이다. 요청 자체의 허용 여부를 결정하는 것이다.
보통 3가지 형태로 사용이 되는데,
1. JWT 기반 인증 가드
- 헤더의 Authorization 토큰 검증
- 토큰 디코드 후 사용자 정보(req.user) 주입
- 인증 실패 시 UnauthorizedException 발생
2. Role-based Access 가드
- 컨트롤러 핸들러에 @Roles('admin') 데코레이터 부착
- Guard에서 Reflector를 사용하여 해당 메타데이터를 읽고 비교
- 권한 없으면 ForbiddenException 발생
3. 요청 조건 검증
- 특정 시간대에만 접근 가능
- IP 화이트리스트 제어
- API Key 유효성 검증
간단한 사용 방식에 대해서는 공식 문서에 정리가 잘 되어 있다.
https://docs.nestjs.com/guards
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea
docs.nestjs.com
Interceptors

인터셉터는 요청과 응답 처리 과정에 개입하여 추가 로직을 수행하는 기능이다.
AOP(Aspect-Oriented Programming) 원칙에 따라 핵심 비즈니스 로직과 분리하여 로깅, 데이터 변환, 예외 처리, 헤더 추가 등 다양한 작업을 수행할 수 있다. 인터셉터는 메서드 실행 전후의 로직을 확장하거나, 반환된 값을 변환하며, 함수 자체를 재정의할 수도 있다.
NestJS의 공식 문서에서 Interceptor에 대한 정의를 살펴보면,
“Interceptors are capable of binding extra logic before/after method execution,
transforming results, extending basic behavior, or even overriding the method completely.”
인터셉터는 단순히 요청 전처리가 아닌, 컨트롤러의 실행 자체를 감싸는 래퍼(wrapper) 역할을 하며 요청이 들어오면 아래 두 단계를 모두 통제할 수 있다.
- Pre-controller phase: Controller 실행 전에 개입
→ 로깅, 인증 전 데이터 준비, 캐시 검사 등- 앞 단계의 가드에서 인가를 판단하고, 뒤의 파이프에서는 요청 데이터를 관리하게 되는데 인터셉터는 그 사이에서 그 과정 전체를 관찰하게 된다.
- Post-controller phase: Controller 실행 후에 개입
→ 응답 변환, 에러 매핑, 응답 캐싱, 결과 감싸기 등
NestJS 의 NestInterceptor
import { Observable } from 'rxjs';
import { ExecutionContext } from './execution-context.interface';
/**
* Interface providing access to the response stream.
*
* @see [Interceptors](https://docs.nestjs.com/interceptors)
*
* @publicApi
*/
export interface CallHandler<T = any> {
/**
* Returns an `Observable` representing the response stream from the route
* handler.
*/
handle(): Observable<T>;
}
/**
* Interface describing implementation of an interceptor.
*
* @see [Interceptors](https://docs.nestjs.com/interceptors)
*
* @publicApi
*/
export interface NestInterceptor<T = any, R = any> {
/**
* Method to implement a custom interceptor.
*
* @param context an `ExecutionContext` object providing methods to access the
* route handler and class about to be invoked.
* @param next a reference to the `CallHandler`, which provides access to an
* `Observable` representing the response stream from the route handler.
*/
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>;
}
클래스 기반 형태 사용
@Injectable()
export class TransformInterceptor implements NestInterceptor {}
위와 같은 방식으로 클래스 기반 형태로 사용할 수 있다.
전역적으로 사용하기 위해서 app.module에 등록해서 사용할 수 있다.
@Module({
imports: [],
providers: [
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
],
})
위와 같은 형태로 모둘에서 APP_INTERCEPTOR 토큰으로 등록을하면 전역이면서 DI가 가능해진다.
주로 사용 되는 곳
- 로깅과 메트릭
- 응답 포맷 통일
- 에러 매핑
Middleware과의 차이점
겉보기에 Middleware도 요청 흐름 중간에서 무언가를 하는 역할이라 비슷해 보일 수 있지만, Middleware과 Interceptor은 근본적으로 동작하는 레벨과 관점이 완전히 다르다. 먼저 표로 정리해보자면,
| 구분 | Middleware | Interceptor |
| 소속 레벨 | Express / Fastify (플랫폼 레벨) | NestJS 내부 (프레임워크 레벨) |
| 실행 시점 | Controller 도달 이전 | Controller 전·후 |
| 실행 범위 | 요청(Request) ~ 응답(Response) 전체 | Nest 컨텍스트(핸들러) 기준 |
| 위치 요약 | Request → Middleware → Guard → Interceptor → Pipe → Controller | Controller 내부 전후 |
| req/res 직접 접근 | 가능 | 불가능 (Nest ExecutionContext 통해서만 가능) |
미들웨어는 서버의 입구에서 요청 직후에 들어오는 모든 HTTP 요청을 가로채는 Express 계층이고
(공학 보안 검색대에서 모든 사람을 검사하고 통과시키는 느낌)
인터셉터는 NestJS의 라우트 핸들러(Controller)를 감싸는 NestJS 프레임워크 계층에 속한다.
(비행기 내 승무원처럼 출발 직전, 비행 중, 착륙 전후 활동 느낌)
그리고 미들웨어는 요청이 라우트 핸들러(Controller)에 도달하기 전에 실행되어 요청을 가로채거나 다음 단계로 제어를 넘겨주지만, 인터셉터는 라우트 핸들러(Controller)의 로직 수행 전후에 모두 실행될 수 있어 응답을 가로채거나 변형할 수 있다는 점에서 차이점이 있다.
미들웨어는 요청이 NestJS로 들어오기 전에 하는일, 인터셉터는 NestJS 안에서 요청을 실행하기 전후를 감싸는 일을 하는 것이다.
Pipes

파이프는 HTTP 요청 데이터가 컨트롤러에 전달되기 전에 데이터를 변환(transformation)하거나 유효성을 검사(validation)하는 역할을 한다.
@Injectable() 데코레이터가 붙은 @PipeTransform 인터페이스를 구현한 클래스로, 요청 데이터를 가공하여 컨트롤러 로직의 복잡성을 줄이고 코드의 반복을 방지하는데 사용된다.
값이 타입과 규칙에 맞도록 변환 (문자열→숫자, 문자열→UUID 등 원하는 타입/형태로 바꿔줌)하거나, 규칙 위반이면 예외를 던져 핸들러 실행 자체를 막을 수 있고, 설정에 따라 파이프에서 던진 예외를 예외 처리 해버릴 수 있다.
main.ts에서 useGlobalPipes()를 사용해 전역 설정해서 사용하는 것이 가장 일반적인 패턴이다.
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // DTO에 정의되지 않은 속성 자동 제거
transform: true, // 요청 데이터를 DTO 타입으로 자동 변환
forbidUnknownValues: false, // DTO에 정의되지 않은 속성이 있으면 예외 발생
transformOptions: { enableImplicitConversion: true },
// enableImplicitConversion: DTO에서 타입을 명시만 하면 자동 캐스팅
}),
);
Exception Filters

Nest에는 내장 예외 처리 계층이 존재하는데, 앱 코드에서 *처리되지 않은 예외가 발생하면 이 계층에서 잡아내 사용자 친화적인 응답을 생성한다. 커스텀 응답과 로깅이 필요하면 예외 필터를 만들어 이 동작을 바꿀 수 있다.
Exception Filter는 애플리케이션에서 발생한 예외를 가로채서 사용자에게 보낼 응답을 커스터마이징하는 기능이다.
기본적으로 내장된 전역 예외필터가 HttpException과 같은 표준 예외를 처리하긴하지만, 개발자는 이를 재정의해서 예외 로깅, 응답 데이터 구조 변경, 맞춤형 오류 메시지 전달 등 더 세밀한 제어가 가능하다.
주요 기능
- 예외 포착 : HttpException에서 파생된 예외나 처리되지 않은 다른 모든 예외를 가로챔
- 응답 커스터마이징: 클라이언트에게 전송되는 오류 응답의 HTTP 상태코드와 응답 본문(body)을 수정할 수 있다.
- 로깅: 예외 발생 시 로그를 기록하는 등의 부가적인 로직을 추가할 수 있다.
- 표준화된 응답: 애플리케이션 전반에 걸쳐 일관된 오류 응답 형식을 강제할 수 있다.
nestjs의 ExceptionFilter 인터페이스
import { ArgumentsHost } from '../features/arguments-host.interface';
/**
* Interface describing implementation of an exception filter.
*
* @see [Exception Filters](https://docs.nestjs.com/exception-filters)
*
* @publicApi
*/
export interface ExceptionFilter<T = any> {
/**
* Method to implement a custom exception filter.
*
* @param exception the class of the exception being handled
* @param host used to access an array of arguments for
* the in-flight request
*/
catch(exception: T, host: ArgumentsHost): any;
}
내장 필터를 그대로 사용하게되면,
@Get()
findAll() {
throw new ForbiddenException('접근 불가!');
}
이러한 코드가 있다면, Nest에서 자동으로 아래와 같은 형태로 응답을 반환한다.
Unhandled Exception (잡히지 않은 예외)
처리되지 않은 예외(잡히지 않은 예외)란 애플리케이션 레벨에서 try/catch로 처리되지 않은 에러를 말한다.
try/catch로 이미 처리한 예외 -> 잡힌 예외
컨트롤러나 서비스가 예외를 던졌지만 처리하지 않은 예외 -> 잡히지 않은 예외
이렇게 HttpException을 사용한 것은 정형화된 예외클래스는 맞지만 형식이 정해져 있는 것을 사용한 것이지 개발자가 직접 처리를 한 try/catch 형태의 에러처리를 한 것이 아니라 그냥 던진 것이기 때문에 처리되지 않은 예외이며, Nest의 기본 필터로 처리한 것이다.
{
"statusCode": 403,
"message": "접근 불가!"
}
나의 경우 아래와 같이 예외 처리 필터를 만들어 전역적으로 사용하고 있다.
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res = ctx.getResponse<any>();
const req = ctx.getRequest<any>();
const isHttp = exception instanceof HttpException;
const status = isHttp
? (exception as HttpException).getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = isHttp
? (exception as HttpException).getResponse()
: 'Internal server error';
const normalizedMessage =
typeof message === 'string'
? message
: ((message as any)?.message ?? 'Internal server error');
const body: ErrorBody = {
success: false,
error: {
message: normalizedMessage,
code: status,
},
meta: {
timestamp: new Date().toISOString(),
path: req?.url ?? '',
},
};
res.status(status).json(body);
}
}
정리 요약
NestJS 요청 라이프사이클
요청이 들어오면 플랫폼(express or fastify) 단의 전처리를 거쳐, NestJS의 보안,검증,흐름 제어를 통과한 뒤 컨트롤러, 서비스가 실행되고 응답으로 되돌아오는 동안 후처리와 예외 처리가 적용된다.
그 과정 순서는 Request → Middleware → Guard → Interceptor(전) → Pipe → Controller → Service → Interceptor(후) → Exception Filter → Response 이다.
- Middleware: 플랫폼 레벨 전처리
- Guard: 접근 허용/차단(인가)
- Interceptor(전/후): 핸들러 실행을 감싸 결과/예외/시간/캐시 제어
- Pipe: 컨트롤러 인자 변환·검증
- Exception Filter: 잡히지 않은 예외를 표준 응답으로 변환
MiddleWare 란?
미들워어는 컨트롤러 이전, 플랫폼(express of fastify)레벨에서 요청과 응답 객체(req,res)를 직접 다루는 전처리 체인이다.
공통 전처리(로깅, CORS, 보안 헤더, 세션, 원본 바디 파싱 등)를 모든 라우트에 일괄 적용하기 위해 사용한다.
플랫폼 의존적이다.(req/res 직접 접근, next() 필요)
Guards 란?
가드는 컨트롤러 실행 직전, 이 요청을 허용할지 거부할지 (인가) 를 결정하는 곳이다.
인증, 권한 정책 등 접근 제어를 코드 곳곳에 두는 것이 아니라 일반적으로 중앙화시키기 위해 사용한다.
Interceptors란?
인터셉터는 컨트롤러 실행을 전후로 감싸는 AOP 훅으로 결과 변환, 예외 매핑, 캐시, 타임아웃, 로깅 등을 처리한다.
핸들러를 감싸서 일관되게 요청과 응답을 관리한다.
Pipes란?
파이프는 컨트롤러 인자가 바인딩되기 직전에 값을 변환하고 검증하며 위반시 예외로 차단하는 역할을 한다.
Exception Filter란?
예외 필터는 잡히지 않은 예외를 받아 일관된 에러 응답으로 바꾸고, 필요 시 로깅 또는 마스킹을 수행하는 역할을 한다.
https://docs.nestjs.com/faq/request-lifecycle
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea
docs.nestjs.com
'NestJS' 카테고리의 다른 글
| nestjs와 spec.ts 그리고 테스트코드 (0) | 2025.10.08 |
|---|---|
| [NestJS] TypeOrmModule 다루기 (Repository) (0) | 2025.08.31 |
| [TypeORM] TypeORMError: Migration class name should have a JavaScript timestamp appended. (0) | 2025.08.31 |
| [NestJS] Validation failed (numeric string is expected) (2) | 2025.07.26 |
| [nestJS] Pipe와 커스텀 데코레이터 (feat. typedi) (1) | 2025.07.13 |