9. Interceptors

인터셉터는  @Injectable() 데코레이터로 주석이 달린 클래스이며 NestInterceptor 인터페이스를 구현한다.

인터셉터에는 AOP( Aspect Oriented 프로그래밍 ) 기술 에서 영감을 받은 유용한 기능 세트가 있습니다 . 이를 통해 다음이 가능해진다.

  • 메소드 실행 전/후에 추가 로직 바인딩
  • 함수에서 반환된 결과를 변환.
  • 함수에서 발생한 예외를 변환.
  • 기본 기능 동작 확장
  • 특정 조건(예: 캐싱 목적)에 따라 기능을 완전히 재정의.

 

기초

각 인터셉터는 두 개의 인수를 사용하는 intercept() 메서드를 구현한다. 첫 번째는ExecutionContext  인스턴스다 ( 가드와 정확히 동일한 개체 ). ExecutionContext ArgumentsHost 에서 상속된다. 예외 필터 장에서 보았었다. 원래 핸들러에 전달된 인수를 둘러싼 래퍼이고 애플리케이션 유형에 따라 다양한 인수 배열을 포함한다는 것을 확인했다.

 

실행 컨텍스트

ArgumentsHost를 확장 하면 ExecutionContext 에는 현재 실행 프로세스에 대한 추가 세부 정보를 제공하는 몇 가지 새로운 헬퍼 메서드도 추가된다. 이러한 세부 정보는 광범위한 컨트롤러, 메서드 및 실행 컨텍스트 집합에서 작동할 수 있는 보다 일반적인 인터셉터를 구축하는 데 도움이 될 수 있다.

 

호출 핸들러

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

 

이 접근 방식은 해당  intercept() 메서드가 요청/응답 스트림을효과적으로 래핑한다는 것을 의미합니다. 결과적으로 최종 라우트 핸들러 실행 전후에 사용자 지정 로직을 구현할 수 있다. handle() 를 호출하기 전에 실행되는 코드를 intercept()  메서드에 작성할 수 있다는 점은 분명하다 . 하지만 이후에 발생하는 상황에 어떤 영향을 미칠까? handle() 메서드가 Observablehandle() 를 반환 하므로 강력한 RxJS 연산자를 사용하여 응답을 추가로 조작 할 수 있다 . 관점 지향 프로그래밍 용어에서 라우트 핸들러의 호출(예: handle()호출 )을 Pointcut 이라고 부르는데 , 이는 추가 논리가 삽입되는 지점임을 나타낸다.

 

예를 들어 들어오는 POST /cats요청을 생각해 보자. 이 요청은 CatsController 의  create() 핸들러로 향한다. handle()  메소드를 호출하지 않는 인터셉터가 도중에 호출되면 create() 메소드가 실행되지 않는다.  handle() 이 호출되고 Observable을 반환되면 create() 핸들러가 트리거된다. 그리고 Observable 을 통해 응답 스트림이 수신되면 스트림에 대해 추가 작업을 수행할 수 있으며 최종 결과가 호출자에게 반환된다.

 

관점(Aspect) 인터셉터

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

