NestJS Request Lifecycle (3) - Interceptor

2022. 12. 26. 17:45ㆍBackend/NestJS

이전 κΈ€


인터셉터

NestJS λ‚΄μ—μ„œ μΈν„°μ…‰ν„°λž€ 컨트둀러 μ „, ν›„μ—μ„œ λ‹€μ–‘ν•œ 역할을 ν•΄μ£ΌλŠ” 생λͺ…μ£ΌκΈ°λ₯Ό μ˜λ―Έν•œλ‹€. κ³΅μ‹λ¬Έμ„œμ—μ„œ μ„€λͺ…ν•˜λŠ” 인터셉터가 ꡬ체적으둜 ν•˜λŠ” 일은 λ‹€μŒκ³Ό κ°™λ‹€.

  • λ©”μ„œλ“œ(컨트둀러) μ‹€ν–‰ μ „, ν›„μ˜ 좔가적인 둜직 μˆ˜ν–‰
  • ν•¨μˆ˜μ—μ„œ λ°˜ν™˜λœ κ²°κ³Ό 및 μ˜ˆμ™Έμ— λŒ€ν•œ λ³€ν™˜
  • κΈ°λ³Έ ν•¨μˆ˜ λ™μž‘μ˜ ν™•μž₯
  • νŠΉμ • 쑰건 ν•˜μ—μ„œ μ™„μ „ν•œ ν•¨μˆ˜μ˜ μž¬μ •μ˜(ex. 캐싱 λͺ©μ )

좜처: NestJS 곡식 λ¬Έμ„œ(https://docs.nestjs.com/interceptors)

μΈν„°μ…‰ν„°μ—λŠ” 2개의 μΈμžκ°€ μ‚¬μš©λœλ‹€. μ²«λ²ˆμ§ΈλŠ” μ•žμ„  κ°€λ“œμ—μ„œλ„ 닀룬 μ‹€ν–‰μ»¨ν…μŠ€νŠΈ(ExecutionContext)이닀. 이λ₯Ό μ΄μš©ν•˜μ—¬ μΈν„°μ…‰ν„°λŠ” νŠΉμ • μ»¨ν…μŠ€νŠΈκ°€ μ–΄λ–€ ν•Έλ“€λŸ¬λ₯Ό 톡해 μ²˜λ¦¬λ˜λŠ”μ§€ μ•Œ 수 μžˆλ‹€. λ‘λ²ˆμ§ΈλŠ” μ½œν•Έλ“€λŸ¬(CallHandler)이닀. μΈν„°μ…‰ν„°λŠ” 이 μ½œν•Έλ“€λŸ¬μ˜ handle() λ©”μ„œλ“œλ₯Ό 톡해 λ°˜ν™˜λ˜λŠ” RxJS Observable 객체λ₯Ό λ°˜ν™˜ν•˜λ„λ‘ μž‘μ„±λ˜μ–΄μ•Όν•œλ‹€. 각각의 인자λ₯Ό 톡해, μΈν„°μ…‰ν„°λŠ” ν•Έλ“€λŸ¬ μ‹€ν–‰ μ „, 후에 μ ‘κ·Όν•  수 μžˆκ²Œλœλ‹€.

 

인터셉터 μž‘μ„±

NestJS의 인터셉터λ₯Ό μž‘μ„±ν•˜κΈ° μœ„ν•΄μ„œλŠ” @Injectable() λ°μ½”λ ˆμ΄ν„°λ₯Ό ν†΅ν•œ 클래슀λ₯Ό μž‘μ„±ν•΄μ£Όμ–΄μ•Όν•œλ‹€. 이 λ•Œ ν•΄λ‹Ή ν΄λž˜μŠ€λŠ” NestJS의 NestInterceptorλ₯Ό μƒμ†ν•΄μ•Όν•˜λ©°, 클래슀 λ‚΄λΆ€μ μœΌλ‘œλŠ” interceptor λ©”μ„œλ“œλ₯Ό ν¬ν•¨ν•΄μ•Όν•œλ‹€. 이 λ•Œ μž‘μ„±λ˜λŠ” interceptor λ©”μ„œλ“œμ˜ 인자둜, μ•žμ„œ μ–ΈκΈ‰ν•œ ExecutionContext와 CallHandlerκ°€ ν¬ν•¨λœλ‹€. 기본적으둜 μž‘μ„±λ˜μ–΄μ•Ό ν•  μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

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

@Injectable()
export class TestInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
        // ν•Έλ“€λŸ¬ 도달 μ „ 인터셉터 둜직
        return next.handle().pipe(
            // ν•Έλ“€λŸ¬ 도달 이후 인터셉터 둜직
        )
    }
}

 

