본문으로 건너뛰기
김신건의 로그

[NestJS] Interceptors

· 수정 · 📖 약 1분 · 436자/단어 #nestjs #interceptor #aop #cross-cutting
NestJS Interceptor, NestJS Interceptors, @UseInterceptors, CallHandler, CacheInterceptor, ClassSerializerInterceptor, LoggingInterceptor, NestJS 인터셉터

정의

NestJS Interceptor 는 요청 처리 전/후에 로직을 삽입하는 클래스입니다. AOP (Aspect-Oriented Programming) 패턴의 구현체로, 로깅, 캐싱, 응답 변환, 예외 매핑, RxJS operator 활용 등에 사용합니다.

기본 구조

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } 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 start = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`Duration: ${Date.now() - start}ms`)),
    );
  }
}
  • intercept(context, next): 요청 처리 전 로직
  • next.handle(): 실제 route handler (Observable 반환)
  • RxJS operators (tap, map, catchError, timeout) 로 파이프라인 구성

부착 방법

Method

@Get()
@UseInterceptors(LoggingInterceptor)
findAll() { ... }

Controller

@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController { ... }

Global

// main.ts
app.useGlobalInterceptors(new LoggingInterceptor());

// 또는 module (DI 지원)
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
  ],
})
export class AppModule {}

주요 사용 사례

1. Logging

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger('LoggingInterceptor');

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const {method, url} = req;
    const start = Date.now();

    this.logger.log(`${method} ${url}`);

    return next.handle().pipe(
      tap({
        next: () => {
          const duration = Date.now() - start;
          this.logger.log(`${method} ${url} ${duration}ms`);
        },
        error: (err) => {
          const duration = Date.now() - start;
          this.logger.error(`${method} ${url} ${duration}ms - ${err.message}`);
        },
      }),
    );
  }
}

2. Response Transformation

응답을 일관된 wrapper 형식으로:

interface Response<T> {
  data: T;
  timestamp: string;
  path: string;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    const req = context.switchToHttp().getRequest();
    return next.handle().pipe(
      map(data => ({
        data,
        timestamp: new Date().toISOString(),
        path: req.url,
      })),
    );
  }
}

3. Cache

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(
    @Inject(CACHE_MANAGER) private cache: Cache,
  ) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const req = context.switchToHttp().getRequest();
    const key = `${req.method}:${req.url}`;

    const cached = await this.cache.get(key);
    if (cached) return of(cached);

    return next.handle().pipe(
      tap(async (data) => {
        await this.cache.set(key, data, 60);
      }),
    );
  }
}

@nestjs/cache-manager 의 내장 CacheInterceptor 활용 관용.

4. Timeout

import { timeout, catchError, TimeoutError } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(() => new RequestTimeoutException());
        }
        return throwError(() => err);
      }),
    );
  }
}

5. Class Serialization

ClassSerializerInterceptor (내장) 는 @Exclude(), @Expose() 데코레이터 기반 응답 필터.

import { Exclude, Expose } from 'class-transformer';

export class UserEntity {
  id: number;
  email: string;

  @Exclude()
  password: string;

  @Expose({groups: ['admin']})
  role: string;

  constructor(partial: Partial<UserEntity>) {
    Object.assign(this, partial);
  }
}
@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
  @Get(':id')
  async findOne(@Param('id') id: string) {
    return new UserEntity(await this.service.findOne(id));
    // password 자동 제외
  }
}

6. Exception Mapping

@Injectable()
export class ExceptionMapInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(err => {
        if (err instanceof PrismaClientKnownRequestError) {
          if (err.code === 'P2002') {
            return throwError(() => new ConflictException('Duplicate entry'));
          }
          if (err.code === 'P2025') {
            return throwError(() => new NotFoundException('Not found'));
          }
        }
        return throwError(() => err);
      }),
    );
  }
}

Interceptor 순서

여러 개는 nested 실행:

@UseInterceptors(A, B, C)
  • 요청: A -> B -> C -> handler
  • 응답: handler -> C -> B -> A

