NestJS Interceptor란?
NestJS Interceptor는 요청/응답 파이프라인에서 핸들러 실행 전후에 로직을 삽입하는 AOP(Aspect-Oriented Programming) 구현체입니다. NestInterceptor 인터페이스를 구현하며, RxJS Observable을 기반으로 응답 스트림을 변환·가공할 수 있습니다. Guard가 “접근 가능한가”를 판단하고, Pipe가 “데이터가 유효한가”를 검증한다면, Interceptor는 “요청/응답을 어떻게 가공할 것인가”를 담당합니다.
기본 구조와 ExecutionContext
Interceptor의 핵심은 intercept() 메서드와 CallHandler입니다.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const now = Date.now();
console.log(`[REQ] ${method} ${url}`);
return next.handle().pipe(
tap((data) => {
console.log(`[RES] ${method} ${url} - ${Date.now() - now}ms`);
}),
);
}
}
next.handle()을 호출하기 전이 Pre-처리, .pipe() 안이 Post-처리입니다. next.handle()을 호출하지 않으면 핸들러가 실행되지 않아, 요청을 완전히 가로챌 수도 있습니다.
응답 변환: Response Mapping
API 응답을 일관된 포맷으로 감싸는 가장 흔한 패턴입니다.
// 표준 응답 래퍼
interface ApiResponse<T> {
success: boolean;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
// 적용 전: { id: 1, name: "John" }
// 적용 후: { success: true, data: { id: 1, name: "John" }, timestamp: "..." }
이 패턴은 모든 컨트롤러에서 일일이 래핑하는 보일러플레이트를 제거합니다. 글로벌로 적용하면 전체 API가 일관된 응답 구조를 갖게 됩니다.
캐싱 Interceptor
핸들러 실행을 건너뛰고 캐시된 값을 반환하는 패턴입니다.
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private readonly cacheManager: Cache) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const key = `cache:${request.method}:${request.url}`;
// 캐시 히트 → 핸들러 실행 없이 즉시 반환
const cached = await this.cacheManager.get(key);
if (cached) {
return of(cached); // next.handle() 호출 안 함!
}
// 캐시 미스 → 핸들러 실행 후 결과 캐싱
return next.handle().pipe(
tap((response) => {
this.cacheManager.set(key, response, 60_000); // 60초 TTL
}),
);
}
}
of(cached)로 즉시 Observable을 반환하면 next.handle()이 호출되지 않아 컨트롤러 로직이 완전히 스킵됩니다. 이것이 Interceptor가 Pipe나 Guard와 다른 핵심 능력입니다.
타임아웃과 에러 처리
RxJS 연산자를 활용한 타임아웃과 에러 핸들링입니다.
import { timeout, catchError, retry } from 'rxjs';
// 타임아웃 Interceptor
@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.name === 'TimeoutError') {
throw new RequestTimeoutException('요청 시간이 초과되었습니다');
}
throw err;
}),
);
}
}
// 재시도 Interceptor
@Injectable()
export class RetryInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
retry({
count: 3,
delay: (error, retryCount) => {
console.log(`재시도 ${retryCount}/3: ${error.message}`);
return timer(retryCount * 1000); // 1초, 2초, 3초 지수 백오프
},
}),
);
}
}
RxJS의 timeout, retry, catchError 연산자를 조합하면 복잡한 에러 복구 로직을 선언적으로 구현할 수 있습니다. 외부 API 호출이 많은 서비스에서 특히 유용합니다.
메타데이터 기반 Interceptor
Reflector로 커스텀 데코레이터의 메타데이터를 읽어 동적으로 동작을 변경합니다.
// 커스텀 데코레이터
export const CacheTTL = (seconds: number) => SetMetadata('cache_ttl', seconds);
export const NoCache = () => SetMetadata('no_cache', true);
@Injectable()
export class SmartCacheInterceptor implements NestInterceptor {
constructor(
private readonly reflector: Reflector,
private readonly cache: Cache,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 메타데이터 읽기
const noCache = this.reflector.get<boolean>('no_cache', context.getHandler());
if (noCache) return next.handle();
const ttl = this.reflector.getAllAndOverride<number>('cache_ttl', [
context.getHandler(),
context.getClass(),
]) ?? 60;
const key = this.buildKey(context);
// ... 캐시 로직 (ttl 활용)
}
}
// 컨트롤러에서 사용
@Controller('products')
@CacheTTL(300) // 클래스 레벨: 5분
export class ProductController {
@Get()
findAll() { ... } // 5분 캐시
@Get(':id')
@CacheTTL(60) // 메서드 레벨 오버라이드: 1분
findOne() { ... }
@Post()
@NoCache() // 캐시 제외
create() { ... }
}
getAllAndOverride는 메서드 → 클래스 순서로 메타데이터를 찾아 첫 번째 값을 반환합니다. 이 패턴은 NestJS ExecutionContext에서 다루는 Reflector 메커니즘의 실전 활용입니다.
파일 직렬화: Exclude와 Serialize
class-transformer와 연동하여 응답에서 민감한 필드를 자동 제거합니다.
import { ClassSerializerInterceptor } from '@nestjs/common';
// 글로벌 적용 (main.ts)
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
// Entity에서 직렬화 제어
import { Exclude, Expose, Transform } from 'class-transformer';
export class UserEntity {
id: number;
name: string;
@Exclude()
password: string; // 응답에서 자동 제거
@Exclude()
internalNote: string;
@Expose({ groups: ['admin'] })
role: string; // admin 그룹에서만 노출
@Transform(({ value }) => value.toISOString())
createdAt: Date; // 포맷 변환
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
// 컨트롤러
@Get(':id')
@SerializeOptions({ groups: ['admin'] }) // admin 그룹 활성화
findOne(@Param('id') id: string) {
return new UserEntity(this.userService.findOne(id));
}
ClassSerializerInterceptor는 NestJS 빌트인으로, @Exclude()가 붙은 필드를 응답에서 자동 제거합니다. password 같은 민감 정보가 실수로 노출되는 것을 방지하는 보안 레이어입니다.
적용 범위와 실행 순서
// 1. 글로벌
app.useGlobalInterceptors(new LoggingInterceptor());
// 2. 모듈 (DI 활용 가능)
@Module({
providers: [{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }],
})
// 3. 컨트롤러
@UseInterceptors(CacheInterceptor)
@Controller('products')
// 4. 메서드
@UseInterceptors(new TimeoutInterceptor(10000))
@Get(':id')
실행 순서: 글로벌 → 컨트롤러 → 메서드 (Pre-처리). Post-처리는 역순입니다. 여러 Interceptor가 있을 때 next.handle()의 호출 체인이 양파 껍질처럼 동작합니다. NestJS Middleware가 가장 먼저 실행되고, Guard → Interceptor → Pipe → Handler 순서로 요청이 흐릅니다.
실전 베스트 프랙티스
| 패턴 | 용도 | 적용 범위 |
|---|---|---|
| Response Transform | 일관된 API 응답 포맷 | 글로벌 |
| Logging | 요청/응답 로깅, 소요 시간 | 글로벌 |
| Serialization | 민감 필드 제거 | 글로벌 |
| Cache | 응답 캐싱 | 컨트롤러/메서드 |
| Timeout | 요청 타임아웃 제어 | 글로벌/메서드 |
| Retry | 외부 호출 재시도 | 메서드 |
정리
NestJS Interceptor는 RxJS Observable 기반으로 요청/응답 스트림을 자유롭게 제어하는 강력한 AOP 도구입니다. 응답 변환, 캐싱, 타임아웃, 직렬화 등 횡단 관심사를 핸들러 코드에서 분리하여 재사용 가능한 모듈로 만들 수 있습니다. Reflector와 커스텀 데코레이터를 결합하면 선언적이고 유연한 동작 제어가 가능합니다. 글로벌에는 로깅·변환·직렬화를, 메서드 단위로는 캐시·타임아웃을 적용하는 것이 실무 표준입니다.