인터셉터 적용

NestJS ν”„λ‘œμ νŠΈμ— 인터셉터λ₯Ό μ μš©ν•˜λŠ” 방법은 크게 3가지가 μ‘΄μž¬ν•œλ‹€. (κ°€λ“œμ˜ 방식과 동일)

  1. λͺ¨λ“ˆ λ°–μ—μ„œ μž‘λ™ν•˜λŠ” μ „μ—­ 인터셉터
  2. λͺ¨λ“ˆ λ‚΄μ—μ„œ μž‘λ™ν•˜λŠ” μ „μ—­ 인터셉터
  3. λͺ¨λ“ˆ λ‚΄ νŠΉμ • 컨트둀러 ν˜Ήμ€ λ©”μ„œλ“œμ— μž‘λ™ν•˜λŠ” 인터셉터

μ΄λ“€μ˜ μž‘λ™ μˆœμ„œλŠ” 쑰금 νŠΉμ΄ν•œλ°, μ΄ˆκΈ°μ— μ˜ˆμƒν•œ 1 -> 2 -> 3 μˆœμ„œκ°€ μ•„λ‹Œ 2 -> 1 -> 3 μˆœμ„œλ‘œ μž‘λ™ν•œλ‹€! (ν•Έλ“€λŸ¬λ₯Ό 거친 λ’€μ—λŠ” μ—­μˆœ)

λͺ¨λ“ˆ λ°–μ—μ„œ μž‘λ™ν•˜λŠ” μ „μ—­ 인터셉터

이λ₯Ό μœ„ν•΄μ„œλŠ” NestJS μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ useGlobalInterceptors() λ©”μ„œλ“œλ₯Ό ν™œμš©ν•  수 μžˆλ‹€. 이 λ•Œ ν•΄λ‹Ή λ©”μ„œλ“œμ—λŠ” 인터셉터 클래슀의 μΈμŠ€ν„΄μŠ€κ°€ 직접 μ „λ‹¬λ˜μ–΄μ•Όν•˜λ©°, λͺ¨λ“ˆ λ°–μ—μ„œ μΆ”κ°€ν•΄μ£ΌλŠ” 인터셉터이기 λ•Œλ¬Έμ— λ‹€λ₯Έ μ˜μ‘΄μ„±μ€ μ‚¬μš©ν•  수 μ—†λ‹€.

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

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

λͺ¨λ“ˆ λ‚΄μ—μ„œ μž‘λ™ν•˜λŠ” μ „μ—­ 인터셉터

ν•΄λ‹Ή 방식을 ν†΅ν•˜λ©΄ λ‹€λ₯Έ μ˜μ‘΄μ„±μ„ μ‚¬μš©ν•˜λ©΄μ„œλ„ 인터셉터λ₯Ό μ „μ—­μœΌλ‘œ μ„€μ •ν•  수 μžˆλ‹€. 

κ³΅μ‹λ¬Έμ„œμ— λ”°λ₯΄λ©΄, ν•΄λ‹Ή 방식은 λ‹€μŒμ˜ νŠΉμ§•μ„ κ°–λŠ”λ‹€.

νŠΉμ • μΈν„°μ…‰ν„°μ˜ μ „μ—­ 섀정을 μœ„ν•΄ ν•΄λ‹Ή λ°©μ‹μœΌλ‘œ λͺ¨λ“ˆμ— μ˜μ‘΄μ„± μ£Όμž…μ„ ν•  λ•Œ, μ£Όμž…λ˜λŠ” λͺ¨λ“ˆμ˜ μœ„μΉ˜μ— 상관없이 μ΄λŠ” μ „μ—­μœΌλ‘œ μž‘λ™ν•©λ‹ˆλ‹€. 즉, μΈν„°μ…‰ν„°μ˜ μ£Όμž…μ€ ν•΄λ‹Ή 인터셉터가 μƒμ„±λœ λͺ¨λ“ˆμ—μ„œ 이루어져도 λ¬΄λ°©ν•©λ‹ˆλ‹€.

