본문 바로가기
NestJS

[NestJS] Interceptor

by Programmer.Junny 2025. 3. 10.

Interceptor(인터셉터)란? 

인터셉터는 메서드 실행 전후에 추가 로직을 실행할 수 있는 강력한 기능이다. 미들웨어나 가드와는 달리 응답을 변경하거나 예외를 처리하는 등 컨트롤러의 요청/응답 흐름을 세밀하게 제어할 수 있도록 설계되어 있다.

인터셉터 주요기능

  • 요청 전후 처리
    • 인터셉터는 컨트롤러의 메서드가 호출되기 전과 후에 실행되어, 예를 들어 로깅, 캐싱, 응답 변환, 예외 처리 등을 수행할 수 있다.
  • 응답 변경
    • 메서드의 응답 데이터를 변경하거나 추가적인 데이터를 붙이는 등의 작업을 할 수 있다.
  • 예외 처리
    • 메서드에서 발생한 예외를 잡아서, 클라이언트에게 일관된 에러 응답을 전달하는 역할도 수행할 수 있다.
  • 비동기 작업 관리
    • RxJS의 Observable을 활용하여 비동기 작업 흐름을 제어하고, 메서드 실행 후 후속 작업을 추가할 수 있다.

인터셉터를 이용해 로거 구현하기

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

@Injectable()
export class LogInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
        /**
         * 요청이 들어올 때 Req 요청이 들어온 타임스탬프를 찍는다.
         * [REQ] {요청 path} {요청 시간}
         * 
         * 응답 시 다시 타임스탬프를 찍는다.
         * [RES] {요청 path} {응답 시간} {얼마나 걸렸는지 ms}
         * 
         * return 전까지는 인터셉터전 요청에서 실행
         */

        const now = new Date();

        const req = context.switchToHttp().getRequest();

        // /posts
        // /common/image
        const path = req.originalUrl;

        // [REQ] {요청 path} {요청 시간}
        console.log(`[REQ] ${path} ${now.toLocaleString('kr')}`)

        /**
         * return next.handle()을 실행하는 순간
         * 라우트의 로직이 전부 실행되고 응답이 반환된다.
         * observable로 반환
         */
        return next
            .handle()
            .pipe(
                tap(
                    // [RES] {요청 path} {응답 시간} {얼마나 걸렸는지 ms}
                    () => console.log(`[RES] ${path} ${new Date().toLocaleDateString('kr')} ${new Date().getMilliseconds() - now.getMilliseconds()} ms`),
                ),
            );
    }
}

인터셉터는 DI 시스템에 의해 주입될 수 있도록 @Injectable() 그리고 implements NestInterceptor 로 구현이 가능하다.

next.handle().pipe() 전의 로직들은 클라이언트에서 서버로 통신된 후 컨트롤러 메서드가 실행되기 전에 실행된다.

이후에 메서드가 실행된 후 next.handle().pipe()내의 로직이 실행된다.

클라이언트에서 서버로 요청 -> next.handle().pipe() 이전 intercept 내의 로직들 -> 컨트롤러 메서드 실행 -> next.handle().pipe() 내부로직 실행 -> 서버에서 클라이언트로 응답
// 1) GET /posts
  //  모든 posts를 가져온다
  @Get()
  @UseInterceptors(LogInterceptor)
  getPosts(
    @Query() query: PaginatePostDto,
  ) { 
    return this.postsService.paginatePosts(query);
  }

작성된 인터셉터는 원하는 컨트롤러 메서드에 @UseInterceptors(인터셉터) 로 로직을 구현하면 된다.

[REQ] /posts?order__createdAt=DESC&take=1 3/10/2025, 4:00:00 PM
[RES] /posts?order__createdAt=DESC&take=1 3/10/2025 32 ms

위와 같이 요청과 응답 그리고 그 차이를 볼 수 있다.

Transaction Interceptor 생성하기

@Post()
  @UseGuards(AccessTokenGuard)
  async postPosts(
    @User('id') userId: number,
    @Body() body: CreatePostDto,
    // @Body('title') title: string,
    // @Body('content') content: string,
  ) {
    /**
     * 트랜잭션과 관련된 모든 쿼리를 담당할
     * 쿼리 러너를 생성한다.
     */
    const qr = this.dataSource.createQueryRunner();
 
    // 쿼리 러너에 연결한다.
    await qr.connect();
 
    /**
     * 쿼리 러너에서 트랜잭션을 시작한다.
     * 이 시점부터 같은 쿼리 러너를 사용하면, 트랜잭션 안에서 데이터베이스 액션을 실행할 수 있다.
     */
    await qr.startTransaction();
 
    try {
      const post = await this.postsService.createPost(userId, body, qr);
 
      for(let i = 0; i < body.images.length; i++) {
        await this.postsImagesService.createPostImage({
          post,
          order: i,
          path: body.images[i],
          type: ImageModelType.POST_IMAGE,
        }, qr);
      }
 
      await qr.commitTransaction();
      await qr.release();
 
      return this.postsService.getPostById(post.id);
    } catch (error) {
      /**
       * 어떤 에러든 에러가 발생하면,
       * 트랜잭션을 종료하고 원래 상태로 되돌린다.
       */
      await qr.rollbackTransaction();
      await qr.release();
      throw new InternalServerErrorException(error);
    }
  }

