개발이 취미인 사람

[Nest.js] 심화 - 인터셉터(Interceptors) 개념 및 사용법 본문

백앤드(Back-End)/Nest.js

[Nest.js] 심화 - 인터셉터(Interceptors) 개념 및 사용법

RyanSin 2022. 6. 18. 16:33
반응형

- 개요

안녕하세요. 이번 시간에는 인터셉터(Interceptors)에 대해 알아보겠습니다.

 

인터셉터를 접하게 되면 항상 따르는 AOP(Aspect Oriented Programming) 기술을 강조하고 있습니다.

  1. 메서드 실행 전/후 추가적으로 로직을 바인딩
  2. 함수에서 반환된 결과를 변환
  3. 함수에서 발생된 예외를 변환
  4. 기본적인 기능에서 확장
  5. 특정 조건에 따라 기능을 재정의

위와 같이 다섯 가지 예시를 들면서 공식 홈페이지에서 설명하고 있습니다.

 

사실 깊이 파고들면 어려운 개념이지만 이번 시간에는 AOP에 대한 이해보다는 Nest.js 프레임워크에서 인터셉터(Interceptors) 사용법에 초점을 맞춰서 진행하겠습니다.

 

AOP에 대한 설명은 다른 포스팅에서 설명하도록 하겠습니다.

 

- 사용법

공식 홈페이지에 나와있는 소스 코드를 예시를 들어 진행하겠습니다.

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

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now(); //현재 시간을 얻기 위해 사용됨
    console.log(`Before... ${Date.now() - now}ms`);

    return next.handle().pipe(
      tap(() => console.log(`After... ${Date.now() - now}ms`)), // 로깅 처리
      map((responseData) => responseData), // 클라이언트에게 반환 되는 정보(각 컨트롤러 결과 값)
      catchError((err) => throwError(() => new BadGatewayException())), // 예외 발생시 처리
    );
  }
}

 

기본적으로 인터셉터를 사용할 때는 IoC 컨테이너에서 관리할 수 있게 @Injectable()  어노테이션을 선언해서 클래스를 관리합니다.

 

그리고 우리가 사용할 인터셉터 기능을 사용하기 위해 NestInterceptor 인터페이스를 implements 합니다.

 

- NestInterceptor Interface

NestInterceptor 인터페이스에 대해 지원하는 기능에 대해 간단하게 설명하고 진행하겠습니다. (내부 소스 코드를 이해해야 된다고 저는 생각하기 때문에 이 부분을 학습하고 진행하겠습니다. :>)

import { Observable } from 'rxjs';
import { ExecutionContext } from './execution-context.interface';
/**
 * Interface providing access to the response stream.
 *
 * @see [Interceptors](https://docs.nestjs.com/interceptors)
 *
 * @publicApi
 */
export interface CallHandler<T = any> {
    /**
     * Returns an `Observable` representing the response stream from the route
     * handler.
     */
    handle(): Observable<T>;
}
/**
 * Interface describing implementation of an interceptor.
 *
 * @see [Interceptors](https://docs.nestjs.com/interceptors)
 *
 * @publicApi
 */
export interface NestInterceptor<T = any, R = any> {
    /**
     * Method to implement a custom interceptor.
     *
     * @param context an `ExecutionContext` object providing methods to access the
     * route handler and class about to be invoked.
     * @param next a reference to the `CallHandler`, which provides access to an
     * `Observable` representing the response stream from the route handler.
     */
    intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>;
}

우리는 NestInterceptor 인터페이스를 사용하고 있습니다.

 

지원하는 메서드는 intercept 메서드 하나만 지원하고 있습니다. 여기서 중요한 부분은 3가지입니다.

 

  1. context: ExecutionContext
  2. next: CallHandler<T>
  3. 반환 타입 : Observable<R> | Promise<Observable<R>>

- ExecutionContext

ExecutionContext는 ArgumentsHost 인터페이스를 상속받아 사용합니다.  (인터페이스 간에 상속은 implements를 사용하지 않고 extends를 사용합니다.)

 

ArgumentsHost는 간단하게 핸들러에게 전달되는 인수 정보를 검색하기 위한 메서드들을 제공합니다.

 

무슨 말일 까요?...  간단하게 클라이언트 요청 정보를 파악한다고 생각하시면 됩니다.

 

응?... 어떤 걸 파악하는 거지 -> 클라이언트와 서버와 통신 시 무조건 서로 합의해야 하는 부분이 있습니다.

 

그건 바로 프로토콜입니다. (Ex: HTTP, RPC, WebSockets)

 

즉, 클라이언트가 요청 정보(요청 데이터, 해더 정보, 라우팅 경로, HTTP메서드 등..)와 같은 정보를 검색하고 파악합니다.

 

