NestJS 帶你飛! 시리즈 번역 12# Interceptor

2023. 6. 5. 13:30개발 문서 번역/NestJS

728x90
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다.

 

Interceptor란 무엇인가요?

가로채는 사람(것) 이라는 뜻입니다.
이는 관점 지향 프로그래밍(Aspect Oriented Programming, 剖面導向程式設計)에 영감을 받아 만들어졌습니다.
원래 있던 기능의 확장을 지원합니다.

특징은 아래와 같습니다.

  • Controller의 메서드를 실행하기 전/후의 요청을 가로채 응답을 참조하거나 가공할 수 있습니다.
  • Controller의 메서드 실행 전에 발동되는 인터셉터는 Pipe 실행 전에 발동됩니다.
  • Middleware 실행 이후에 실행됩니다.
  • 데이터와 Exception을 수정할 수 있습니다.

Interceptor 만들기

Interceptor는 CLI를 통해 생성이 가능합니다.

$ nest generate interceptor <INTERCEPTOR_NAME>
주의: <INTERCEPTOR_NAME>은 경로를 포함할 수 있습니다. ex: interceptors/hello-world
경로를 포함하여 생성시 src 폴더 안에 경로를 포함하여 Interceptor가 생성됩니다.

 

HelloworldInterceptorinterceptors 폴더 안에 생성해보겠습니다.

$ nest generate interceptor interceptors/hello-world

src 폴더 내에 interceptors 라는 폴더가 보입니다.
안에는 hello-world.interceptor.tshello-world.interceptor.spec.ts가 들어있습니다.

Interceptor의 뼈대가 생성되었습니다.

Interceptor에도 @Injectable 데코레이터의 class가 구현되어있는것을 확인할 수 있습니다.

그러나 Interceptor는 NestInterceptor의 인터페이스와 intercept(context: ExecutionContext, next: CallHandler) 메서드를 반드시 구현해야 합니다.

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

@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle();
  }
}

 

CallHandler

CallHandler는 Interceptor의 중요한 구성원입니다.

handle() 메서드의 구현을 통해 라우팅 처리 메서드를 호출하고 해당하는 Controller 메서드로 진입시킵니다.

즉 매 Interceptor에서 CallHandlerhandle()을 반환하지 않을 시 라우팅 처리는 작동하지 않습니다.

 

CallHandler는 intercept의 파라미터이므로 반드시 intercept 내에서 호출됩니다.

다시말해 handle()을 반환하기 전의 로직을 작성하여 Controller 메서드에 진입 전에 실행할 수 있습니다.
또한 handle()Observable을 반환하기 때문에 이 반환값을 pipe를 사용하여 조정하여 Controller 실행 후의 로직을 처리할 수 있습니다.

 

주의: handle()Observable이며 intercept의 반환값으로 사용함으로써
Nest가 해당 Observablesubscribe하여 내부 로직을 실행할 수 있도록 합니다.

*Observable의 특징은 subscribe하지 않으면 내부 로직이 실행되지 않습니다.
이것이 handle()을 반환하지 않을 시 라우팅 처리가 작동에 실패하는 원인입니다.

ExecutionContext

ExecutionContextArgumentHostclass를 상속받았기 때문에 요청과 관련된 더 많은 정보를 제공할 수 있습니다.

아래는 ExecutionContext가 제공하는 두개의 메서드를 소개합니다.

이 두가지 메서드를 통해 애플리케이션의 유연성을 크게 높일 수 있습니다.

 

Controller Class 받아오기

getClass()를 통해 현재 요청에 해당하는 Controller Class를 가져올 수 있습니다.

const Controller: TodoController = context.getClass<TodoController>();
Controller Method 받아오기

getHandler()를 통해 현재 요청에 해당하는 Controller Method를 가져올 수 있습니다.

예를 들어 현재 요청이 TodoControllergetAll()을 호출할 시 이 메서드는 getAll 함수를 반환할 것입니다.

const method: Function = context.getHandler();

Interceptor 사용하기

사용 전에 먼저 hello-world.intercpetor.ts를 수정해봅시다.

Interceptor에 들어오면 Hello World!를 출력하고 변수를 사용해 Interceptor 진입 시간을 저장한 후 tap을 통해 빠져나가는 시간과 진입 시간의 시간 차를 구해보겠습니다.

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

@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Hello World!');
    const input = Date.now();
    const handler = next.handle();
    return handler.pipe(
      tap(() => console.log(`${ Date.now() - input } ms`))
    );
  }
}

Interceptor를 수정했다면 한번 사용해봅시다. @UseInterceptors 데코레이터를 통해 쉽게 적용할 수 있습니다.

사용하는 방식은 크게 두가지가 있습니다.

1. 단일 Resource: Controller의 메서드 안에 @UseInterceptors 데코레이터를 적용시킵니다.
이는 해당 Resource에만 적용됩니다.

2. Controller: Controller에 @UseInterceptors 데코레이터를 적용시킵니다.
이는 해당 Controller 안의 모든 Resource에 적용됩니다.

 

아래는 Controller를 적용시킨 app.controller.ts의 예제입니다.

 

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

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

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

http://127.0.0.1:3000에 들어가보면 터미널에 아래와 비슷한 결과가 찍힐겁니다.

Hello World!
3 ms

전역 Interceptor

공용 Interceptor를 모든 Resource에 전부 적용 시키고 싶다면
main.tsuseGlobalInterceptors를 통해 전역 Interceptor를 설정할 수 있습니다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

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

의존성 주입을 통해 전역 Interceptor 구현하기

위의 방법은 모듈 외부에서 전역 구성을 완료하는 방법입니다.

Pipe와 똑같이 의존성 주입을 통해 구현할 수 있으며

Provider의 tokenAPP_INTERCEPTOR로 지정해주어 구현할 수 있습니다.

또한 useClass를 사용해 인스턴스화할 클래스를 지정해줍니다.

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: HelloWorldInterceptor
    }
  ]
})
export class AppModule {}

 

 

마치며

Interceptor는 Controller를 수정하지 않고도 그 로직을 확장시킬 수 있습니다. 매우 편리한 기능이죠?

오늘 배운 것들을 요약하며 포스팅을 마치도록 하겠습니다.

 

1. Interceptor는 Middleware 이후에 실행되지만 Pipe와 Controller 실행 전/후에 실행될 수 있습니다.

2. Interceptor를 사용하면 원래 로직을 변경하지 않고도 로직을 확장하는것이 가능해집니다.

3. CallHandler는 Interceptor의 중요한 구성원입니다. handle()을 호출하여 라우팅 이 동작할수 있도록 해야합니다.

4. ExecutionContextgetClass()getHandler() 메서드를 제공하여 유연성을 증가시켜줍니다.

5. 전역 Interceptor는 의존성 주입을 통해 구현할 수 있습니다.