프레임워크/Nest.js

Nest.js 인터셉터 (Interceptor) 란?

Jake Seo 2024. 2. 7. 23:10

인터셉터

  • Nest.js 의 인터셉터 클래스는
    • @Injectable 데코레이터를 추가해야 한다.
    • NestInterceptor 인터페이스를 상속해야 한다.

picture 0

인터셉터는 Aspect Oriented Programming 기술에서 영감을 받았다.

  • 메서드 실행 전 후에 추가 로직 바인딩
  • 함수 결과를 변환
  • 함수 예외를 변환
  • 함수 동작을 확장
  • 조건부 함수 완전 재정의 (ex. 캐싱 목적)

기초

  • 인터셉터는 intercept() 메서드를 구현해야 한다.
  • 메서드엔 두개의 인자가 있다.
    • 1번째 인자, ExecutionContext: 요청의 타입 정보나 호출된 컨트롤러 메서드 정보 등을 가져올 수 있다.
      • ex) http 에서 헤더, 쿠키 등의 정보 그리고 컨트롤러 메서드 정보
      • 이는 ArgumentsHost 를 상속한다.
    • 2번째 인자, Call handler: handle() 메서드를 이용해 본래 호출된 메서드를 매핑할 때 사용한다.
      • Observable 을 반환하여 RxJS 연산자로 응답을 추가 조작할 수 있다.
      • 객체지향에서는 이 handle() 메서드를 포인트컷이라고 한다.

예제1: 로깅

인터셉터 작성

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> {
    console.log("Before...");

    const now = Date.now();
    return next
      .handle()
      .pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
  }
}

컨트롤러에 적용

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

결과

Before...
After... 1ms

글로벌 적용

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
// app.module.ts
import { Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";

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

예제2: 응답 매핑 (변환)

데이터를 객체 안에 넣기

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

export interface Response<T> {
  data: T;
}

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, Response<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable<Response<T>> {
    return next.handle().pipe(map((data) => ({ data })));
  }
}
{
  "data": []
}

null 인 경우 공백 출력하기

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(map((value) => (value === null ? "" : value)));
  }
}

예제3: 예외 재정의

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  BadGatewayException,
  CallHandler,
} from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(catchError((err) => throwError(() => new BadGatewayException())));
  }
}

예제 4: 스트림 오버라이딩

  • 호출하는 메서드를 보고 응답이 이미 연산된 결과면 캐시된 결과를 내보낼 수 있음
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from "@nestjs/common";
import { Observable, of } from "rxjs";

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const isCached = true;
    if (isCached) {
      return of([]);
    }
    return next.handle();
  }
}

예제 5: 타임아웃 구성하기

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  RequestTimeoutException,
} from "@nestjs/common";
import { Observable, throwError, TimeoutError } from "rxjs";
import { catchError, timeout } from "rxjs/operators";

@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);
      })
    );
  }
}
반응형