Interceptors

이연중·2021년 8월 22일
0

NestJS

목록 보기
9/22

인터셉터는 @Injectable() 데코레이터로 주석이 달린 클래스이다.

인터셉터는 NestInterceptor 인터페이스를 구현해야 한다.

인터셉터에는 AOP 기술에서 영감을 받은 유용한 기능들이 있다.

  • 메서드 실행 전/후 추가 로직 바인딩
  • 함수에서 반환된 결과 변환
  • 함수에서 던져진 예외 반환
  • 기본 기능 동작 확장
  • 특정 조건에 따른 기능 재정의(ex: 캐싱 목적)

Basics


인터셉터는 두개의 인수를 취하는 intercept() 메서드를 구현한다.

  • ExecutinContext 인스턴스(첫번째 인수): ArgumentsHost에서 상속됨. 이를 확장하여 현재 실행 프로세스에 대한 추가 세부정보를 제공하는 몇가지 새로운 헬퍼 메서드를 추가함으로써 인터셉터를 빌드하는데 도움을 줌

  • CallHandler(두번째 인수): 인터셉터의 특정 지점에서 route handler 메서드를 호출하는데 사용할 수 있는 handle() 메서드를 구현한다. intercept() 메서드 구현에서 handle() 메서드를 호출하지 않으면 route handler 메서드가 실행되지 않음

    이를 통해 최종 route handler 실행 전과 후에 custom하게 로직을 구현할 수 있다.

    route handler 메서드 실행 전 intercept() 메서드에서 커스텀 로직을 구현하고, handle() 메서드를 호출하면, route handler 메서드가 호출되어 로직을 수행한다.

    이후 handle() 메서드는 Observerble을 반환하기 때문에 RxJS 연산자를 이용해 응답을 조작할 수 있다.

Aspect Interception


인터셉터의 첫번째 사용사례: 사용자 상호작용 기록(ex: 사용자 호출 저장)

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

Binding Interceptors


인터셉터를 바인딩하기 위해 @UseInterceptors() 데코레이터를 사용한다.

컨트롤러, 메서드, 전역 범위로 인터셉터를 바인딩할 수 있다.

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

내부 인스턴스를 전달하는 방법도 있다.

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

전역 인터셉터를 설정하기 위해서는 다음과 같이 하면된다.

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

마찬가지로 전역 인터셉터를 위와 같이 지정하게 되면, 종속성을 주입할 수 없게 되는데 이를 해결하기 위해 모든 모듈에서 직접 인터셉터를 설정하면 된다.

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

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

Response Mapping


위에서 handle()Observable을 반환한다고 했다. 그렇기에 응답 스트림에는 route handler에서 반환된 값이 포함되어 있다. 이를 RxJS의 map() 연산자를 이용해 쉽게 변경할 수 있다.

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

인터셉터는 전체 어플리케이션에서 발생하는 요구사항에 대한 재사용 가능한 솔루션을 만드는데 큰 가치를 둔다.

예를 들어, 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 ));
  }
}

Exception Mapping


RxJS의 catchError() 연산자를 활용해 던져진 예외를 재정의할 수도 있다.

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

Stream Overrding


어떤 경우에서는 handler 호출을 방지해야되는 경우도 있다.

한가지 예는 바로 응답시간 개선을 위한 캐시 구현이다.

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() 연산자에 의해 생성된 새 스트림을 반환하기에 route handler가 전혀 호출되지 않는다는 것이다.

More Operators


RxJS 연사자를 이용해 스트림을 조작할 수 있는 것으로 우리는 많은 것을 할 수 있다.

다음은 route 요청 시간 초과를 처리하는 예이다.

일정 시간 내에 route handler 메서드에서 아무것도 반환하지 않으면 오류 응답으로 종료한다.

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

참고

https://docs.nestjs.kr/interceptors

profile
Always's Archives

0개의 댓글