NestJS Interceptor로 효율적인 로깅 시스템 구축하기

2022. 12. 14. 19:12Backend Development/NestJS

반응형

NestJS는 강력한 모듈화와 데코레이터 기반 구조를 통해 애플리케이션의 다양한 측면을 쉽게 관리할 수 있는 프레임워크입니다. 이 중 인터셉터는 요청과 응답을 가로채어 추가 로직을 실행할 수 있는 중요한 기능입니다. 이 글에서는 인터셉터를 사용하여 로깅 처리를 구현하는 방법을 다룹니다.


1. NestJS의 인터셉터란?

NestJS의 인터셉터는 요청과 응답의 흐름을 제어하거나 데이터를 변환할 때 사용됩니다. 다음과 같은 기능을 수행할 수 있습니다:

  1. 요청 및 응답 가로채기
    요청이 컨트롤러에 도달하기 전, 응답이 클라이언트로 전달되기 전에 추가 작업을 수행합니다.
  2. 응답 데이터 변환
    컨트롤러에서 반환된 데이터를 원하는 형태로 변환할 수 있습니다.
  3. 예외 처리
    실행 중 발생한 예외를 포착하여 변환하거나 추가적인 처리를 수행할 수 있습니다.

인터셉터의 구조

NestJS의 인터셉터는 NestInterceptor 인터페이스를 구현하여 생성됩니다.

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

@Injectable()
export class ExampleInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 요청 전 로직
    console.log('Before...');

    return next
      .handle() // 다음 미들웨어 또는 컨트롤러 호출
      .pipe(
        // 응답 후 로직
      );
  }
}

2. 로깅 인터셉터 구현하기

로깅 인터셉터를 만들어 요청 처리 시간을 측정하고 로그를 기록하는 방법을 살펴보겠습니다.

로깅 인터셉터 코드

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> {
    const now = Date.now();
    console.log('Request started...');

    return next
      .handle()
      .pipe(
        tap(() =>
          console.log(`Request completed in ${Date.now() - now}ms`),
        ),
      );
  }
}

코드 설명

  1. ExecutionContext: 요청에 대한 정보를 가져올 수 있습니다. 예: 요청의 경로, 메서드, 헤더 등.
  2. CallHandler: 컨트롤러의 요청 처리를 담당합니다.
  3. tap 연산자: RxJS의 연산자로, 응답이 처리된 후 추가 로직을 실행할 수 있습니다.

3. 인터셉터 적용하기

3.1. 특정 라우트에 적용

인터셉터를 특정 라우트에서만 사용하려면, 메서드 수준에서 @UseInterceptors() 데코레이터를 사용합니다.

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('example')
export class ExampleController {
  @Get()
  @UseInterceptors(LoggingInterceptor)
  getExample() {
    return { message: 'Hello World!' };
  }
}

3.2. 컨트롤러 전체에 적용

컨트롤러 수준에서 인터셉터를 적용하려면, @UseInterceptors() 데코레이터를 클래스 위에 추가합니다.

@UseInterceptors(LoggingInterceptor)
@Controller('example')
export class ExampleController {
  @Get()
  getExample() {
    return { message: 'Hello World!' };
  }
}

3.3. 글로벌 적용

애플리케이션 전체에서 인터셉터를 사용하려면, app.useGlobalInterceptors()를 사용합니다.

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

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

4. 고급 활용: 응답 데이터 변환

인터셉터는 로깅 외에도 응답 데이터를 변환하거나 특정 형식으로 가공하는 데 유용합니다.

응답 데이터 변환 예제

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

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => ({
        success: true,
        data,
      })),
    );
  }
}

결과

컨트롤러에서 반환한 데이터를 아래와 같이 변환합니다:

{
  "success": true,
  "data": {
    "message": "Hello World!"
  }
}

5. 모범 사례

  1. 단일 책임 원칙 적용
    각 인터셉터는 단일 목적을 가져야 합니다. 로깅, 데이터 변환 등 각 작업을 분리하여 관리하세요.
  2. 재사용 가능한 인터셉터 작성
    공통 로직은 재사용 가능하도록 모듈화하고 글로벌 적용을 고려하세요.
  3. 에러 핸들링
    실행 중 발생할 수 있는 예외를 인터셉터에서 처리하여 일관된 에러 응답을 제공합니다.

6. 마무리

NestJS의 인터셉터는 요청 및 응답 흐름을 제어하고, 공통 작업을 효율적으로 관리할 수 있는 강력한 도구입니다. 이 글에서는 로깅을 중심으로 인터셉터의 기본 사용법과 고급 활용 방법을 살펴보았습니다. 인터셉터를 적절히 활용하면 애플리케이션의 유지보수성과 확장성을 크게 향상시킬 수 있습니다.

궁금한 점이 있다면 댓글로 남겨주세요! 😊

반응형