BackEnd/Nest.js

[Nest.js] 인터셉터

Grace 2023. 5. 31. 11:06

인터셉터

인터셉터는 요청과 응답을 가로채서 변형을 가할 수 있는 컴포넌트입니다.

인터셉터는 관점 지향 프로그래밍에서 영향을 많이 받았습니다. 인터셉터를 이용하면 다음과 같은 기능을 수행할 수 있습니다.

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

인터셉터는 미들웨어와 수행하는 일이 비슷하지만, 수행 시점에 차이가 있습니다. 미들웨어는 요청이 라우트 핸들러로 전달되기 전에 동작하며, 인터셉터는 요청에 대한 여러 개의 미들웨어를 조합하여 각기 다른 목적을 가진 미들웨어 로직을 수행할 수 있습니다. 어떤 미들웨어가 다음 미들웨어에 제어권을 넘기지 않고 요청/응답 주기를 끝내는 일도 가능합니다.

아래는 라우트 핸들러가 요청을 처리하기 전후에 어떤 로그를 남기고 싶을 경우를 구현한 인터셉터입니다.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
// 인터셉터는 @nestjs/common 패키지에서 제공하는 NestInterceptor 인터페이스를 구현한 클래스
export class LoggingInterceptor implements NestInterceptor { 
  // NestInterceptor 인터페이스의 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`)), // 요청 처리 후 로그 출력
      )
  }
}

인터셉터를 적용할 때 특정 컨트롤러나 메서드에 적용하고 싶다면 @UseInterceptors()를 이용하면 됩니다. 전역으로 구현하는 경우 아래와 같이 작성합니다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  ...
  app.useGlobalInterceptors(new LoggingInterceptor());
  await app.listen(3000);
}
bootstarp();
export interface NestInterceptor<T = any, R = any> {
  intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> |
  Promise<Observable<R>>;
}

export interface CallHandler<T = any> {
  handle(): Observable<T>;
}

구현해야 하는 intercept에 전달되는 인수가 2개 있습니다. 두번째 인수인 CallHandlerhandle() 메서드를 구현해야 합니다. handle() 메서드는 라우트 핸들러에 전달된 응답 스트림을 돌려주고 RxJSObservable로 구현되어 있습니다. 만약 인터셉터에서 핸들러가 제공하는 handle() 메서드를 호출하지 않으면 라우터 핸들러가 동작하지 않습니다. handle()을 호출하고 Observable을 수신한 후에 응답 스트림에 추가 작업을 수행할 수 있는 것입니다. 응답을 다루는 방법은 RxJS에서 제공하는 여러 가지 메서드로 구현이 가능합니다.

응답과 예외 매핑

인터셉터를 통해 응답과 발생한 예외를 잡아 변형을 가할 수 있습니다. 아래는 라우터 핸들러에서 전달한 응답을 객체로 감싸서 전달하도록하는 TransformInterceptor를 구현한 코드입니다.

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } 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})))
  }
}

TransformInterceptorGeneric으로 타입 T를 선언하고 있습니다. NestInterceptor 인터페이스의 정의를 보면 Generic으로 T, R 타입 2개를 선언하도록 되어 있습니다. 사실 둘 다 기본이 any 타입이기 때문에 어떤 타입이 와도 상관없습니다. T는 응답 스트림을 지원하는 Observable 타입이어야 하고, R은 응답의 값을 Observable로 감싼 타입을 정해줘야 합니다. Tany 타입이 될 것이고, RResponse를 지정했습니다. Response는 우리의 요구 사항에 맞게 정의한 타입, 즉 data 속성을 가지는 객체가 되도록 강제합니다.

이제 useGlobalInterceptors에 콤마로 인터셉터 객체를 추가하여 전역으로 사용합니다.

...
import { TransformInterceptor } from './transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(
    new LoggingInterceptor(),
    new TransformInterceptor()
  )
  await app.listen(3000)
}
bootstrap()

아래는 라우트 핸들링 도중 던져진 예외를 잡아서 변환하는 코드입니다. 발생한 모든 에러를 잡아서 502 Bad Gateway로 변경하지만 이것은 사실 예외 필터에서 다루는 것이 좋으니 예 정도로만 알고 있는게 좋습니다.

import { Injectable, NestInterceptor, ExecutionContext, BadGatewayExeption, CallHandler}
from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(
        catchError(err => throwError(()=> new BadGatewayException()))
      )
  }
}

만들어진 intercepter를 라우트 핸들러 GET /users/:id 엔드포인트에만 적용합니다.

@UseInterceptors(ErrorsInterceptor)
@Get(':id')
findOne(@Param('id') id: string) {
  throw new InternalServerErrorException()
}

'BackEnd > Nest.js' 카테고리의 다른 글

[Nest.js] 헬스 체크  (0) 2023.06.01
[Nest.js] 태스크 스케줄링  (0) 2023.05.31
[Nest.js] 예외 필터  (0) 2023.05.30
[Nest.js] 로깅  (0) 2023.05.30
[Nest.js] JWT 인증/인가  (0) 2023.05.26