Interceptors

Interceptors

인터셉터는 @Injectable()데코레이터로 주석이 달린 클래스입니다. 인터셉터는 NestInterceptor 인터페이스를 구현해야 합니다.

인터셉터에는 Aspect Oriented Programming (AOP) 기술에서 영감을 얻은 유용한 기능 세트가 있습니다. 그들은 다음을 가능하게 합니다.

  • 메소드 실행 전/후에 추가 로직 바인딩

  • 함수에서 반환 된 결과를 변환

  • 함수에서 발생 된 예외를 변환

  • 기본 기능 확장

  • 특정 조건에 따라 기능을 완전히 재정의 (예 : 캐싱 목적)

Basics

각 인터셉터는 두 개의 인수를받는 intercept()메소드를 구현합니다. 첫 번째는 ExecutionContext인스턴스입니다 (guards와 정확히 같은 객체). ExecutionContextArgumentsHost에서 상속받습니다. 예외 필터 챕터에서 앞서 ArgumentsHost를 보았습니다. 여기서 우리는 원래 핸들러로 전달된 인수를 감싸는 래퍼이며 응용 프로그램의 유형에 따라 다른 인수 배열을 포함한다는 것을 알았습니다. 이 주제에 대한 자세한 내용은 예외 필터를 다시 참조하십시오.

Execution context

ExecutionContextArgumentsHost를 확장하여 현재 실행 프로세스에 대한 추가 정보를 제공합니다. 그 모습은 다음과 같습니다.

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

getHandler()메소드는 호출될 경로 핸들러에 대한 참조를 리턴합니다. getClass()메소드는 이 특정 핸들러가 속하는 Controller 클래스의 유형을 리턴합니다. 예를 들어, 현재 처리 된 요청이 CatsControllercreate()메소드로 예정된 POST 요청인 경우, getHandler()create() 메소드에 대한 참조를 리턴하고 getClass()CatsController type (인스턴스 아님)을 반환합니다.

Call handler

두 번째 인수는 CallHandler입니다. CallHandler 인터페이스는 handle()메소드를 구현하는데, 인터셉터의 어느 시점에서 경로 핸들러 메소드를 호출하는 데 사용할 수 있습니다. intercept()메소드 구현에서 handle()메소드를 호출하지 않으면 라우트 핸들러 메소드가 전혀 실행되지 않습니다.

이 접근법은 intercept()메소드가 효과적으로 요청/응답 스트림을 랩핑한다는 것을 의미합니다. 결과적으로 최종 라우트 핸들러 실행 전후에 커스텀 로직을 구현할 수 있습니다. handle()을 호출하기 에 실행되는 intercept() 메소드에 코드를 작성할 수 있다는 것이 분명하지만, 나중에 어떻게 되는지에 어떤 영향을 미칩니까? handle()메소드는 Observable을 리턴하므로, 강력한 RxJS 연산자를 사용하여 응답을 추가로 조작 할 수 있습니다. Aspect Oriented Programming 용어를 사용하여 라우트 핸들러의 호출 (즉, handle()호출)을 Pointcut이라고합니다. 추가 로직이 삽입됩니다.

예를 들어, 들어오는POST/cats 요청을 고려하십시오. 이 요청은 CatsController 안에 정의된 create()핸들러를 대상으로합니다. handle()메소드를 호출하지 않는 인터셉터가 도중에 호출되면 create()메소드가 실행되지 않습니다. handle()이 호출되고 (그리고 그것의 Observable이 리턴되면) create()핸들러가 트리거됩니다. 응답 스트림이 Observable을 통해 수신되면 추가 작업을 스트림에서 수행할 수 있으며 최종 결과는 호출자에게 반환됩니다.

Aspect interception

우리가 살펴볼 첫번째 사용 사례는 인터셉터를 사용하여 사용자 상호 작용을 기록하는 것입니다 (예: 사용자 호출 저장, 비동기 적으로 이벤트 디스패치 또는 타임 스탬프 계산). 우리는 아래에 간단한 LoggingInterceptor를 보여줍니다:

