NestJS Interceptor란?
NestJS Interceptor는 @Injectable() 클래스에 NestInterceptor 인터페이스를 구현하여 만든다. 요청(Request)과 응답(Response) 양쪽 흐름에 개입할 수 있어, AOP(Aspect-Oriented Programming) 패턴을 NestJS에서 구현하는 핵심 도구다.
Interceptor가 동작하는 위치는 Guard 이후, Pipe 이전(요청 단계)이며, 컨트롤러 핸들러 실행 이후 응답 스트림에도 개입한다. 이 양방향 특성 덕분에 로깅, 캐싱, 응답 변환, 타임아웃 등 다양한 횡단 관심사를 깔끔하게 분리할 수 있다.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
const req = context.switchToHttp().getRequest();
console.log(`[REQ] ${req.method} ${req.url}`);
return next.handle().pipe(
tap(() => console.log(`[RES] ${req.method} ${req.url} - ${Date.now() - now}ms`)),
);
}
}
next.handle()이 반환하는 Observable이 핵심이다. 이 스트림을 RxJS 오퍼레이터로 조작하면 응답 데이터를 자유롭게 변환할 수 있다.
실전 패턴 1: 응답 래핑 (Response Mapping)
API 응답을 일관된 포맷으로 통일하는 것은 프론트엔드 협업의 기본이다. Interceptor로 모든 응답을 { success, data, timestamp } 형태로 래핑할 수 있다.
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseWrapInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
이 패턴의 장점은 컨트롤러에서 순수한 비즈니스 데이터만 반환하면 된다는 것이다. 응답 포맷 변경 시 Interceptor 하나만 수정하면 전체 API에 반영된다.
실전 패턴 2: RxJS 기반 타임아웃
외부 API 호출이나 무거운 쿼리에 타임아웃을 걸어 서버 리소스를 보호하는 패턴이다.
import { timeout, catchError } from 'rxjs/operators';
import { throwError, TimeoutError } from 'rxjs';
import { RequestTimeoutException } from '@nestjs/common';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
constructor(private readonly ms: number = 5000) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(this.ms),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException(
`Request timed out after ${this.ms}ms`,
));
}
return throwError(() => err);
}),
);
}
}
RxJS timeout 오퍼레이터가 지정 시간 내에 응답이 오지 않으면 TimeoutError를 발생시키고, 이를 NestJS의 RequestTimeoutException으로 변환한다. HTTP 408 상태 코드가 자동으로 반환된다.
실전 패턴 3: 인메모리 캐싱
동일한 GET 요청에 대해 일정 시간 캐시된 응답을 반환하는 Interceptor다. next.handle()을 호출하지 않으면 컨트롤러 자체가 실행되지 않는다는 점을 활용한다.
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, { data: any; expiresAt: number }>();
constructor(private readonly ttlMs: number = 30000) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
if (req.method !== 'GET') return next.handle();
const key = req.url;
const cached = this.cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return of(cached.data); // 컨트롤러 실행 안 함
}
return next.handle().pipe(
tap(data => {
this.cache.set(key, {
data,
expiresAt: Date.now() + this.ttlMs,
});
}),
);
}
}
프로덕션 환경에서는 Map 대신 Redis를 사용하고, NestJS 공식 @nestjs/cache-manager 모듈과 결합하는 것이 좋다. 하지만 원리를 이해하면 커스텀 캐싱 전략을 자유롭게 구현할 수 있다.
Interceptor 바인딩 3가지 방법
| 방법 | 범위 | 코드 |
|---|---|---|
| 메서드/컨트롤러 | 특정 핸들러 또는 컨트롤러 | @UseInterceptors(LoggingInterceptor) |
| 글로벌 (모듈) | 전체 애플리케이션 (DI 가능) | APP_INTERCEPTOR provider 등록 |
| 글로벌 (부트스트랩) | 전체 애플리케이션 (DI 불가) | app.useGlobalInterceptors() |
가장 권장되는 방법은 APP_INTERCEPTOR를 사용한 글로벌 등록이다. DI 컨테이너를 통해 주입되므로 다른 서비스에 의존할 수 있다.
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: ResponseWrapInterceptor,
},
],
})
export class AppModule {}
Interceptor 실행 순서
여러 Interceptor를 등록하면 양파(Onion) 모델로 실행된다. 요청 시에는 등록 순서대로, 응답 시에는 역순으로 실행된다.
// 등록 순서: A → B → C
// 요청 흐름: A.before → B.before → C.before → Handler
// 응답 흐름: C.after → B.after → A.after → Client
이 순서는 Express 미들웨어와 동일한 패턴이다. 로깅 Interceptor는 가장 바깥(첫 번째)에, 응답 변환은 가장 안쪽(마지막)에 배치하는 것이 일반적이다.
실전 패턴 4: ExecutionContext로 분기 처리
ExecutionContext는 현재 요청의 컨텍스트 정보를 제공한다. HTTP, WebSocket, gRPC 등 다양한 전송 계층에서 동일한 Interceptor를 재사용할 수 있다.
@Injectable()
export class SmartLoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const type = context.getType(); // 'http' | 'ws' | 'rpc'
const handler = context.getHandler();
const controller = context.getClass();
// 커스텀 데코레이터 메타데이터 읽기
const isPublic = Reflect.getMetadata('isPublic', handler);
if (isPublic) return next.handle(); // 퍼블릭 엔드포인트는 로깅 스킵
const label = `${controller.name}.${handler.name}`;
console.log(`[${type.toUpperCase()}] ${label} - started`);
return next.handle().pipe(
tap(() => console.log(`[${type.toUpperCase()}] ${label} - completed`)),
);
}
}
Reflect.getMetadata를 활용하면 커스텀 데코레이터(@Public(), @CacheTTL(60) 등)와 Interceptor를 연동할 수 있어 매우 유연한 설계가 가능하다.
Interceptor vs Middleware vs Guard 비교
| 구분 | 실행 시점 | 응답 접근 | DI 지원 | 주 용도 |
|---|---|---|---|---|
| Middleware | 가장 먼저 | 제한적 | 제한적 | 요청 전처리, CORS |
| Guard | Middleware 이후 | 없음 | 완전 | 인증/인가 |
| Interceptor | Guard 이후 | 완전 (RxJS) | 완전 | 로깅, 캐싱, 변환 |
| Pipe | Interceptor 이후 | 없음 | 완전 | 유효성 검증, 변환 |
Interceptor만이 요청과 응답 양쪽 모두에 개입할 수 있다는 점이 가장 큰 차별점이다. 이 특성 덕분에 실행 시간 측정, 응답 변환, 에러 매핑 같은 작업에 최적이다.
프로덕션 팁: 에러 매핑 Interceptor
외부 라이브러리에서 발생하는 에러를 NestJS HttpException으로 변환하는 패턴도 자주 쓰인다.
import { catchError } from 'rxjs/operators';
import { BadRequestException, ConflictException } from '@nestjs/common';
@Injectable()
export class DbErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(err => {
if (err.code === '23505') { // PostgreSQL unique violation
return throwError(() => new ConflictException('중복된 데이터입니다'));
}
if (err.code === '23503') { // FK violation
return throwError(() => new BadRequestException('참조 무결성 위반'));
}
return throwError(() => err);
}),
);
}
}
이렇게 하면 컨트롤러와 서비스 계층에서 DB 에러를 일일이 처리하지 않아도 된다. Interceptor가 횡단 관심사를 깔끔하게 분리해주는 대표적인 예시다.
정리
NestJS Interceptor는 RxJS Observable 기반으로 요청/응답 양방향에 개입하는 강력한 AOP 도구다. 응답 래핑, 타임아웃, 캐싱, 에러 매핑 등 프로덕션 필수 패턴을 Interceptor 하나로 해결할 수 있다. ExecutionContext와 커스텀 데코레이터를 결합하면 메타데이터 기반 동적 분기도 가능하다. Guard, Pipe, Middleware와의 역할을 명확히 구분하고, 양파 모델 실행 순서를 이해하면 유지보수성 높은 NestJS 애플리케이션을 설계할 수 있다.