본문 바로가기
NestJS & Fastify

[NestJS] Interceptor를 사용해서 Transaction 분리하기

by yhames 2025. 3. 16.
728x90

 

Transaction 이란?

데이터베이스의 여러 테이블의 값을 수정할 때 일관성을 유지하기 위해서 트랜잭션 단위로 처리

 

QueryRunner

TypeORM에서 트랜잭션 처리를 위해서는 QueryRunner를 사용합니다.

 

QueryRunner는 데이터베이스 트랜잭션과 쿼리 실행을 관리하는 객체로, 여러 데이터베이스 쿼리를 하나의 트랜잭션 내에서 실행할 수 있도록 도와줍니다. 보통 복잡한 트랜잭션을 다룰 때 유용하게 사용되며, 여러 쿼리를 안전하게 묶어 실행할 수 있도록 돕습니다.

 

import { getManager } from "typeorm";
import { User } from "./entity/User";
import { Post } from "./entity/Post";

async function example() {
  const queryRunner = getManager().connection.createQueryRunner();

  // 트랜잭션 시작
  await queryRunner.startTransaction();

  try {
    // 첫 번째 쿼리: 사용자 생성
    const user = await queryRunner.manager.save(User, { name: "Alice" });

    // 두 번째 쿼리: 게시글 생성
    const post = await queryRunner.manager.save(Post, { title: "Hello", user });

    // 트랜잭션 커밋
    await queryRunner.commitTransaction();
  } catch (error) {
    // 오류 발생 시 롤백
    await queryRunner.rollbackTransaction();
    console.error(error);
  } finally {
    // 쿼리 러너 해제
    await queryRunner.release();
  }
}

 

Interceptor로 분리하기

QueryRunner를 사용할때마다 가져와서 사용하는 것은 비효율적이고 코드가 중복됩니다. 이를 인터셉터로 분리하면 중복을 줄일 수 있습니다.

transaction.Interceptor.ts

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

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

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<unknown>> {
    const req = context.switchToHttp().getRequest();
    const queryRunner = this.dataSource.createQueryRunner();

    await queryRunner.connect();
    await queryRunner.startTransaction();
    req.queryRunner = queryRunner;

    return next.handle().pipe(
      catchError(async (err) => {
        await queryRunner.rollbackTransaction();
        await queryRunner.release();
        throw err;
      }),
      tap(async () => {
        await queryRunner.commitTransaction();
        await queryRunner.release();
      }),
    );
  }
}

 

decorator를 활용하여 @Runner를 사용하기

@Req는 보다는 명시적으로 @Runner라는 데코레이터를 만들어서 사용하면 가독성이 좋아집니다.

transaction.decorator.ts

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

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

    if (!req.queryRunner) {
      throw new InternalServerErrorException(
        "No TransactionInterceptor not found",
      );
    }

    return req.queryRunner;
  },
);

export default Runner;

 

사용 예시

class postsController {

    @Post()
    @UseInterceptors(TransactionInterceptor)    // 👈 TransactionInterceptor 사용하도록 명시
    async postPosts(@Body() body: CreatePostDto, @Runner() qr: QueryRunner) {	// 👈 QueryRunner를 가져온다. TransactionInterceptor를 사용하지 않으면 오류가 발생한다.
        const post = await this.postsService.createPost(userId, body, qr);
        return post;
    }
}
class postsService {

    getRepository(qr?: QueryRunner) {
        if (!qr) {		// 👈 qr이 없으면 주입받은 this.postsRepository를 사용하고
            return this.postsRepository;
        }
        return qr.manager.getRepository<PostsModel>(PostsModel);    // 👈 qr이 있으면 qr.manager에서 Repository를 사용한다.
    }

    async createPost(userId: string, body: CreatePostDto, qr?: QueryRunner) {    // 👈 QueryRunner를 nullable로 받는다.
        const repository = this.getRepository(qr);    // 👈 qr을 통해 Repository를 가져온다.
        const post = repository.create({ ...body, user });

        return repository.save(post);
    }
}
반응형

'NestJS & Fastify' 카테고리의 다른 글

[Fastify] Prisma에서 Transaction 추상화  (0) 2025.03.16