@@filename(logging.interceptor)
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`)),
      );
  }
}
@@switch
import { Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor {
  intercept(context, next) {
    console.log('Before...');

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

info 힌트 NestInterceptor<T,R>TObservable<T>(응답 스트림을 지원)의 유형을 나타내고 RObservable<R>에 의해 랩핑된 값의 유형인 일반 인터페이스입니다.

warning 알림 컨트롤러, 프로 바이더, 가드 등과 같은 인터셉터는생성자를 통해 종속성을 주입 할 수 있습니다.

handle()은 RxJS Observable을 반환하므로 스트림을 조작하는 데 사용할 수 있는 연산자를 다양하게 선택할 수 있습니다. 위의 예제에서 우리는 tap()연산자를 사용했는데, 이는 관찰 가능 스트림이 정상적으로 종료되거나 예외적으로 종료될 때 익명 로깅 기능을 호출하지만 응답주기를 방해하지는 않습니다.

Binding interceptors

인터셉터를 설정하기 위해 @nestjs/common 패키지에서 가져온 @UseInterceptors()데코레이터를 사용합니다. pipesguards와 같이 인터셉터는 컨트롤러 범위, 방법 범위 또는 전역 범위 일 수 있습니다.

@@filename(cats.controller)
@UseInterceptors(LoggingInterceptor)
export class CatsController {}

info 힌트 @UseInterceptors()데코레이터는 @nestjs/common 패키지에서 가져옵니다.

위의 구성을 사용하여 CatsController에 정의된 각 경로 핸들러는 LoggingInterceptor를 사용합니다. 누군가 GET /cats 엔드 포인트를 호출하면 표준 출력에 다음 출력이 표시됩니다.

Before...
After... 1ms

인스턴스 대신 LoggingInterceptor 유형을 전달하여 프레임 워크의 인스턴스화와 종속성 주입을 가능하게 합니다. 파이프, 가드 및 예외 필터와 마찬가지로 내부 인스턴스도 전달할 수 있습니다.

@@filename(cats.controller)
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}

언급한 바와 같이, 위의 구성은 인터셉터를이 컨트롤러가 선언한 모든 핸들러에 연결합니다. 인터셉터의 범위를 단일 방법으로 제한하려면 메소드 수준에서 데코레이터를 적용하면됩니다.

전역 인터셉터를 설정하기 위해 Nest 애플리케이션 인스턴스의 useGlobalInterceptors()메소드를 사용합니다.

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());

전역 인터셉터는 모든 컨트롤러와 모든 경로 핸들러에 대해 전체 애플리케이션에서 사용됩니다. 의존성 주입의 관점에서, 모듈 외부에서 등록된 전역 인터셉터 (위의 예에서와 같이 useGlobalInterceptors()로)는 의존성이 주입될 수 없습니다. 이는 모듈의 컨텍스트 외부에서 수행되기 때문입니다. 이 문제를 해결하기 위해 다음 구성을 사용하여 모든 모듈에서 직접 인터셉터를 설정할 수 있습니다.

@@filename(app.module)
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

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

info 힌트 인터셉터에 대한 의존성 주입을 수행하기 위해이 접근법을 사용할 때, 이 구성이 사용되는 모듈에 관계없이, 인터셉터는 실제로는 전역적입니다. 어디에서 해야 합니까? 인터셉터 (위 예에서 LoggingInterceptor)가 정의된 모듈을 선택하십시오. 또한 커스텀 프로 바이더 등록을 다루는 유일한 방법은 useClass가 아닙니다. 여기에 대해 자세히 알아보십시오.

Response mapping

우리는 handle()Observable을 반환한다는 것을 이미 알고 있습니다. 이 스트림에는 경로 처리기의 반환 값이 포함되어 있으므로 RxJS의 map()연산자를 사용하여 쉽게 변경할 수 있습니다.

warning 경고 응답 매핑 기능은 라이브러리 별 응답 전략에서 작동하지 않습니다 (@Res()객체를 직접 사용하는 것은 금지됨).

프로세스를 시연하기 위해 사소한 방식으로 각 응답을 수정하는 TransformInterceptor를 만들어 봅시다. RxJS의 map()연산자를 사용하여 응답 객체를 새로 생성된 객체의 data 속성에 할당하여 새 객체를 클라이언트에 반환합니다.

@@filename(transform.interceptor)
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 })));
  }
}
@@switch
import { Injectable } from '@nestjs/common';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor {
  intercept(context, next) {
    return next.handle().pipe(map(data => ({ data })));
  }
}

info 힌트 Nest 인터셉터는 동기 및 비동기 intercept()메소드와 함께 작동합니다. 필요한 경우 단순히 메소드를 비동기로 전환 할 수 있습니다.

위의 구성에서 누군가가 GET /cats 엔드 포인트를 호출하면 응답은 다음과 같습니다 (라우트 핸들러가 빈 배열[]을 리턴한다고 가정).

{
  "data": []
}

인터셉터는 전체 애플리케이션에서 발생하는 요구 사항에 대한 재사용 가능한 솔루션을 작성하는 데 큰 가치가 있습니다. 예를 들어, 각각의 null 값을 빈 문자열 ''로 변환해야 한다고 상상해보십시오. 한 줄의 코드를 사용하여 인터셉터를 전역적으로 바인딩하여 등록된 각 핸들러에서 자동으로 사용할 수 있습니다.

@@filename()
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 ));
  }
}
@@switch
import { Injectable } from '@nestjs/common';
import { map } from 'rxjs/operators';

@Injectable()
export class ExcludeNullInterceptor {
  intercept(context, next) {
    return next
      .handle()
      .pipe(map(value => value === null ? '' : value ));
  }
}

Exception mapping

또 다른 흥미로운 사용 사례는 RxJS 의catchError()연산자를 사용하여 발생된 예외를 무시하는 것입니다.

@@filename(errors.interceptor)
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())),
      );
  }
}
@@switch
import { Injectable, BadGatewayException } from '@nestjs/common';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorsInterceptor {
  intercept(context, next) {
    return next
      .handle()
      .pipe(
        catchError(err => throwError(new BadGatewayException())),
      );
  }
}

Stream overriding

핸들러 호출을 완전히 막고 대신 다른 값을 반환하려는 몇가지 이유가 있습니다. 명백한 예는 응답 시간을 개선하기 위해 캐시를 구현하는 것입니다. 캐시에서 응답을 반환하는 간단한 캐시 인터셉터를 살펴 보겠습니다. 실제 예에서 우리는 TTL, 캐시 무효화, 캐시 크기 등과 같은 다른 요소를 고려하고 싶지만 이 논의의 범위를 벗어납니다. 여기에서는 기본 개념을 보여주는 기본 예제를 제공합니다.

@@filename(cache.interceptor)
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();
  }
}
@@switch
import { Injectable } from '@nestjs/common';
import { of } from 'rxjs';

@Injectable()
export class CacheInterceptor {
  intercept(context, next) {
    const isCached = true;
    if (isCached) {
      return of([]);
    }
    return next.handle();
  }
}

우리의 CacheInterceptor는 하드 코딩된 isCached 변수와 하드 코딩된 응답 []을 가지고 있습니다. 여기서 주목할 점은 RxJS of()연산자에 의해 생성된 새로운 스트림을 반환하므로 경로 핸들러는 전혀 호출되지 않습니다. 누군가 CacheInterceptor를 사용하는 엔드 포인트를 호출하면 응답 (하드 코딩된 빈 배열)이 즉시 리턴됩니다. 일반적인 솔루션을 만들기 위해 Reflector를 활용하고 사용자 정의 데코레이터를 만들 수 있습니다. Reflectorguards 챕터에 잘 설명되어 있습니다.

More operators

RxJS 연산자를 사용하여 스트림을 조작할 수 있으므로 많은 기능이 제공됩니다. 다른 일반적인 사용 사례를 고려해 봅시다. 경로 요청에서 시간 초과를 처리한다고 가정해 보십시오. 일정 시간 후에 엔드 포인트가 아무것도 리턴하지 않으면 오류 응답으로 종료하려고합니다. 다음과 같은 구성으로 가능합니다.

@@filename(timeout.interceptor)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(timeout(5000))
  }
}
@@switch
import { Injectable } from '@nestjs/common';
import { timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor {
  intercept(context, next) {
    return next.handle().pipe(timeout(5000))
  }
}

5 초 후에 요청 처리가 취소됩니다.

Last updated