그렇다면 ExcutionContext는 무엇을 하는 걸까요. ArgumentsHost를 상속받아 사용하고 있기 때문에 원리는 동일합니다.

 

실제 ExecutionContext를 정보를 확인하면 아래와 같은 정보를 확인할 수 있습니다. (더보기를 클릭해주세요. :>)

더보기

context :  ExecutionContextHost {
  args: [
    IncomingMessage {
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      socket: [Socket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      headers: [Object],
      rawHeaders: [Array],
      trailers: {},
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '/board',
      method: 'POST',
      statusCode: null,
      statusMessage: null,
      client: [Socket],
      _consuming: true,
      _dumped: false,
      next: [Function: next],
      baseUrl: '',
      originalUrl: '/board',
      _parsedUrl: [Url],
      params: {},
      query: {},
      res: [ServerResponse],
      body: [Object],
      _body: true,
      length: undefined,
      route: [Route],
      [Symbol(kCapture)]: false,
      [Symbol(RequestTimeout)]: undefined
    },
    ServerResponse {
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: false,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: true,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      _contentLength: null,
      _hasBody: true,
      _trailer: '',
      finished: false,
      _headerSent: false,
      socket: [Socket],
      _header: null,
      _keepAliveTimeout: 5000,
      _onPendingData: [Function: bound updateOutgoingData],
      _sent 100: false,
      _expect_continue: false,
      req: [IncomingMessage],
      locals: [Object: null prototype] {},
      statusCode: 201,
      [Symbol(kCapture)]: false,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype]
    },
    [Function: next]
  ],
  constructorRef: [class BoardController],
  handler: [Function: createBoard],
  contextType: 'http'
}

 

클라이언트 요청 정보를 확인할 수 있습니다. 필요시에 해당 context를 사용해서 특정 로직을 구현하면 됩니다.

 

 

- CallHandler

CallHandler는 클라이언트 요청 경로로 라우팅 하는 역할을 합니다.

 

Nest.js에는 요청 주기(Request Lifecycle)라는 생명주기가 존재합니다.

 

출처 : https://docs.nestjs.com/faq/request-lifecycle

공식 홈페이지에 나와 있는 정보를 확인해보면 Interceptors 다음 Pipes 그리고 Controller라는 걸 확인할 수 있습니다.

 

즉, CallHandler는 Interceptors 다음 생명주기로 넘어갈 때 사용됩니다.

 

또한 Retrun 타입이 Observable <T> 타입이기 때문에 rxjs에 대한 이해도도 필요합니다. (rxjs는 다른 포스팅에서 소개하겠습니다.)

 

- Observable | Promise<observable></observable

반환 타입에 대해서는 옵저버 패턴과 javascript에서 사용하는 옵져버 패턴 라이브러리에 대한 소개는 다른 포스팅에서 진행하겠습니다.

 


위 3가지에 대해서 꼭 알아두셔야 원하는 로직을 구현하는데 문제가 없습니다.


이제 LoggingInterceptor를 선언해서 사용하는 방법에 대해 알아보겠습니다. (위 소스 코드에 주석을 남겨놨기 때문에 코드를 이해 하실겁니다.)

 

- 인터셉터 선언 방법

인터셉터를 선언하는 방법은 보통 3가지가 있습니다.

 

  1. 프로젝트 전체에 적용
  2. 특정 컨트롤러에 적용
  3. 특정 컨트롤러 라우트에 적용

 

프로젝트 전체에 적용

  const app = await NestFactory.create(AppModule);

  //프로젝트 전체에 Interceptor 적용 방식
  app.useGlobalInterceptors(new LoggingInterceptor());

 

특정 컨트롤러에 적용

@Controller('board')
//특정 Controller에 Interceoptor 적용
@UseInterceptors(LoggingInterceptor)
export class BoardController {
  constructor(private readonly boardService: BoardService) {}
}

 

특정 컨트롤러 라우트에 적용

 /**
   * @author Ryan
   * @description 게시글 전체 조회
   *
   * @returns 게시글 정보 리스트
   */
  @Get('')
  //특정 Controller에 Route에 Interceoptor 적용
  @UseInterceptors(LoggingInterceptor)
  getBoardList() {
    return this.boardService.getBoardList();
  }

 

이번 시간에는 인터셉터(Interceptor)에 대해 알아봤습니다. 꼭 실습을 통해 학습하시는 걸 추천드리겠습니다.

 

소스 코드

GitHub : https://github.com/Ryan-Sin/adavnced-nest/tree/Interceptors

 

GitHub - Ryan-Sin/adavnced-nest

Contribute to Ryan-Sin/adavnced-nest development by creating an account on GitHub.

github.com