[Nest JS] Interceptors

minidoo·2021년 10월 6일
0

NestJS

목록 보기
5/8
post-thumbnail

인터셉터는 Injectable() 데코레이터로 주석이 달린 클래스이며, NestInterceptor 인터페이스를 구현해야 한다. 인터셉터에는 AOP(Aspect Oriented Programming) 기술에서 영감을 받은 유용한 기능 세트가 있다.

Execution context & Call handler

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request, Response } from 'express';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request: Request = context.switchToHttp().getRequest();
    const response: Response = context.switchToHttp().getResponse();
	  
		return next.handle();
	}
}

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

첫번째는 ExcutionContext 인스턴스이다. 이 인스턴스를 통해 라우터 핸들러의 요청과 응답 세부정보를 얻을 수 있다.

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

이 접근방식은 최종 라우터 핸들러 실행 전과 후에 커스텀 로직을 추가할 수 있다. handle() 메서드는 Observable을 반환하기 때문에 강력한 rxjs 연산자를 사용하여 응답을 추가로 조작할 수 있다. AOP 용어를 사용하여 라우트 핸들러의 호출을 Pointcut이라고 한다.

Aspect Interception

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

인터셉터를 사용하여 사용자 상호작용을 기록하는 예시이다. 위의 예시는 응답주기를 방해하지 않는 tap() 연산자를 사용했다.

Response Mapping

스트림에는 라우트 핸들러에서 반환된 값이 포함되어 있으므로 rxjs의 map() 연산자를 사용하여 쉽게 변경할 수 있다.

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

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

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() 연산자를 사용하여 throw 예외를 재정의할 수 있다. Sentry를 사용하여 에러를 기록할 때 유용하다.

import { Injectable, NestInterceptor, ExecutionContext, 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(() => error)),
      );
  }
}

Stream Overriding

때로는 핸들러 호출을 완전히 방지하고 대신 다른 값을 반환해야 하는 경우가 있다. 캐시에서 응답을 반환하는 캐시 인터셉터를 알아본다.

import { Injectable, ExecutionContext, CallHandler, NestInterceptor } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { Request, Response } from 'express';
import { RedisCacheService } from 'src/redisCache/redis-cache.service';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(private cacheManager: RedisCacheService) {}
  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
 
    const request: Request = context.switchToHttp().getRequest();
    const response: Response = context.switchToHttp().getResponse();

    const cacheData = await this.cacheManager.get('abcdefg');
    if (!cacheData) {
      return next.handle();
    }

    return of(
      response.status(200).json({
        errorMessage: null,
        result: cacheData.result,
      }),
    );
  }
}

캐시데이터가 있다면 rxjs of() 연산자에 의해 생성된 새 스트림을 여기에 반환하므로 라우트 핸들러가 전혀 호출되지 않는다. 누군가 CacheInterceptor 를 사용하는 엔드포인트를 호출하면 응답이 즉시 반환된다.

Binding Interceptors

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

컨트롤러 & 메서드 범위

@UseInterceptors(LoggingInterceptor)
export class CatsController {}
export class CatsController {
  @Get()
  @UseInterceptors(LoggingInterceptor)
  async getInfo() {
    ...
  }
}

전역 범위

// main.ts

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 {}

Nest JS 공식문서

Interceptos | 네스트 JS 한국어 매뉴얼

0개의 댓글