NestJS 인터셉터 이해하기

SEUNGJUN·2024년 6월 23일
0

NestJS

목록 보기
5/8

NestJS에서 인터셉터(Interceptor)는 요청(Request)와 응답(Response)을 가로채서 추가적인 로직을 실행할 수 있는 기능을 제공한다. 인터셉터는 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)의 한 형태이다.

왜 인터셉터를 사용하는가?

1. 반복되는 코드의 제거

  • 여러 컨트롤러나 서비스에 걸쳐 반복되는 코드를 한 곳에서 관리할 수 있다.

2. 관심사의 분리

  • 비즈니스 로직과 공통 기능을 분리하여 코드의 가독성과 유지보수성을 높일 수 있다.

3. 모듈화

  • 공통 기능을 모듈화하여 여러 곳에서 재사용할 수 있다.

사용 예제

1. 로깅(Logging) 인터셉터

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 request = context.switchToHttp().getRequest();
    const method = request.method;
    const url = request.url;
    const now = Date.now();

    console.log(`Incoming request: ${method} ${url}`);

    return next
      .handle()
      .pipe(
        tap(() => console.log(`Outgoing response: ${method} ${url} - ${Date.now() - now}ms`))
      );
  }
}

설명

  • @Injectable() : 이 클래스가 NestJS의 의존성 주입 시스템에 의해 관리되는 서비스임을 나타낸다.

  • intercept 메소드 : 모든 요청에 대해 가로채는 메소드이다.

    • context.switchToHttp().getRequest() : 현재 HTTP 요청 객체를 가져온다.
    • next.handle().pipe(tap(...)): 요청을 처리한 후 응답 시간을 로그로 기록한다.

2. 변환(Transformation) 인터셉터

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 => {
        // 응답 데이터를 변환하는 로직을 작성한다.
        return {
          statusCode: context.switchToHttp().getResponse().statusCode,
          data
        };
      })
    );
  }
}

설명

  • @Injectable(): 이 클래스가 NestJS의 의존성 주입 시스템에 의해 관리되는 서비스임을 나타낸다.

  • intercept 메소드: 모든 요청에 대해 가로채는 메소드이다.

    • next.handle().pipe(map(...)): 요청을 처리한 후 응답 데이터를 변환한다.
    • map(data => {...}): 응답 데이터에 추가적인 메타데이터(여기서는 statusCode)를 추가한다.

3. 캐싱(Caching) 인터셉터

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

const cache = new Map();

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const key = `${request.method}-${request.url}`;

    if (cache.has(key)) {
      return of(cache.get(key));
    }

    return next.handle().pipe(
      tap(response => cache.set(key, response))
    );
  }
}

설명

  • @Injectable(): 이 클래스가 NestJS의 의존성 주입 시스템에 의해 관리되는 서비스임을 나타낸다.

  • intercept 메소드: 모든 요청에 대해 가로채는 메소드이다.

    • const cache = new Map(): 캐시를 저장하기 위한 맵 객체이다.
    • const key = request.method{request.method}-{request.url};: 요청을 구분하기 위한 키이다.
    • if (cache.has(key)) { return of(cache.get(key)); }: 캐시에 데이터가 있으면 해당 데이터를 반환한다.
    • next.handle().pipe(tap(response => cache.set(key, response))): 요청을 처리한 후 결과를 캐시에 저장한다.

4. 권한 검사(Authorization checks) 인터셉터

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

@Injectable()
export class AuthInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    if (!user || !user.hasAccess) {
      throw new ForbiddenException('You do not have access to this resource');
    }

    return next.handle();
  }
}

설명

  • @Injectable(): 이 클래스가 NestJS의 의존성 주입 시스템에 의해 관리되는 서비스임을 나타낸다.

  • intercept 메소드: 모든 요청에 대해 가로채는 메소드이다.

    • const user = request.user;: 현재 요청에서 사용자 객체를 가져온다.
    • if (!user || !user.hasAccess) { throw new ForbiddenException('...'); }: 사용자가 없거나 접근 권한이 없으면 예외를 던진다.
    • return next.handle();: 요청을 계속 처리한다.

인터셉터 설정 방법

컨트롤러 설정

// src/app.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
import { CacheInterceptor } from './cache.interceptor';
import { AuthInterceptor } from './auth.interceptor';

@Controller('example')
@UseInterceptors(LoggingInterceptor, TransformInterceptor, CacheInterceptor, AuthInterceptor)
export class AppController {
  @Get()
  findAll() {
    return { message: 'Hello World' };
  }
}

각각의 컨트롤러 별로 사용하려면 @UesIntercaeptors를 통해서 각각 인터셉터들을 설정해준다.

전역 인터셉터 설정 (선택 사항)

만약에 각각 컨트롤러가 아닌 전체적으로 적용을 하고 싶다면 main.ts 파일을 수정해준다.

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
import { CacheInterceptor } from './cache.interceptor';
import { AuthInterceptor } from './auth.interceptor';

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

사용자 객체 설정

권한 검사 인터셉터가 제대로 동작하려면 요청에 사용자 객체를 추가해야 한다. 이를 위해 간단한 미들웨어를 사용한다.

// src/user.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class UserMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    req.user = { hasAccess: true }; // 또는 실제 사용자 데이터를 설정
    next();
  }
}

이 미들웨러를 모듈에 적용한다.

// src/app.module.ts
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { UserMiddleware } from './user.middleware';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(UserMiddleware)
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

기대 결과

1. LoggingInterceptor:

  • 요청과 응답 시간이 로그로 기록된다.

2. TransformInterceptor

  • 응답 데이터가 { statusCode: 200, data: { message: 'Hello World' } } 형식으로 변환한다.

3. CacheInterceptor

  • 같은 요청을 반복하면 캐시된 결과가 반환된다.

4. AuthInterceptor

  • 사용자가 hasAccess 속성을 가지고 있지 않으면 403 Forbidden 에러가 발생한다.
profile
RECORD DEVELOPER

0개의 댓글