tap 등 side effect 순서 주의.

Async Interceptor

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
  const user = await this.userService.findByToken(...);
  return next.handle();
}

Promise 도 리턴 가능. Nest 가 자동 unwrap.

Global 등록 순서

Guards -> Interceptors (before) -> Pipes -> Handler -> Interceptors (after) -> Exception Filters

Guards 가 먼저 실행. Interceptor 는 next.handle() 전후로 감쌈.

실전 조합

API 응답 통일 + 로깅 + timeout

// main.ts
app.useGlobalInterceptors(
  new LoggingInterceptor(),
  new TimeoutInterceptor(),
  new TransformInterceptor(),
);

또는 module 로:

@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
    { provide: APP_INTERCEPTOR, useClass: TimeoutInterceptor },
    { provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
  ],
})
export class AppModule {}

함정

WARNING

ClassSerializerInterceptor + Fastify. Fastify 는 기본 JSON.stringify 성능이 매우 빠른데, class-transformer 를 거치면 오히려 느려질 수 있음. 벤치마크.

CAUTION

Interceptor 안에서 응답 조작. map() 으로 새 객체 반환 필수. 원본 mutate 는 side effect.

WARNING

@Res() 사용 endpoint 에는 interceptor after 로직 안 실행. passthrough: true 로 우회 가능.

IMPORTANT

Global interceptor 에서 특정 route 우회. Reflector + custom decorator (@SkipInterceptor()) 로.

CAUTION

RxJS 학습 부담. tap, map, catchError, throwError 는 기본. 복잡한 파이프는 이해하기 어려움.

관련 위키

이 글의 용어 (10개)
[FastAPI] Middlewarefastapi
정의 FastAPI Middleware 는 모든 요청과 응답을 가로채 로깅, 인증, 압축, CORS 등 공통 처리를 하는 계층입니다. Starlette 의 ASGI middlew…
[Framework] NestJSframeworks
정의 NestJS 는 Kamil Myśliwiec 이 2017년 발표한 Node.js 서버측 프레임워크 입니다. TypeScript first, Angular 에서 영감받은 모듈…
[Koa] Middleware (Onion Model)frameworks
정의 Koa Middleware 는 요청/응답 사이에 실행되는 async 함수입니다. 각 미들웨어는 (context) 와 (다음 미들웨어를 실행하는 함수) 를 받고, 로 down…
[NestJS] Controllersframeworks
정의 NestJS Controller 는 데코레이터가 붙은 클래스로, HTTP 요청을 라우팅하고 응답을 반환합니다. Method 별로 , , , , 데코레이터를 붙여 URL + …
[NestJS] Exception Filtersframeworks
정의 NestJS Exception Filter 는 애플리케이션에서 발생한 예외를 catch 하여 클라이언트에게 응답 형식을 통일합니다. 데코레이터로 어떤 예외를 처리할지 지정합…
[NestJS] Guardsframeworks
정의 NestJS Guard 는 특정 요청이 controller method 에 도달하기 전에 실행되어 접근 허용 여부 를 결정합니다. Middleware 이후, Intercep…
[NestJS] Middlewareframeworks
정의 NestJS Middleware 는 route handler 실행 이전, Guard 도 실행되기 전에 실행되는 함수/클래스입니다. Express/Fastify 표준 midd…
[NestJS] Modulesframeworks
정의 NestJS Module 은 데코레이터가 붙은 클래스로, 관련된 controller, provider, import/export 를 하나로 묶는 조직 단위입니다. Angul…
[NestJS] Pipes (Validation, Transformation)frameworks
정의 NestJS Pipe 는 controller method 로 들어가는 인자를 변환 (transformation) 하거나 검증 (validation) 하는 클래스입니다. Gu…
[NestJS] Providers & Dependency Injectionframeworks
정의 NestJS Provider 는 데코레이터가 붙은 클래스 (또는 값/factory) 로, DI container 에 등록되어 다른 클래스에 주입됩니다. Service, Re…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기