Nest.js를 배워보자/6. NestJS Middleware , Guard , Intercept

Interceptor (인터셉터): 로깅, 응답 변형, 캐싱

_Blue_Sky_ 2025. 12. 2. 18:34
728x90

 


1. 🎭 Interceptor란 무엇인가요?

인터셉터(Interceptor)AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍) 개념을 구현한 NestJS의 컴포넌트입니다. 요청이 컨트롤러 핸들러로 가기 직전과, 컨트롤러가 응답을 반환한 후 양방향으로 실행 흐름을 가로채서(Intercept) 추가적인 로직을 실행할 수 있습니다.

이는 컨트롤러 메서드 실행 전후에 공통적인 부가 기능(Cross-cutting Concerns)을 삽입하여, 컨트롤러의 핵심 비즈니스 로직을 깨끗하게 유지하는 데 도움을 줍니다.

Interceptor의 주요 역할

  • 응답 변형 (Response Mapping): 컨트롤러가 반환하는 결과에 공통적인 형식(예: statusCode, data, message)을 래핑(Wrapping)합니다.
  • 로깅 (Logging): 요청이 들어온 시점과 응답이 나가는 시점의 시간을 측정하여, API의 처리 시간을 정확히 기록합니다.
  • 캐싱 (Caching): 요청이 컨트롤러에 도달하기 전에 캐시를 확인하고, 유효한 캐시가 있다면 컨트롤러 실행을 건너뛰고 캐시된 응답을 반환합니다.
  • 예외 변환: 데이터베이스 관련 예외 등 특정 예외를 HTTP 친화적인 다른 예외로 변환합니다.
728x90

2. 🏗️ Interceptor 구현 방법

Interceptor는 NestInterceptor 인터페이스를 구현하며, RxJS의 Observable 스트림을 사용하여 비동기적으로 작동합니다.

2.1. NestInterceptor 인터페이스

핵심 메서드는 intercept(context, next)입니다.

  • context: Guard와 마찬가지로 현재 실행 컨텍스트(ExecutionContext) 정보를 담고 있습니다.
  • next: 컨트롤러 핸들러의 실행을 나타내는 RxJS의 CallHandler 인터페이스입니다. next.handle()을 호출해야 비로소 컨트롤러 메서드가 실행됩니다.

2.2. 응답 시간 로깅 Interceptor 예시

이 예시는 요청과 응답 사이의 시간을 측정하여 기록합니다.

// logging.interceptor.ts
import { 
  Injectable, NestInterceptor, ExecutionContext, CallHandler 
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators'; // RxJS의 연산자 사용

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const req = context.switchToHttp().getRequest();

    console.log(`[REQUEST START] ${req.url} @ ${now}`);

    // 💡 next.handle()은 컨트롤러 핸들러 실행을 나타내는 Observable을 반환합니다.
    return next
      .handle()
      .pipe(
        // 💡 응답이 나가는 시점에 실행될 로직을 tap 연산자를 사용하여 정의
        tap(() => {
          const responseTime = Date.now() - now;
          console.log(`[RESPONSE END] ${req.url} took ${responseTime}ms`);
        }),
      );
  }
}

2.3. 응답 래핑(Wrapping) Interceptor 예시

컨트롤러의 반환 값을 { data: result } 형태로 변형하여 일관성을 부여합니다.

// transform.interceptor.ts
import { Injectable, NestInterceptor, CallHandler } from '@nestjs/common';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    // 💡 map 연산자를 사용하여 컨트롤러 반환 값을 조작
    return next.handle().pipe(
      map(data => ({
        statusCode: context.switchToHttp().getResponse().statusCode,
        timestamp: new Date().toISOString(),
        data: data, // 컨트롤러의 실제 반환 값
      }))
    );
  }
}

3. 📌 Interceptor 적용 및 실행 순서

Interceptor는 Guard, Pipe와 마찬가지로 @UseInterceptors() 데코레이터를 사용하여 컨트롤러, 메서드, 또는 글로벌 레벨에 적용할 수 있습니다.

3.1. 실행 순서 (요청 및 응답 흐름)

Interceptor는 실행 흐름을 가로채기 때문에 요청 처리 과정 중 두 번 실행됩니다.

  1. Middleware
  2. Guard
  3. Pipe
  4. Interceptor (요청 전): next.handle() 호출 직전
  5. Controller Handler (실행)
  6. Interceptor (응답 후): .pipe()의 tap이나 map 연산자가 실행
  7. Client (응답)


728x90

3.2. DI와 확장성

Guard와 Pipe처럼 Interceptor 역시 DI 컨테이너에 의해 관리되므로, 로깅 서비스, 캐싱 서비스 등 다른 의존성 서비스를 주입받아 사용할 수 있습니다. 이는 복잡한 횡단 관심사(Cross-cutting Concerns)를 시스템에 깨끗하게 통합하는 데 매우 강력한 방법입니다.

728x90