기존 post를 생성하는 postPosts 컨트롤러 메서드는 불필요한 QueryRunner까지 가지고 있었다.

이 부분을 인터셉터를 이용해서 수정해보도록 하자.

/* eslint-disable @typescript-eslint/no-misused-promises */
import { CallHandler, ExecutionContext, Injectable, InternalServerErrorException, NestInterceptor } from "@nestjs/common";
import { catchError, Observable, tap } from "rxjs";
import { DataSource } from "typeorm";

@Injectable()
export class TransactionInterceptor implements NestInterceptor {
    constructor(
        private readonly dataSource: DataSource,
    ){}

    async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
        const req = context.switchToHttp().getRequest();
        /**
         * 트랜잭션과 관련된 모든 쿼리를 담당할
         * 쿼리 러너를 생성한다.
         */
        const qr = this.dataSource.createQueryRunner();

        // 쿼리 러너에 연결한다.
        await qr.connect();

        /**
         * 쿼리 러너에서 트랜잭션을 시작한다.
         * 이 시점부터 같은 쿼리 러너를 사용하면, 트랜잭션 안에서 데이터베이스 액션을 실행할 수 있다.
         */
        await qr.startTransaction();

        req.queryRunner = qr;

        return next.handle().pipe(
            catchError(
                async (e) => {
                    await qr.rollbackTransaction();
                    await qr.release();

                    throw new InternalServerErrorException(e.message);
                }
            ),
            tap(async () => {
                await qr.commitTransaction();
                await qr.release();
            })
        )
    }
}

인터셉터에 적용할 TransactionInterceptor를 위와 같이 구현하였다.

QueryRunner를 연결하고 실행하는 부분은 어느 로직에서든 공통이며, 이후 메서드가 실행이 완료된 후 Response하기 전에 next.handle().pipe()내의 로직이 실행된다.

그 안에는 트랜잭션을 롤백(rollback)하거나 커밋(commit)하는 로직을 작성할 수 있다.

QueryRunner 커스텀 데코레이터 만들기 & Transaction Interceptor 적용하기

Transaction Interceptor 적용하기

@Post()
@UseInterceptors(TransactionInterceptor)
@UseGuards(AccessTokenGuard)
async postPosts(
...
) {
...
}

우선 구현한 TransactionInterceptor를 적용한다. 

req.queryRunner = qr;

TransactionInterceptor에서 QueryRunner를 받아 Req 요청객체에 프로토타입을 만들어 적용시켰는데, 이것을 postPosts 컨트롤러 메서드에서 받아와야할 필요가 있다.

QueryRunner 커스텀 데코레이터 만들기

import { createParamDecorator, ExecutionContext, InternalServerErrorException } from "@nestjs/common";
import { QueryRunner } from "typeorm";

export const QueryRunnerDecorator = createParamDecorator((data, context: ExecutionContext) => {
    const req = context.switchToHttp().getRequest();

    if(!req.queryRunner) {
        throw new InternalServerErrorException(
            `QueryRunner Decorator를 사용하려면 TransactionInterceptor를 적용해야 합니다.`
        );
    }

    return req.queryRunner as QueryRunner;
})

커스텀 데코레이터는 User 데코레이터를 만들었을 때를 떠올리면 된다.

반환을 TransactionInterceptor에서 작성한 req.queryRunner 를 하면 된다.

@Post()
  @UseInterceptors(TransactionInterceptor)
  @UseGuards(AccessTokenGuard)
  async postPosts(
    @User('id') userId: number,
    @Body() body: CreatePostDto,
    @QueryRunnerDecorator() qr: QueryRunner,
    // @Body('title') title: string,
    // @Body('content') content: string,
  ) {
  ...
  }

위와 같이 적용한다.

완성된 postPosts 메서드

 // 3) POST /posts
  //  post를 생성한다.
  //
  // DTO - Data Transfer Object (데이터 전송 객체)
  @Post()
  @UseInterceptors(TransactionInterceptor)
  @UseGuards(AccessTokenGuard)
  async postPosts(
    @User('id') userId: number,
    @Body() body: CreatePostDto,
    @QueryRunnerDecorator() qr: QueryRunner,
    // @Body('title') title: string,
    // @Body('content') content: string,
  ) {
      const post = await this.postsService.createPost(userId, body, qr);

      for(let i = 0; i < body.images.length; i++) {
        await this.postsImagesService.createPostImage({
          post,
          order: i,
          path: body.images[i],
          type: ImageModelType.POST_IMAGE,
        }, qr);
      }

      return this.postsService.getPostById(post.id, qr);
  }

이전과는 다르게 QueryRunner 부분이 없어져 굉장히 깔끔한 컨트롤러 메서드가 되었다.

'NestJS' 카테고리의 다른 글

[NestJS] Middleware  (0) 2025.03.11
[NestJS] Exception Filter  (0) 2025.03.10
[NestJS] Transaction  (0) 2025.03.10
[NestJS] 파일 업로드 (선 업로드 방식)  (0) 2025.03.07
[NestJS] 파일 업로드 (클래식 방식)  (0) 2025.03.07

최근댓글

최근글

skin by © 2024 ttuttak