본문 바로가기
backend/NestJS

[NestJS] NestJS에서의 Interceptor를 통한 AOP 구현

by BK0625 2023. 8. 11.
반응형

이번 포스팅에서는 NestJS에서 AOP를 인터셉트를 사용해 구현하는걸 알아보고자 한다.

 

기본적으로 Nest에서 인터셉터는 AOP에서 영감을 받았기 때문이다.

 

 

AOP(Aspect Oriented Programming)

AOP는 우리말로 관점 지향 프로그래밍이라고 한다. 목적은 바로 모듈성을 높이는 것. AOP에서의 관점은 흩어진 관심사를 응집시키는 것을 말한다. 기본적으로 객체지향 프로그램에서는 설계 시 책임, 관심사에 따라 단일 책임을 지도록 클래스를 분리하고 함수를 짜게 된다. 이로 인해 결합도는 낮아지고 응집도는 높아져서 변화하는 요구사항에 더 유연하게 대처하고 유지보수를 하기 편한 안전하고 생산성 높은 프로그래밍이 가능해진다.

 

 

하지만 만약 특정 클래스들에서 공통적으로 사용하는 기능이 있다면? 심지어 비즈니스 로직과 관련이 없거나 부가적인 기능이라면?

 

이미지 출처 https://tasddc.tistory.com/129

 

 

그렇다면 각각의 클래스마다 해당 기능을 파편화 시켜 넣는건 비효율적이다. 이걸 하나의 책임을 지는 기능으로 묶는 뒤에 (중요한건 이 '관점'에 대해서만 책임을 지도록 응집화를 해야한다.) 공통 기능을 사용할 각각의 클래스, 모듈에 덮어씌워서 사용하는 것이다.

 

 

인터셉터를 통한 구현

 

일단 나는 어떤 요청이 들어왔을 때 해당 요청을 처리하는데에 얼마나 걸리는지 시간을 재서 로그로 기록을 남기고 싶었다. 이건 비즈니스 로직과는 전혀 관련이 없는 운영적인 측면이고 이걸 함수 하나하나에 다 기능을 넣는건 너무 비효율적인 일이였다. 그래서 인터셉터를 통해 AOP를 구현해보았다.

 

인터셉터는 먼저 컨트롤러를 두고 전후로 수행이 된다.

 

순서를 나열해보자면 Client Requert -> Pre Interceptor -> Controller Handler -> Service -> ... -> Controller Handler -> Post Interceptor 가 되고 Pre, Post Interceptor에서 수행된다. 우선 코드를 보면서 설명하자.

 

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

@Injectable()
export class MethodTimeMeterInterceptor implements NestInterceptor{
    //constructor(private readonly reflector: Reflector) {}
   
    intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
        console.log("func timer")
        const start = Date.now();
       

        console.log(context.getHandler().name);
        console.log(context.getClass().name);

        const funcName = context.getHandler().name;
        const className = context.getClass().name;

        return next.handle().pipe(
            tap(async () => {
                const end = Date.now();
                const time = end - start;
                const logger = new Logger();
                logger.debug(`function '${funcName}' of class '${className}' executed in ${time} ms. `)
            })
        )
    }
}

 

NestJS에서 Interceptor를 사용하기 위해서는 @nsetjs/common 패키지에서 제공하는 NestInterceptor 인터페이스를 구현해야한다. 인터페이스에 대해서 공부한 사람은 알겠지만 해당 인터페이스에서는 intercept 메서드를 구현하도록 되어 있고, 이 메소드는 ExecutionContext와 CallHandler를 인자로 받아, 비동기 혹은 동기 형태로 Observable을 반환하는 메서드이다. 먼저 start 변수에 Date.now()를 통해 요청이 들어온 시간을 넣어놓는다. 그리고 첫번째 인자인 ExecutionContext엔 Client 부터의 요청에 대한 정보가 담겨 있는데 

 const funcName = context.getHandler().name;
 const className = context.getClass().name;

 

이렇게 해당 요청이 들어온 클래스와 함수 이름을 알 수가 있다. 그리고 next의 handle() 메서드를 통해 그 다음 로직을 실행하거나 요청을 반환할 수 있다. 즉 첫번째 인자인 ExecutionContext를 사용하면 Pre Interceptor가 될 수 있고, 두 번째 인자인 next를 사용하면 Post Interceptor가 될 수 있다.

 

 

Observable?

처리 과정을 살펴보자. 해당 요청이 들어왔을 때 이 요청은 컨트롤러 내부에 있는 해당 요청 핸들러로 향하게 된다. handle() 메서드를 호출하지 않는 인터셉터가 중간에 호출되면 요청된 메서드는 실행되지 않는다. 비즈니스 로직 메서드를 실행하려면 handle()을 호출해야 한다. handle() 메소드가 호출되고 Observable이 반환되면 그 때 요청 메소드 핸들러가 트리거가 된다. 그리고 응답 스트림이 Observable을 통해 수신되면 스트림에서 추가 작업을 수행할 수 있고 최종 결과를 반환하게 된다.

 

여기서 RxJS 라이브러리가 사용되는데 이는 observable sequences를 사용하여 비동기 및 이벤트 기반 프로그램을 구성하기 위한 라이브러리이다. 여기서 사용된 tap은 Observable이 내보낸 값에 대해 다른 영향 없이 수행할 수 있게 해준다. 즉 컨트롤러에서 처리가 끝난 뒤에 연산을 할 수 있다는 것. 여기서 해당 요청이 끝난 시간을 알 수 있게 된다. tap 함수 안에서 Date.now()를 사용하여 종료 시간을 기록하고 시작 시간과 종료 시간의 차이를 계산하여 로그로 나타낸다.

 

 

 

적용

인터셉터를 전역으로 적용하기 위해 app.module.ts에 다음과 같이 코드를 작성해주면 된다.

 

이미 다른 인터셉터도 존재한다.

 

이렇게 하면

 

 

 

이렇게 어떤 클래스의 함수를 실행했을 때 시간이 얼마나 걸렸는지를 로그로 나타낼 수가 있다.

 

 

공부하면서 정리한 내용입니다. 모든 지적 감사히 받겠습니다:)

반응형