본문 바로가기
개발/Node.js, TypeScript

[NestJS] Prisma Transaction

by mixxeo(믹서) 2025. 3. 2.

들어가며

오늘은 NestJS + Prisma 조합의 애플리케이션에서 데이터베이스 트랜잭션(Transaction)을 구현하는 방법을 알아보겠습니다. 데이터베이스 트랜잭션은 DBMS에서 사용되는 더 이상 쪼갤 수 없는 작업의 최소 단위입니다.

예를 들어 주문-결제 로직이 상품 수량 업데이트 → 유저 정보 업데이트 → PG API 호출 → 결제 상태 업데이트 이렇게 작성되어있는데, PG API 호출 단계에서 예외가 발생해 종료되었다고 했을 때 아무런 처리가 되어있지 않다면 데이터 일관성이 깨지게됩니다. (상품, 유저 테이블은 결제 완료 처리가 반영되었는데 결제 테이블은 반영되지 않음)

이런 경우 상품, 유저, 결제 테이블 업데이트를 하나의 트랜잭션으로 묶어서 처리할 수 있습니다. 이 글에서는 nestjs-cls/transactional 플러그인을 사용하여 Prisma에서 트랜잭션을 구현하는 방법에 대해 알아봅니다.

💡 CLS(Continuation Local Storage, Context Local Storage)
CLS는 비동기 코드에서 특정 컨텍스트를 유지할 수 있도록 하는 방법을 의미합니다. 즉, Context Local Storage는 비동기 실행 중에서도 특정 데이터를 유지하고 공유할 수 있도록 하는 저장소이며, nestjs-cls도 CLS 기반으로 동작하는 라이브러리입니다.

 

 

 

Prisma Transaction 적용

nestjs-cls/transactional 플러그인

Transactional 플러그인은 CLS 기반의 트랜잭션을 쉽게 사용할 수 있도록 도와주는 라이브러리입니다. 이를 사용하면 함수 호출을 CLS 컨텍스트에 저장된 트랜잭션 내에서 실행할 수 있으며 같은 요청 내의 다른 서비스에서도 동일한 트랜잭션을 공유할 수 있습니다. 특정 ORM에 종속되어있지 않고, Prisma와 함께 사용하기 위해 Prisma adapter를 함께 설치합니다.

 

 

사용 방법

설치

npm install @nestjs-cls/transactional @nestjs-cls/transactional-adapter-prisma

 

모듈 설정

nestjs-cls로 Transactional 플러그인을 등록하기 위해, ClsModule.forRoot에 plugins 배열을 전달합니다.

이렇게 하면 ClsPluginTransactional이 DI 컨테이너에 등록되고, TransactionalAdapterPrisma가 Prisma와 연동되어 CLS에서 트랜잭션을 관리할 수 있게 됩니다.

// database.module.ts
import { ClsModule } from 'nestjs-cls';
import { ClsPluginTransactional } from '@nestjs-cls/transactional';
import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';

@Global()
@Module({
  imports: [
    ClsModule.forRoot({
      plugins: [
        new ClsPluginTransactional({
          imports: [PrismaModule],
          adapter: new TransactionalAdapterPrisma({
            prismaInjectionToken: PrismaClient,
          }),
        }),
      ],
    }),
  ],
  // ...
})
export class DatabaseModule {}

 

플러그인이 등록되면 TransactionHost를 전역적으로 사용할 수 있게됩니다. TransactionHost는 현재 실행 중인 트랜잭션을 CLS에 저장하고 서비스 간 트랜잭션을 공유할 수 있도록 관리하는 역할을 하는 객체입니다.

 

TransactionHost 인스턴스 사용

저희 팀은 Repository 클래스에서 TransactionHost를 주입받아 사용하고 있습니다.

// payment.repository.ts

@Injectable()
export class PaymentRepository {
  constructor(
    private readonly prisma: TransactionHost<TransactionalAdapterPrisma<PrismaClient>>
  ) {}
  
  async create(data: { accountId: number, amount: number }) {
    return this.prisma.tx.payment.create({ data });
  }
}

 

공식 문서에서는 prisma:TransactionHost<TransactionalAdapterPrisma> 타입으로 TransactionHost를 사용하는데, 이 방식에서는 트랜잭션이 없으면 prisma.tx를 사용할 수 없고 PrismaClient를 별도로 사용해야합니다. 반면, 위의 예시처럼 TransactionHost<TransactionalAdapterPrisma<PrismaClient>> PrismaClient를 주입하면 트랜잭션을 사용하지 않을 때도 prisma.tx를 사용할 수 있어 코드를 더 직관적이고 일관적으로 작성할 수 있습니다.

 

Transactional 데코레이터 사용

Transactional 데코레이터를 메서드에 붙이면, 암시적으로 메서드를 withTransaction으로 감싸게됩니다. 그래서 TransactionHost가 자동으로 트랜잭션을 공유하게됩니다.

@Injectable()
export class PaymentService {
  constructor(
    private readonly paymentRepository: PaymentRepository,
    private readonly productService: ProductService,
    private readonly userService: UserService,
  ) {}
  
  @Transactional() // 트랜잭션 시작
  async completePayment(paymentId: number) {
      // ...
    await this.productService.update();
    await this.userService.update();

		await this.updatePayment(paymentId);
  }

  // 중첩 트랜잭션은 추가할 필요 없음
  private async completePayment(paymentId: number) {
    
    // ...
    await this.paymentRepository.updateStatus()
  }
}

 

 

 

마치며

오늘은 Transactional 라이브러리와 Prisma Adapter를 사용하여 NestJS에서 데이터베이트 트랜잭션을 구현하는 방법에 대해 알아보았습니다. 처음에는 TransactionHost의 역할이나 Transactional 데코레이터의 사용법을 잘 모르고 개발을 했었는데, 작동 원리와 역할에 대해 공부를 하고나니 좀 더 정확한 방식으로 사용하고 로직을 작성할 수 있을 것 같습니다. prisma.$transactional을 사용하는 등 다른 방식도 있지만, Transactional 라이브러리는 데코레이터를 사용해 코드를 간결하게 작성할 수 있고 자동으로 서비스간 트랜잭션을 공유할 수 있다는 장점이 있어 도입해보시는 것을 추천합니다.