처음부터 차근차근

[NestJS] interceptor 본문

FrameWork/NestJS

[NestJS] interceptor

HangJu_95 2023. 11. 27. 23:42
728x90

Interceptor란?

  • 의미 : 가로채는 사람, 가로채는 것을 의미합니다.

interceptor는 컨트롤러 전, 후에서 다양한 역할을 해줍니다.

공식문서에 나와있는 예시로는

  • 메서드(컨트롤러) 실행 전, 후의 추가적인 로직 수행
  • 함수에서 반환된 결과 및 예외에 대한 변환(ex. Logger)
  • 기본 함수 동작의 확장
  • 특정 조건 하에서 완전한 함수의 재정의(ex. 캐싱 목적)

이렇게 있습니다. Interceptor는 AOP(관점 지향 프로그래밍 기법) 기술에서 영감을 받아 만들어졌습니다.

모든 interceptor에는 intercept() 메서드가 들어가며 두가지 중요한 인자가 들어가있습니다.

Interceptor의 인자

1. Execution Context

Execution Context(실행컨텍스트)는 Guard와 마찬가지로 ArgumentsHost를 상속받습니다. Guard와 마찬가지로 Request와 Response 객체를 받기 위해 사용됩니다.

2. Call handler

CallHandler interface에는 handle() 메서드가 있으며, 이는 interceptor 내에서 route handler의 메서드를 호출하는데 사용할 수 있습니다. 만약 handle() 메서드를 intercept() 함수 내에서 호출하지 않는다면 route handler 메서드는 실행될 수 없습니다.

 

이 handler() 메서드 덕분에 intercept() 메서드는 효과적으로 요청과 응답 모두 Custom logic을 포함할 수 있게됩니다.

handle() 메서드는 Observable을 리턴하기 때문에, Response를 더 변형할 수 있는 RxJS를 사용하여 응답을 변형할 수 있게됩니다.

(RxJS는 추후에 다룰 예정입니다.) AOP에서 route handler를 호출하는 것을 Pointcut이라고 합니다.

 

즉, 우리가 intercept를 통해 메서드 실행 후의 추가적인 로직을 수행하기 위해서는 handle() 메서드가 필수적으로 들어가야 하며, 이는 route handler를 호출하고 처리를 계속 진행하게 하는 매서드입니다.

Aspect interception

NestJS의 간단한 예시를 보겠습니다.

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' 연산자를 통해 라우터 핸들러 이후 로직을 실행합니다.
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

Logging 인터셉터를 제작하였습니다.

handle 메서드를 통해 다음 핸들러의 처리를 계속 진행시키고, pipe 처리가 끝난 후 해당하는 로직을 수행시켰습니다.

tap은 Response에 아무런 영향을 미치지 않지만, exception 혹은 Logger를 사용할 때 적합합니다.

Binding interceptors

이제 interceptor를 연결시켜보겠습니다.

pipe나 guard와 동일하게 적용시킬 수 있으므로 간단하게 보고 넘어가겠습니다.

@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

이번에는 Response를 변형해볼 예정입니다.

RxJS는 응답을 변형시킬 수 있는 함수 또한 가지고 있습니다. 

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 함수를 통해 Response에 있는 data를 객체 안으로 넣어 응답을 mapping시켰습니다.

Exception mapping

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

핸들러를 호출하지 않고 다른 데이터를 반환하는 케이스도 존재합니다.

예를 들어 캐싱 처리를 하기 위해 캐싱된 데이터를 반환하는 경우입니다.

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

 

More operators

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

위와 같이 timeout operator를 이용해 일정 시간 동안 응답이 없는 경우 error처리가 가능하도록 할 수 있습니다.

다양한 RxJS operator를 활용해 수많은 기능의 interceptor를 제작할 수 있습니다.

 

'FrameWork > NestJS' 카테고리의 다른 글

[NestJS] Exception filter  (0) 2023.11.28
[NestJS] Pipe  (0) 2023.11.28
[NestJS] Guard  (0) 2023.11.27
[NestJS] Middleware  (0) 2023.11.25
[NestJS] Request Lifecycle  (0) 2023.11.25