[NestJS] Interceptors

nakkim·2022년 7월 19일
0

NestJS docs

목록 보기
8/10

https://docs.nestjs.com/interceptors

Interceptors

interceptor는 @Injectable() 데코레이터를 사용하고, NestInterceptor 인터페이스를 구현한다.

Aspect Oriented Programming(AOP)를 적용 → 핵심 기능에서 부가적인 기능을 독립적인 애스펙트로 빼냄

따라서,

  • 메서드 실행 전후에 추가 로직 수행 가능
  • 함수가 반환한 결과 변경
  • 함수가 던진 예외 변경
  • 함수의 동작 확장
  • 필요한 경우, 함수를 오버라이드함

이런 것들을 가능하게 한다.


Basics

인터셉터는 intercept() 메서드를 구현한다. 이 메서드는 요청/응답 스트림을 잘 포장한다. 그 결과로, 최종 라우트 핸들러 실행 전후에 커스텀 로직 추가 가능

매개변수

  • ExecutionContext 인스턴스 (Guards에서 본 그것)
  • CallHandler

Execution context

ArgumentsHost를 extend하면서 여러 헬퍼 메서드(현재 실행 프로세스에 관한 추가 정보 제공)를 추가

→ 더욱 일반적인(컨트롤러, 메서드, 실행 컨텍스트의 집합을 가로지르는ㅋㅋ) 인터셉트 구현에 도움이 됨

Call handler

CallHandler 인터페이스는 handle() 메서드를 구현한다.

  • handle(): 인터셉터 내에서 라우트 핸들러 메서드를 호출
    • intercept() 메서드의 구현에서 handle()을 호출해야 라우트 핸들러 메서드 사용 가능
  • handle() 메서드는 Observable을 리턴 → RxJS 연산자를 이용하여 응답 조작 가능 (RxJS랑 옵저버블이 뭐임?)

AOP에서는, 라우트 핸들러 호출(using handle())은 Pointcut 이라고 부른다. (추가적인 로직이 들어오는 곳)

POST /cats 요청이 들어왔다고 치자.

  1. 이 요청은 CatsController의 create()로 갈 거임. 만약 handle()을 호출하지 않은 인터셉트라면, create() 메서드는 실행되지 않는다.
  2. handle()이 호출되고 Observable이 리턴되면, create()가 트리거됨. 그리고 Observable을 통해 응답 스트림을 받아서 추가 작업을 수행할 수 있으며 최종값은 호출자에게 리턴된다.

Aspect interception

유저 상호작용을 로깅하기 위해 인터셉트를 사용하는 예시를 보자.

간단하게 LogginInterceptor를 만들어 봅시다.

nest g in logging

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> {
    return next.handle();
  }
}

NestInterceptor<T, R>은 제네릭 인터페이스
T: Observable<T>의 타입
R: type of the value wrapped by Observable<R>
?? 무슨말이지

다른 것들과 마찬가지로 constructor를 통한 DI 가능

intercept() 메서드를 아래와 같이 수정

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

아마

Before… 찍힘 → 라우트 핸들러 실행(by handle()) → After… 걸린시간ms 찍힘

인듯..하다

handle()이 RxJS Observable을 리턴하기 때문에, 스트림 조작에 사용할 수 있는 오퍼레이터의 종류가 다양하다.

위의 코드에서 tap() 오퍼레이터를 사용했음. 이건 observable 스트림이 정상/예외 종료 시 콘솔로그(After 머시기)를 호출하지만, 그렇지 않으면 응답 사이클에 간섭하지 않는다.

(tap()은 RxJS에 있음 - npm i rxjs)


Binding interceptors

인터셉터 설정은 @UseInterceptors() 데코레이터를 사용한다.

다른 것들 처럼.. controller-scoped, method-scoped, global-scoped 가능

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

마찬가지로.. 타입을 넘겨줌으로써 프레임워크에 DI 책임 전가함. 인스턴스 넘겨줘도 됨

글로벌로 설정하기 위해서는 어플리케이션 인스턴스의 useGlobalInterceptors() 메서드를 사용한다.

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

하지만 이젠.. 눈치 챌 수 있음.. 이렇게 하면 DI가 안된다~ 왜? 모듈 밖이라서

그렇다면 해결 방법은 역시나 모듈에서 설정하는 것이겠죠?

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

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

useClass is not the only way of dealing with custom provider registration. Learn more here.


Response mapping

자 handle()은 Observable을 리턴한다. 스트림은 라우트 핸들러가 리턴한 값을 가지고 있다. 따라서, RxJS의 map() 연산자를 이용하면 응답을 변경할 수 있다.

주의!
응답 매핑 기능은 특정 라이브러리의 응답과 같이 사용할 수 없음
@Res() 객체를 직접 사용하는 것은 금지된다.

TransformInterceptor를 만듭시다.

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

map() 연산자를 보면 라우트 핸들러의 응답을 data로 받아서 { 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 ));
  }
}

짠 이 인터셉트를 전역으로 등록하면 자동으로 null을 빈 문자열로 바꿔준다.


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 overriding

핸들러 호출을 막고 다른 값을 리턴해야 하는 경우가 있다.

→ 응답 시간 향상을 위해 캐시를 구현

현실적으로는, TTL, 캐시 무효화, 캐시 사이즈 등을 고려해야 하지만.. 일단은 간단한 구조만 봅시다.

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

of()는 새로운 스트림을 리턴(라우터 핸들러는 호출되지 않음)

캐시인터셉터를 사용하는 엔드포인트에 접근하면, 응답이 바로 반환된다.

일반적인 솔루션(?)을 만드는 대신, Reflector와 커스텀 데코레이터를 이용할 수 있다.


More operators

다양하게 있지만.. 마지막으로 timeout 설정 예시를 봅시다.

주어진 시간 내에 리턴이 없으면 예외를 던진다.

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);
      }),
    );
  };
};
profile
nakkim.hashnode.dev로 이사합니다

0개의 댓글