// logging.interceptor.ts

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`)),
      );
  }
}

힌트) NestInterceptor<T, R> 는 제네릭 인터페이스 이며 T는 응답 스트림을 지원하는 Observable <T> 이고, R 은 래핑된 Observable<R> 유형이다.

 

힌트) 컨트롤러, 프로바이더, 가드 등의 인터셉터는 생성자를 통해 주입된다.

 

바인딩 인터셉터

인터셉터를 설정하기 위해 @nestjs/common 패키지에서 가져온  @UseInterceptors() 데코레이터를 사용한다. 파이프  가드 와 마찬가지로 인터셉터는 컨트롤러 범위, 메서드 범위 또는 전역 범위일 수 있다.

// cats.controller.ts

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

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

Before...
After... 1ms

LoggingInterceptor  인스턴스 대신 유형을 전달하여 인스턴스화에 대한 책임을 프레임워크에 맡기고 종속성 주입을 활성화했다는 점에 유의하자. 파이프, 가드 및 예외 필터와 마찬가지로 내부 인스턴스를 전달할 수도 있다.

// cats.controller.ts

@UseInterceptors(new LoggingInterceptor())
export class CatsController {}

언급한 대로 위의 구성은 이 컨트롤러가 선언한 모든 핸들러에 인터셉터를 연결한다. 인터셉터의 범위를 단일 메서드로 제한하려면 간단히 메서드 수준 에서 데코레이터를 적용하면 된다 .

 

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

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

전역 인터셉터는 모든 컨트롤러와 모든 라우터 핸들러를 위해 전체 응용 프로그램에서 사용된다. 종속성 주입 측면에서, 모듈 외부에서 등록된 전역 인터셉터( 위 예에서  useGlobalInterceptors() 같이 )는 종속성을 주입할 수 없다. 이는 모듈의 컨텍스트 외부에서 수행되기 때문이다. 이 문제를 해결하려면 다음 구성을 사용하여 모든 모듈에서 직접 인터셉터를 설정할 수 있다.

// app.module.ts

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

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

힌트) 이 접근 방식을 사용하여 인터셉터에 대한 종속성 주입을 수행하는 경우 이 구성이 사용되는 모듈에 관계없이 인터셉터는 실제로 전역적이라는 점에 유의하자. 이 작업은 어디서 수행될까? 인터셉터( 위 예에서 LoggingInterceptor )가 정의된 모듈을 선택된다. 또한 useClass 가 사용자 지정 공급자 등록을 처리하는 유일한 방법은 아닙니다. 여기에서 자세히 알수 있다.

 

응답 매핑

우리는 Observable 를 리턴하는 handle()에 대해 배웠다. 스트림에는 라우트 핸들러에서 반환된 값이 포함되어 있으므로 RxJS map() 연산자를 사용하여 쉽게 변경할 수 있다.

 

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

 

TransformInterceptor 프로세스를 보여주기 위해 간단한 방법으로  각 응답을 수정하는 예를 보겠다. RxJS의  map() 연산자를 사용하여 새로 생성된 객체의 속성에 응답 객체를 할당하고 새 객체를 클라이언트에 반환한다.

// trasform.interceptor.ts

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 })));
  }
}

힌트) Nest 인터셉터는 동기식 및 비동기식 방법으로 작동합니다. 필요한 경우 간단히 intercept() 를 async 로 전환할 수 있다.

 

위 구성을 사용하면 누군가 GET /cats엔드포인트를 호출하면 응답은 다음과 같다(라우트 핸들러가 빈 배열  [] 을 반환한다고 가정).

{
  "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 ));
  }
}

 

예외 매핑

또 다른 흥미로운 사용 사례는 RxJS catchError() 연산자를 활용하여 발생한 예외를 재정의하는 것이다.

// erros.interceptor.ts

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())),
      );
  }
}

 

스트림 재정의 

때때로 핸들러 호출을 완전히 방지하고 대신 다른 값을 반환하려는 데에는 몇 가지 이유가 있다. 확실한 예는 응답 시간을 향상시키기 위해 캐시를 구현하는 것이다. 캐시에서 응답을 반환하는 간단한 캐시 인터셉터를 살펴보겠다 . 현실적인 예에서는 TTL, 캐시 무효화, 캐시 크기 등과 같은 다른 요소를 고려하고 싶지만 이는 이 논의 범위를 벗어나기 때문에 여기서는 주요 개념을 보여주는 기본 예만 살펴보자.

// cache.interceptor.ts

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();
  }
}

 

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

 

더 많은 연산자

RxJS 연산자를 사용하여 스트림을 조작할 수 있는 가능성은 우리에게 많은 기능을 제공한다. 또 다른 일반적인 사용 사례를 고려해 보자. 경로 요청에 대한 시간 초과를 처리하고 싶다고 가정해 보면, 엔드포인트가 일정 기간 후에도 아무것도 반환하지 않으면 오류 응답으로 종료하려고 한다. 다음 구성을 보자.

// timeout.interceptor.ts

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);
      }),
    );
  };
};

5초 후에 요청 처리가 취소된다. RequestTimeoutException던지기 전에 사용자 로직 (예: Release resources) 를 추가할 수도 있다 .

'Backend(Framework) > NestJS 개요(공식문서 번역)' 카테고리의 다른 글

11. Custom provider  (1) 2023.11.19
10. Custom decorators  (0) 2023.11.19
8. Guards  (0) 2023.11.19
7. Pipes  (0) 2023.11.19
6. Exception filters  (0) 2023.11.17
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유