이에 λ”°λ₯΄λ©΄ μ „μ—­ 인터셉터 섀정을 μœ„ν•œ μ˜μ‘΄μ„± μ£Όμž…μ€ κΌ­ μ΅œμƒμœ„ λͺ¨λ“ˆμ— 이루어지지 μ•Šμ•„λ„ λœλ‹€λŠ” 것을 μ˜λ―Έν•œλ‹€.

이λ₯Ό ν™•μΈν•˜κΈ° μœ„ν•΄ μ΅œμƒμœ„ λͺ¨λ“ˆμ΄ μ•„λ‹Œ TestModule을 μƒμ„±ν•΄μ„œ μ˜μ‘΄μ„± μ£Όμž…μ„ 해보겠닀.

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { TestInterceptor } from './app.interceptor';

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

ν•΄λ‹Ή λͺ¨λ“ˆμ„ μ΅œμƒμœ„ λͺ¨λ“ˆ(app.module.ts)에 μ£Όμž… ν›„ TestModuleκ³Ό λ¬΄κ΄€ν•œ 컨트둀러λ₯Ό ν˜ΈμΆœν•΄λ³΄λ‹ˆ μ „μ—­μœΌλ‘œ 잘 μž‘λ™ν•œλ‹€! 

λͺ¨λ“ˆ λ‚΄ νŠΉμ • 컨트둀러 ν˜Ήμ€ λ©”μ„œλ“œμ— μž‘λ™ν•˜λŠ” 인터셉터

ν•΄λ‹Ή 방식을 ν†΅ν•˜λ©΄, νŠΉμ • 컨트둀러 ν˜Ήμ€ λ©”μ„œλ“œμ—λ§Œ μ μš©λ˜λŠ” 인터셉터λ₯Ό μΆ”κ°€ν•  수 μžˆλ‹€.  NestJS의 @UseInterceptors() λ°μ½”λ ˆμ΄ν„°λ₯Ό 톡해 μΆ”κ°€ν•˜λŠ” 방식이고, λ©”μ„œλ“œμ˜ μΈμžλ‘œλŠ” 인터셉터 클래슀 ν˜Ήμ€ 클래슀λ₯Ό ν†΅ν•œ μΈμŠ€ν„΄μŠ€κ°€ μš”κ΅¬λœλ‹€.

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { TestInterceptor } from './interceptors/test2.interceptor';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('test')
  @UseInterceptors(TestInterceptor) // 인터셉터λ₯Ό μΆ”κ°€ν•˜λŠ” 방식
  getHello(): string {
    return this.appService.getHello();
  }
}

 

인터셉터 ν™œμš©

λ‹€μŒμ€ NestJSμ—μ„œ 인터셉터λ₯Ό ν™œμš©ν•˜λŠ” λ°©μ•ˆλ“€μ— λŒ€ν•΄ μ•Œμ•„λ³΄κ² λ‹€. μ˜ˆμ‹œλ“€μ€ κ³΅μ‹λ¬Έμ„œμ˜ 예제λ₯Ό κ°€μ Έμ˜¨ 것도 있고, μ‹€μ œ ν”„λ‘œμ νŠΈμ— ν™œμš©ν–ˆλ˜ μ½”λ“œλ₯Ό κ°€μ Έμ˜¨ 뢀뢄도 μ‘΄μž¬ν•œλ‹€.

ν•Έλ“€λŸ¬ 처리 μ‹œκ°„ 둜그 좜λ ₯ 예제

μ—¬κΈ°μ—μ„œλŠ” NestJS 인터셉터와 RxJS의 tap Operatorλ₯Ό μ΄μš©ν•˜μ—¬ ν•Έλ“€λŸ¬λ₯Ό μ²˜λ¦¬ν•˜λŠ”λ° μ–Όλ§ˆλ‚˜ κ±Έλ¦¬λŠ”μ§€ 둜그λ₯Ό 좜λ ₯ν•˜λŠ” 예제λ₯Ό μž‘μ„±ν•œλ‹€. μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

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

@Injectable()
export class TestInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    const timer = Date.now();
    console.log('ν•Έλ“€λŸ¬ 도착 μ „...');
    return next
      .handle()
      .pipe(tap(() => console.log(`ν•Έλ“€λŸ¬ 도착 ν›„...${Date.now() - timer}ms`)));
  }
}

λ°˜ν™˜ ν˜•νƒœ ν…œν”Œλ¦Ώ 적용

