[Nestjs][TIL][개인과제] Nestjs + TypeORM | 공연 예매 사이트 만들기 - Transaction 적용하기

Trippy·2023년 12월 30일
0

Nest.js

목록 보기
7/16
post-thumbnail

Transaction

프로젝트를 진행하면서 한 번의 post request가 들어왔을 때 다수의 엔티티 인스턴스를 생성 혹은 수정해야 하는 일이 생길 수 있다. 이럴 때에는 트랜잭션 단위로 묶어서 처리 해줘야 하는데, TypeORM에서는 어떻게 트랜잭션 처리를 할 수 있도록 제공하는지 알아보자.

트랜잭션(Transaction)이란?

트랜잭션이란 데이터베이스 상태를 변경시키기 위해 수행하는 하나의 작업 단위 이다.

예를 들어 물건을 구매하는 결제 과정 상황을 생각해 보자.
사용자는 구매하려는 물건을 담고, 구매하려는 물건의 개수를 정하고, 결제를 진행하여 구매 버튼을 누르면, 사용자의 잔액을 확인하고, 결제가 진행되며, 물건의 재고 상황을 확인한 뒤 구매 내역을 생성하는 로직이 진행되어야 한다.
이 때 몇 가지의 과정 중 결제만 되어서도 안되고, 구매 내역만 생성되어서도 안되며 일련의 모든 과정이 함께 동작되어야 한다. 이러한 작업 단위 하나를 트랜잭션이라고 한다.

이렇게 전부 완료하거나, 하나도 완료되지 않아야 한다는 것이 트랜잭션의 중요한 특성이다.


트랜잭션 처리 방법

1) 트랜잭션 생성 및 사용

TypeORM에서는 트랜잭션을 DataSource 혹은 EntityManager를 통해 만들 수 있으며, 콜백함수를 실행하여 원하는 동작을 처리할 수 있도록 제공하고 있다.

await myDataSource.manager.transaction(async (transactionalEntityManager) => {
  await transactionalEntityManager.save(users);
  await transactionalEntityManager.save(photos);
  // ...
});

2) queryRunner를 이용한 트랜잭션 제어

queryRunner는 single database connetion을 제공하기 때문에 트랜잭션 제어가 가능하다. 좀 더 세부적으로 직접 트랜잭션을 제어하고 싶을 때에는 QueryRunner를 사용하면 된다.

// queryRunner 생성!!
const queryRunner = dataSource.createQueryRunner();

// 새로운 queryRunner를 연결한다.
await queryRunner.connect();

// 생성한 쿼리러너를 통해 쿼리문을 날리는 것도 가능하다.
await queryRunner.query('SELECT * FROM users');

// 새로운 트랜잭션을 시작한다는 의미의 코드이다.
await queryRunner.startTransaction();

try {
  // 원하는 트랜잭션 동작을 정의하면 된다!
  await queryRunner.manager.save(user1);
  await queryRunner.manager.save(user2);

  // 모든 동작이 정상적으로 수행되었을 경우 커밋을 수행한다.
  await queryRunner.commitTransaction();
} catch (err) {
  // 동작 중간에 에러가 발생할 경우엔 롤백을 수행한다.
  await queryRunner.rollbackTransaction();
} finally {
  // queryRunner는 생성한 뒤 반드시 release 해줘야한다.
  await queryRunner.release();
}

3) 공연 등록에 적용해 보는 트랜잭션

@Injectable()
export class PerformanceService {
  constructor(
    @InjectRepository(Performance)
    private performanceRepository: Repository<Performance>,
    @InjectRepository(Schedule)
    private scheduleRepository: Repository<Schedule>,
    private dataSource: DataSource, // DataSource 주입
  ) {}

  // 공연 등록
  async create(
    createPerformanceDto: CreatePerformanceDto,
    createScheduleDto: CreateScheduleDto,
  ) {
    // Transaction
    // DataSource에서 새로운 쿼리 러너 생성
    const queryRunner = this.dataSource.createQueryRunner();
    // 쿼리 러너를 사용하여 데이터베이스에 연결
    await queryRunner.connect();
    // 쿼리 러너를 사용하여 새 트랜잭션 시작
    await queryRunner.startTransaction();

    // performance 테이블 작성
    try {
      // Performance 테이블에 데이터 저장
      const post = await this.performanceRepository.save(createPerformanceDto);
      // 새로 저장된 performanceId 가져오기
      const id: any = post.performanceId;
      console.log('나오나?', id);

      // schedule 테이블 작성,저장
      const { startTime, endTime } = createScheduleDto;
      await this.scheduleRepository.save({
        performance: id,
        startTime,
        endTime,
      });

      // 모든 작업이 성공적으로 수행되면 트랜잭션을 커밋
      await queryRunner.commitTransaction();
      // 성공적으로 등록된 데이터 반환
      return { post };
    } catch (error) {
      // 작업 중 오류가 발생하면 트랜잭션을 롤백하여 데이터 일관성을 유지
      await queryRunner.rollbackTransaction();
    } finally {
      // 트랜잭션이 완료되거나 롤백된 후에 쿼리 러너를 해제
      await queryRunner.release();
    }
  }
}

궁금한 점!

release는 왜 필요한가?

release의 사전적 의미 : 해제하다, 방출하다, 공개하다

  • 리소스 정리 : 트랜잭션이 완료되거나 오류가 발생한 경우, queryRunner를 해제하면 데이터베이스 연결이 적절하게 종료되어, 리소스를 해제하고 잠재적인 메모리 누수를 방지한다.

  • 연결 누수 방지 : queryRunner를 해제하지 않으면 연결 누수가 발생할 수 있다. 연결이 적절히 해제되지 않으면 연결 풀이 고갈되어 애플리케이션이 사용 가능한 연결을 소진하고, 성능에 영향을 미치거나 충돌을 일으킬 수 있다.

profile
감금 당하고 개발만 하고 싶어요

0개의 댓글