transaction

정희준·2023년 5월 7일
0
post-custom-banner

transaction

Transaction은 처리되는 작업의 단위로,

데이터베이스에서의 Transaction 처리는 Business Logic 상 굉장히 중요한 기능입니다.

따라서, 서로 다른 트랜잭션들을 처리하는 도중 하나의 단위 트랜잭션에서 에러가 발생한다면 이전에 성공했던 트랜잭션들을 다시 rollback 시켜 데이터의 Consistency가 깨지지 않도록 해주는 것입니다. 모두 성공했을 경우에는 commit을 통해 확정 지어주게 됩니다.

DB의 Transaction Flow는 간단하게 보면 다음과 같습니다.

1. 서로 다른 Transaction을 부분적으로 처리합니다.

2. 모든 Transaction이 정상적으로 완료되면 Commit 합니다.

3. 만약 Transaction중 하나라도 비정상적으로 처리되면 rollback을 수행합니다.

예제 코드

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { User } from '../users/entities/user.entity';
import {
  PointTransaction,
  POINT_TRANSACTION_STATUS_ENUM,
} from './entities/pointTransaction.entity';
import { IPointsTransactionsServiceCreate } from './interfaces/points-transactions-service.interface';

@Injectable()
export class PointsTransactionsService {
  constructor(
    @InjectRepository(PointTransaction)
    private readonly pointsTransactionsRepository: Repository<PointTransaction>,

    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,

    private readonly dataSource: DataSource,
  ) {}

  async create({
    impUid,
    amount,
    user: _user,
  }: IPointsTransactionsServiceCreate): Promise<any> {
    // this.pointsTransactionsRepository.create(); // 등록을 위한 빈 객체 생성
    // this.pointsTransactionsRepository.insert(); // 결과는 못 받는 등록 방법
    // this.pointsTransactionsRepository.update(); // 결과는 못 받는 수정 방법

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      // 1. PointTransaction 테이블에 거래기록 1줄 생성
      const pointTransaction = this.pointsTransactionsRepository.create({
        impUid,
        amount,
        user: _user,
        status: POINT_TRANSACTION_STATUS_ENUM.PAYMENT,
      });
      // await this.pointsTransactionsRepository.save(pointTransaction);
      await queryRunner.manager.save(pointTransaction);

      // throw new Error('강제로 에러 발생!22');

      // 2. 유저의 돈 찾아오기
      // const user = await this.usersRepository.findOne({
      //   where: { id: _user.id },
      // });
      const user = await queryRunner.manager.findOne(User, {
        where: { id: _user.id },
      });

      // 3. 유저의 돈 업데이트
      const updatedUser = this.usersRepository.create({
        ...user,
        point: user.point + amount,
      });
      await queryRunner.manager.save(updatedUser);
      await queryRunner.commitTransaction();

      // 4. 최종결과 브라우저에 돌려주기
      return pointTransaction;
    } catch (error) {
      await queryRunner.rollbackTransaction();
    } finally {
      await queryRunner.release(); // release가 없으면, commit이 끝나도 커넥션이 안끊겨서 문제가 됨(하지만, 에러나면 자동으로 끊김)
    }
  }
}

Repository는 @InjectRepository 데코레이터로 의존성 주입을 하지만,
DataSource 객체는 이미 TypeOrm Module을 import 한 것만으로도 의존성을 가져올 수 있습니다.
이 의존성을 토대로 createQueryRunner() 함수를 통해 Transaction Manager를 수행할 수 있습니다.

createQueryRunner() 함수로 queryRunner를 선언하고,

connect() 함수를 통해 DB와 연결시켜 준 뒤,

startTransaction() 함수로 Transaction의 시작을 선언해줍니다.

Commit, Rollback을 수동으로 제어할 수 있듯이 Transaction의 시작과 끝 또한 제어할 수 있습니다.

전체 로직을 try-catch-finally 로 감싸주고

Transaction을 처리하는 save method는 repository가 아니라 queryRunner.manager로 대체해 줍니다.

queryRunner.manager 로 바꿔주지 않는다면 Transaction을 시작하더라도 Transaction과 관계 없기에 DB의 오염은 그대로 일어납니다.

Error 없이 모든 로직을 수행하면 Transaction이 완료되어 

commitTransaction() 함수를 호출하여 확정 지어주고,

finally에서 release() 함수를 호출해 Transaction을 종료합니다.

만약 중간에 Error가 발생했을 경우 catch에서 잡아내서 rollbackTransaction() 함수를 통해 rollback을 수행합니다.


트랜잭션의 속성(ACID)에 대한 이해

ACID 는 Transaction을 정의하는 4가지 속성을 가리키는 약어 입니다.

  • A(Atomicity) : 원자성 → 안전성 보장을 위해 가져야 할 성질 중의 하나로 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력.
    • 모두 성공할 것 아니면 모두 실패하게 만드는 것(DB의 오염을 막기 위함).
  • C(Consistency) : 일관성 → 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것.
    • 똑같은 쿼리를 조회할 때마다 동일한 결과값이 나타나야하는 것.
    • 수정과 삭제로 인해 결과값이 달라지는 것은 당연함.
  • I(Isolation) : 격리성 → 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것.
    • A 사람의 요청을 처리하는 동안 B사람의 요청은 잠시 기다리는 것
  • D(Durability) : 지속성 → 성공적으로 수행된 트랜잭션에 대한 로그가 남아야하는 성질로 런타임 오류나 시스템 오류가 발생하더라도, 해당 기록은 영구적이어야 하는 것.
    • 성공하여 commit이 되었으면 서버를 다시 켜도 그 데이터는 그대로 유지가 되어야 되는 것.
profile
같이 일하고 싶은 사람이 되어보자! 다시 시작하는 개발 블로그.
post-custom-banner

0개의 댓글