DAY27

yejichoi·2022년 12월 15일
0
post-thumbnail

Algorithm study

Backend Class

Transaction

Transaction은 처리되는 작업의 단위로, 데이터베이스에서의 Transaction 처리는 Business Logic 상 굉장히 중요한 기능 => 서로 다른 트랜잭션들을 처리하는 도중 하나의 단위 트랜잭션에서 에러가 발생한다면 이전에 성공했던 트랜잭션들을 다시 rollback 시켜 데이터의 Consistency가 깨지지 않도록 해줘야 함 => 성공했을 경우에는 commit

⭕️ 데이터 오염을 해결하기 위해 ACID 트랜잭션 사용

DB의 Transaction Flow
1. 서로 다른 Transaction을 부분적으로 처리
2. 모든 Transaction이 정상적으로 완료되면 Commit
3. 만약 Transaction중 하나라도 비정상적으로 처리되면 rollback을 수행

// pointTransaction.service.ts


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';

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

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

    private readonly dataSource: DataSource,
  ) {}

  async create({ impUid, amount, user: _user }) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();

    // ========================= transaction 시작!!! ========================
    await queryRunner.startTransaction();
    // =====================================================================

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

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

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

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

      // ========================= commit 성공 확정!!! =========================
      await queryRunner.commitTransaction();
      // =====================================================================

      // 4. 최종결과 프론트엔드에 돌려주기
      return pointTransaction;
    } catch (error) {
      // ========================= rollback 되돌리기!!! =========================
      await queryRunner.rollbackTransaction();
      // =====================================================================
    } finally {
      // ========================= 연결 해제!!! =========================
      await queryRunner.release();
      // ==============================================================
    }
  }
}

createQueryRunner() queryRunner를 선언
connect() DB와 연결
startTransaction() Transaction의 시작 선언
Transaction을 처리하는 save method는 repository가 아니라 queryRunner.manager로 대체
=> queryRunner.manager 로 바꿔주지 않는다면 Transaction을 시작하더라도 Transaction과 관계 없기에 DB의 오염 발생
Error 없이 모든 로직을 수행하면 commitTransaction() 호출
finally에서 release() 함수를 호출해 Transaction을 종료
중간에 Error가 발생했을 경우 catch에서 잡아내서 rollbackTransaction() 수행
release() Transaction을 종료

ACID

Transaction 을 정의하는 4가지 속성

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

Isolation-level

Transaction의 격리 수준

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE
    (*아래 단계로 내려갈수록 안전해지는 반면 성능이 느려짐)

Dirty Read : 트랜잭션이 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있게 되는 현상
NON REPEATABLE READ : 동일한 결과값이 나타나지 않을 때 나타나는 현상
- 한 트랜잭션 내에서 같은 쿼리를 두번 수행할 때, 그 사이에 다른 트랜잭션이 값을 수정 또는 삭제함으로써 두 쿼리가 상이하게 나타나는 현상 => 동일한 쿼리를 요청하면 매번 동일한 결과값이 나타나야 함.

1단계 : READ UNCOMMITTED

각 트랜잭션에서의 변경 내용이 COMMIT이나 ROLLBACK 여부에 상관 없이 다른 트랜잭션에서 값을 읽을 수 있음 => commit 되지 않는 데이터들을 조회할 수 있으나, 정합성에 문제가 많은 격리 수준이기 때문에 사용하지 않는 것을 권장!

2단계 : READ COMMITTED

  • RDB(관계형 데이터베이스)에서 대부분 기본적으로 사용되고 있는 격리 수준
  • Dirty Read 발생 X
  • 실제 테이블 값을 가져오는 것이 아니라 Undo 영역에 백업된 레코드에서 값을 가져옴

3단계 : REPEATABLE READ

RDB(관계형 데이터베이스)에서 대부분 기본적으로 사용되고 있는 격리 수준

MySQL에서는 트랜잭션마다 트랜잭션 ID를 부여하여 트랜잭션 ID보다 작은 트랜잭션 번호에서 변경한 것만 읽게됨

Undo 공간에 백업해두고 실제 레코드 값을 변경

  • 백업된 데이터는 불필요하다고 판단하는 시점에 주기적으로 삭제
  • Undo에 백업된 레코드가 많아지면 MySQL 서버의 처리 성능이 떨어질 수 있음

=> 이러한 변경방식은 MVCC(Multi Version Concurrency Control)라고 부름

PHANTOM READ : 종종 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상

4단계 : SERIALIZABLE

 async findAll() {
    const queryRunner = await this.connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction('SERIALIZABLE');
    try {
      // 조회시 락을 걸고 조회함으로써, 다른 쿼리에서 조회 못하게 막음(대기시킴) => Select ~ For Update
      const payment = await queryRunner.manager.find(Payment, {
        //PHANTOM READ 가 발생하지 않기 위해서 lock을 걸어줌 
        lock: { mode: 'pessimistic_write' },
      });
      console.log(payment);
      

HW

0개의 댓글