NestJS Request Lifecycle 전체 흐름
NestJS에서 HTTP 요청이 들어오면 7단계의 파이프라인을 순서대로 통과한다. 각 단계에서 요청을 변환하거나, 차단하거나, 후처리할 수 있다. 이 순서를 정확히 이해해야 미들웨어·가드·인터셉터·파이프를 올바르게 배치할 수 있다.
전체 흐름은 다음과 같다:
- Middleware → 2. Guard → 3. Interceptor (before) → 4. Pipe → 5. Route Handler → 6. Interceptor (after) → 7. Exception Filter
1. Middleware: 가장 먼저 실행
Middleware는 Express/Fastify 레벨에서 동작하며, NestJS의 DI 시스템에 진입하기 전에 실행된다. 글로벌 미들웨어 → 모듈 미들웨어 순서로 실행된다.
// 실행 순서: Global Middleware → Module Middleware
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[Middleware] ${req.method} ${req.url}`);
// 여기서 next()를 호출하지 않으면 요청이 중단됨
next();
}
}
// Module에서 등록
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*');
}
}
용도: 로깅, CORS, 쿠키 파싱, 요청 ID 부여 등 NestJS 기능에 의존하지 않는 범용 처리.
2. Guard: 접근 제어 게이트
Guard는 canActivate()가 true를 반환해야 다음 단계로 진행한다. Global Guard → Controller Guard → Method Guard 순서로 실행된다.
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization;
if (!token) {
// false 반환 시 ForbiddenException 자동 발생
// → Exception Filter로 직행
return false;
}
return true;
}
}
// 실행 순서 확인
@UseGuards(ControllerGuard) // 2번째
@Controller('orders')
export class OrderController {
@UseGuards(MethodGuard) // 3번째 (Global Guard가 1번째)
@Get()
findAll() { ... }
}
핵심: Guard에서 false를 반환하거나 예외를 던지면, Interceptor·Pipe·Handler는 실행되지 않고 바로 Exception Filter로 넘어간다.
3. Interceptor (before): 요청 전처리
Interceptor는 RxJS Observable 기반으로, 핸들러 실행 전후에 모두 개입할 수 있다. next.handle() 호출 전이 “before”, 후가 “after”다.
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// ── before (Handler 실행 전) ──
const start = Date.now();
console.log('[Interceptor] Before handler');
return next.handle().pipe(
// ── after (Handler 실행 후) ──
tap(() => {
console.log(`[Interceptor] After handler: ${Date.now() - start}ms`);
}),
);
}
}
Global Interceptor → Controller Interceptor → Method Interceptor 순서로 실행된다.
4. Pipe: 변환과 검증
Pipe는 핸들러의 파라미터에 대해 동작한다. 변환(transform)과 검증(validation)을 담당한다.
@Controller('orders')
export class OrderController {
@Post()
create(
// Global Pipe → Parameter Pipe 순서
@Body(new ValidationPipe({ whitelist: true }))
dto: CreateOrderDto,
@Param('id', ParseIntPipe) // '123' → 123 변환
id: number,
) {
// Pipe 통과 후 dto는 검증 완료, id는 number 타입 보장
return this.orderService.create(id, dto);
}
}
핵심: Pipe에서 ValidationError가 발생하면, Handler는 실행되지 않고 Exception Filter로 넘어간다. 이때 Interceptor의 “after” 로직(Observable)도 실행되지 않는다.
5. Route Handler: 비즈니스 로직
모든 전처리를 통과한 후 실제 컨트롤러 메서드가 실행된다.
6. Interceptor (after): 응답 후처리
Handler가 반환한 값은 Interceptor의 next.handle() Observable을 통해 흐른다. 여기서 응답을 변환하거나 캐싱할 수 있다:
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
// Handler 반환값을 표준 응답 포맷으로 래핑
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
주의: after 단계의 실행 순서는 before의 역순이다. Method Interceptor → Controller Interceptor → Global Interceptor 순서로 실행된다 (양파 모델).
7. Exception Filter: 예외 최종 처리
Exception Filter는 파이프라인 어디에서든 발생한 예외를 잡는다. Method Filter → Controller Filter → Global Filter 순서로 매칭을 시도하며, 첫 번째로 매칭되는 필터가 처리한다.
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
response.status(status).json({
success: false,
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}
실행 순서 종합 정리
| 순서 | 컴포넌트 | 스코프 실행 순서 | 예외 시 |
|---|---|---|---|
| 1 | Middleware | Global → Module | Express 에러 핸들러 |
| 2 | Guard | Global → Controller → Method | Exception Filter |
| 3 | Interceptor (before) | Global → Controller → Method | Exception Filter |
| 4 | Pipe | Global → Parameter | Exception Filter |
| 5 | Handler | — | Exception Filter |
| 6 | Interceptor (after) | Method → Controller → Global | Exception Filter |
| 7 | Exception Filter | Method → Controller → Global | — |
흔한 실수와 해결법
1. Middleware에서 DI가 안 되는 이유
// ❌ 함수형 미들웨어는 DI 불가
export function logger(req, res, next) {
// this.service 접근 불가
next();
}
// ✅ 클래스 미들웨어는 DI 가능
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
constructor(private readonly configService: ConfigService) {}
use(req, res, next) {
// this.configService 사용 가능
next();
}
}
2. Guard vs Middleware: 어디서 인증할까?
Guard를 사용하라. Guard는 ExecutionContext에 접근할 수 있어 Reflector로 메타데이터(@Public(), @Roles())를 읽을 수 있다. Middleware는 어떤 핸들러가 실행될지 모른다.
3. Interceptor에서 예외 발생 시 흐름
// Interceptor before에서 예외 → Handler 실행 안 됨
// Interceptor after(pipe)에서 예외 → Exception Filter로 전달
return next.handle().pipe(
catchError(err => {
// Handler에서 발생한 예외를 여기서 잡을 수 있음
// 잡지 않으면 Exception Filter로 전달
return throwError(() => new InternalServerErrorException());
}),
);
정리
NestJS Request Lifecycle은 Middleware → Guard → Interceptor(before) → Pipe → Handler → Interceptor(after) → Exception Filter 순서로 동작한다. 각 컴포넌트는 고유한 역할과 실행 스코프가 있으며, 예외 발생 시 즉시 Exception Filter로 분기한다. 이 흐름을 정확히 이해하면, 인증·검증·변환·로깅·에러 처리를 올바른 위치에 배치하여 유지보수성 높은 아키텍처를 구축할 수 있다.