λ‹€μŒμ€ ν΄λΌμ΄μ–ΈνŠΈμ˜ μš”μ²­μ— λŒ€ν•œ 응닡을 μΌμ •ν•œ 데이터 ꡬ쑰λ₯Ό 톡해 λ°˜ν™˜ν•˜κΈ° μœ„ν•œ 인터셉터 μ˜ˆμ œμ΄λ‹€.

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

// μ‚¬μš©μžμ—κ²Œ λ°˜ν™˜ν•  데이터 ν…œν”Œλ¦Ώ
interface ResponseTemplate<T> {
  message: string;
  data: T;
}

@Injectable()
export class TestInterceptor<T>
  implements NestInterceptor<T, ResponseTemplate<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ):
    | Observable<ResponseTemplate<T>>
    | Promise<Observable<ResponseTemplate<T>>> {
    return next.handle().pipe(
      map((resData) => ({
        message: resData.message || '',
        data: resData.data || '',
      })),
    );
  }
}

μ˜ˆμ™Έ 처리 

λ‹€μŒμ€ μ—λŸ¬κ°€ λ°œμƒν–ˆμ„ λ•Œ 이λ₯Ό λ¬΄μ‹œν•˜κ³  μΌκ΄„μ μœΌλ‘œ NestJS BadRequest 처리λ₯Ό ν•΄μ£ΌλŠ” 인터셉터이닀.

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

@Injectable()
export class TestInterceptor<T>
  implements NestInterceptor<T, ResponseTemplate<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ):
    | Observable<ResponseTemplate<T>>
    | Promise<Observable<ResponseTemplate<T>>> {
    return next
      .handle()
      // μ—λŸ¬ λ°œμƒ μ‹œ μΌκ΄„μ μœΌλ‘œ NestJS BadRequestException 처리
      .pipe(catchError((err) => throwError(() => new BadRequestException())));
  }
}

μš”μ²­ μ‹œκ°„ μ œν•œ 인터셉터

λ‹€μŒ μ˜ˆμ œλŠ”, μš”μ²­ λ°˜ν™˜κΉŒμ§€ κ±Έλ¦¬λŠ” μ‹œκ°„μ΄ 5μ΄ˆκ°€ λ„˜λŠ” 경우, μΌκ΄„μ μœΌλ‘œ 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), // 5000ms의 λŒ€κΈ° ν›„ TimeoutErrorλ₯Ό 던짐
      catchError((err) => {
        if (err instanceof TimeoutError) { // λ§Œμ•½ μ—λŸ¬κ°€ TimeoutError라면 RequestTimeoutException을 던짐
          return throwError(() => new RequestTimeoutException());
        }
        return throwError(() => err);
      }),
    );
  }
}

캐싱 인터셉터

λ§ˆμ§€λ§‰μ€ μš”μ²­μ΄ 메인 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ—μ„œ 처리되기 μ „, 캐싱 데이터가 μžˆλ‹€λ©΄ κ³§λ°”λ‘œ μš”μ²­μ„ μ²˜λ¦¬ν•˜λŠ” 인터셉터이닀.

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

μœ„ μ½”λ“œλŠ” λ‹¨μˆœ μ˜ˆμ‹œμ— λΆˆκ³Όν•˜λ©°, μ‹€μ œ μ½”λ“œμ—μ„œλŠ” isCached 뢀뢄에 μ‹€μ œ 캐싱 μ—¬λΆ€λ₯Ό ν™•μΈν•˜λŠ” 둜직이 ν¬ν•¨λ˜μ–΄μ•Ό ν•œλ‹€. λ§Œμ•½ 캐싱이 λ˜μ–΄μžˆλŠ” 경우 of()λ₯Ό 톡해 μƒμ„±λœ μƒˆλ‘œμš΄ μŠ€νŠΈλ¦Όμ„ κ³§λ°”λ‘œ ν΄λΌμ΄μ–ΈνŠΈμ— λ°˜ν™˜ν•˜κ³ , 컨트둀러 λ‘œμ§μ€ μ²˜λ¦¬λ˜μ§€ μ•ŠλŠ”λ‹€.

'Backend > NestJS' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€

NestJS Request Lifecycle (5) - Exception Filter  (0) 2023.01.13
NestJS Request Lifecycle (4) - Pipe  (0) 2022.12.29
NestJS Request Lifecycle (2) - Guard  (0) 2022.12.23
NestJS Request Lifecycle (1) - Middleware  (0) 2022.12.22
NestJS Request Lifecycle (0) - κ°œμš”  (0) 2022.12.22