본문 바로가기

외부 활동/immersion

TypeOrm 트랙잭션의 적용

https://velog.io/@ljh305/Transaction-ACID-Lock-qsm3wctj

 

Transaction / ACID / Lock

서비스에서 가장 치명적인 문제는 데이터의 오염이다.중요한 데이터를 오염시키지 않기 위해트랜잭션을 만들어 성공했을때는 모두 성공을 실패했을 때는 롤백 시켜주어야 한다.typeorm에서는 트

velog.io

https://itchallenger.tistory.com/231

 

TypeORM 스터디 : QueryBuilder 2편 - CRUD 심화

1편 보기 TypeORM 스터디 : QueryBuilder 1편 - CRUD 기본 TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases. Works in NodeJS, Browser, Ionic, Cord

itchallenger.tistory.com

 

https://suhwan.dev/2019/06/09/transaction-isolation-level-and-lock/

 

Lock으로 이해하는 Transaction의 Isolation Level

개요 내게 transaction의 isolation level은 개발할 때 항상 큰 찝찝함을 남기게 하는 요소였다. row를 읽기만 할 때는 REPEATABLE READ로, row를 삽입 / 수정 / 삭제할 때는 SERIALIZABLE로 isolation level을 지정했지

suhwan.dev

https://hwannny.tistory.com/81

 

비관적 잠금(선점 잠금, Pessimistic Lock)과 낙관적 잠금(비선점 잠금, Optimistic Lock)

들어가며 최근 DDD Start! 라는 DDD 관련 서적을 읽다가 비관적, 낙관적 잠금에 대한 내용이 나왔다. 애그리거트를 수정, 조회시 멀티스레드 환경에서 발생되는 문제를 다루는 내용이다. 해당 문제

hwannny.tistory.com

위 4 분의 블로그를 참조

https://typeorm.io/

 

TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server,

 

typeorm.io

 

 

 

ORM

Object-Relational Mapping의 약자로, 객체 지향 프로그래밍 언어와 관계형 데이터베이스 간의 중개 역할을 하는 소프트웨어이다.

ORM은 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 테이블 간의 매핑을 제공한다.

이를 통해 개발자는 객체 지향 프로그래밍 언어의 객체를 사용하여 관계형 데이터베이스의 데이터에 접근하고 조작할 수 있다.

 

1. 객체와 테이블 간 매핑: ORM은 데이터베이스의 테이블과 객체 지향 프로그래밍 언어의 클래스를 매핑한다.

즉, 테이블의 각 컬럼은 객체의 속성으로, 테이블의 각 레코드(행)는 객체의 인스턴스(클래스 기반 실체화된 개체)로 대응다.

2. 쿼리 대신 메서드 사용: ORM을 사용하면 SQL 쿼리를 직접 작성하지 않고도 메서드를 통해 데이터베이스에 접근할 수 있습니다. 개발자는 객체의 메서드를 호출하고 데이터를 조작하는 방식으로 데이터베이스와 상호작용할 수 있습니다.

3. 데이터베이스 독립성: ORM을 사용하면 데이터베이스의 종류에 상관없이 동일한 코드를 사용하여 다양한 데이터베이스 시스템과 함께 작동할 수 있습니다. ORM은 데이터베이스 시스템의 종속성을 줄여주며, 데이터베이스 변경에 더욱 유연하게 대응할 수 있게 합니다.

4. 관계 매핑: ORM은 관계형 데이터베이스의 관계를 객체 지향 프로그래밍 언어의 관계로 표현할 수 있습니다. 예를 들어, 외래 키를 사용하여 테이블 간의 관계를 매핑할 수 있습니다.

 

TypeORM

자바스크립트 ORM으로, 관계형 데이터베이스와 자바스크립트 객체 간의 매핑을 제공한다.

TypeORM은 타입스크립트로 작성되어 있으며, 타입스크립트의 강력한 타입 체크 기능을 사용하여 개발자의 실수를 방지할 수 있다.

 

 

 

Query Builder

==>  복잡한 쿼리를 함수형으로 작성할 수 있게 해준다

 

현재 Immersion에서는

nest 프레임 워크를 사용하여  주로 왼쪽과 같이

레포지토리를 사용해서 DB로 접근한다.

 

 

 

쿼리 빌더 타입

이외에도 여러 특이한 타입이 있다. ex) relationQueryBuilder

주로 일대일, 다대일, 다대다 같은 관계에 따라 사용법이 달라 차후에 이 부분은 따로 공부를 해야할 것같다.

 

 

Where 사용

.where을 두번 쓰면 뒤의 where이 무시된다.

Having 사용

.having을 두번 쓰면 앞의 조건이 무시된다.

 

 

Limit 사용

페이지네이션 사용시에는 skip을 쓰는게 좋다.

 

 

페이지네이션 사용

take와 skip은 limit과 offset을 사용하는 것처럼 보일 수 있지만 실제로는 그렇지 않다.

limit과 offset은 조인 또는 하위 쿼리가 포함된 더 복잡한 쿼리가 있는 경우 예상대로 작동하지 않을 수 있다.

take와 skip을 사용하면 이러한 문제를 방지할 수 있습니다.

 

차후 게시판 페이지네이션을 구현하기 이전에 미리 더 실습과 공부를 하고 다시 봐야할 부분인것 같다.

 

 

 

 

트랜잭션

  => 하나 이상의 작업들을 논리적인 작업 단위로 묶어서 데이터베이스에 대한 일련의 연산들을 수행하는 것

 

 

트랜잭션은 원자성, 일관성, 지속성, 격리성이라는 네 가지 속성을 갖추어야 합니다.


원자성: 트랜잭션은 성공적으로 완료되거나 실패해야 합니다. 중간에 중단되면 데이터베이스는 변경되지 않습니다.
일관성: 트랜잭션이 완료되면 데이터베이스는 일관된 상태에 있어야 합니다.

 즉, 트랜잭션이 시작되기 전에 데이터베이스에 존재했던 모든 제약 조건을 충족해야 합니다.
지속성: 트랜잭션이 완료되면 변경 사항은 영구적으로 데이터베이스에 저장되어야 합니다.
격리성: 트랜잭션이 실행되는 동안 다른 트랜잭션은 트랜잭션의 영향을 받지 않아야 합니다. 

즉, 트랜잭션이 실행되는 동안 다른 트랜잭션은 트랜잭션이 완료될 때까지 데이터베이스의 변경 사항을 볼 수 없습니다.

 

 

 

트랜잭션의 격리 수준 ==> 트랜잭션이 다른 트랜잭션의 변경 사항을 볼 수 있는 정도를 결정.

DIRTY READ현상 :   트랜잭션이 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있게 되는 현상

pantom read 현상 : 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상(쓰기잠금을 걸어야함)


read uncommitted : 가장 낮은 격리 수준.   (dirty read현상 발생)

트랜잭션은 다른 트랜잭션의 변경 사항을 볼 수 있다.

즉, 트랜잭션은 아직 커밋되지 않은 변경 사항도 볼 수 있다.

성능이 가장 좋지만, 데이터베이스의 일관성이 가장 낮다.

COMMIT이나 ROLLBACK 여부에 상관 없이 다른 트랜잭션에서 값을 읽을 수 있다  

 

 

read committed : 중간 격리 수준    (dirty read현상 발생X)

 

 

트랜잭션은 다른 트랜잭션의 커밋된 변경 사항만 볼 수 있다.

즉, 트랜잭션은 아직 커밋되지 않은 변경 사항은 볼 수 없다.

성능과 데이터베이스의 일관성 사이에서 적절한 균형을 제공합니다.

RDB에서 대부분 기본적으로 사용되고 있는 격리 수준

실제 테이블 값을 가져오는 것이 아니라 Undo 영역에 백업된 레코드에서 값을 가져온다

 

 


repeatable read : 높은 격리 수준.   (pantom read 현상 발생)

트랜잭션은 같은 데이터를 두 번 읽어도 같은 결과를 얻을 수 있다.

즉, 다른 트랜잭션이 해당 데이터를 변경하더라도 트랜잭션은 변경된 데이터를 볼 수 없다.

데이터베이스의 일관성이 가장 높지만, 성능이 가장 낮다.

pantom read 현상 : 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상(쓰기잠금을 걸어야함)

 


serializable : 가장 높은 격리 수준.

트랜잭션은 다른 트랜잭션과 동시에 실행되지 않는다.

즉, 트랜잭션은 다른 트랜잭션의 변경 사항을 볼 수 없다.

데이터베이스의 일관성이 가장 높지만, 성능이 가장 낮다.

거의 사용X


트랜잭션의 격리 수준은 데이터베이스의 특정 요구 사항에 따라 선택해야 한다.

성능이 중요하다면 낮은 격리 수준을 선택해야 한다. 

 

 

 

 

 

Typeorm에서의 트랜잭션

1.connection or  EnitityManager의 transation함수 사용하기(JPA)

 

 

2.queryRunner(nest,JPA)

 

3.데코레이터 사용하기(JPA,NEST)

 

 

NEST에서는 트랜잭션 인터셉터를 생성하여 커스텀 데코레이터 생성 후 컨트롤러에 적용하는 법도 괜찮을듯

 

비관적 잠금(선점 잠금선점 잠금, Pessimistic Lock)

어떤 스레드가 주문 정보를 먼저 구했다면 주문 정보를 통해 어떠한 기능의 수행이 끝나기 전까지는 다른 스레드들이 주문정보를 구하지 못하도록 막는, 잠그는 방식

비관적 잠슴 시 교착 상태가 발생 하여 두 스레드 모두 영원히 작업을 끝낼수 없게 되는 경우가 발생한다.

이를 방지하지 위해 최대 잠금 시간을 지정해 주어야한다.

 

낙관적 잠금(비선점 잠금, Optimistic Lock)

비관적 잠금과는 달리 실제 잠금(선점)을 하지 않는다.

@Version 및 @UpdatedDate 데코레이터를 포함시켜 선점(잠금) 없이 트랜잭션 충돌을 방지 할 수 있다

 

TypeORM은 트랜잭션 시 기본적으로 낙관적 잠금을 사용한다. 

낙관적 잠금은 데이터를 읽은 후 잠금을 설정하는 기법이며 잠금이 설정되지 않으면 다른 트랜잭션이 해당 데이터를 읽거나 수정할 수 있다. 

낙관적 잠금은 성능을 향상시키는 데 효과적이지만, 데이터의 일관성을 보장하지 않을 수 있다.

TypeORM은 트랜잭션 시 강제 잠금을 사용하도록 설정할 수도 있다.

강제 잠금은 데이터를 읽기 전에 잠금을 설정하는 기법이며 잠금이 설정되면 다른 트랜잭션이 해당 데이터를 읽거나 수정할 수 없다.

강제 잠금은 데이터의 일관성을 보장하는 데 효과적이지만, 성능을 저하시키는 단점이 있다.

// typeorm.config.js

typeorm.configure({
  ...
  lockMode: "optimistic", // default
  // lockMode: "pessimistic",
});

 

강제 잠금을 사용하면 데이터의 일관성이 보장되지만, 성능이 저하될 수 있습니다. 따라서, 성능과 데이터의 일관성 사이에서 적절한 균형을 찾는 것이 중요합니다.

 

 

subquery

 차후 코딩을하며 현재 알고 있는 메소드와 쿼리로 데이터를 뽑지 못할떄 더 찾아 보자.

 

soft delete

데이터를 물리적으로 삭제하지 않고, 데이터의 상태를 삭제된 것으로 표시하는 방법.

soft delete를 사용하면 데이터를 완전히 삭제하지 않고도 데이터를 숨길 수 있다.

데이터를 영구적으로 삭제하지 않고 보관해야 하는 경우
데이터를 다시 사용할 수 있도록 해야 하는 경우
데이터를 분석해야 하는 경우

 

데이터베이스에서 soft delete를 지원하는 기능을 사용하는 방법

1. 라이브러리 사용 2. 직접 코딩 3. 데이터베이스에서 지원하는 soft delete기능 사용

typeorm에서의 soft delete

soft delete를 수행할 엔티티에 softDelete 속성을 추가.
softDelete 속성에 true 값을 지정 후 엔티티를 삭제.

@Entity()
export class User {

  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  email: string;

  @Column()
  password: string;

  @Column()
  softDelete: boolean = false;

}

 

const user = await repository.findOne(1);

user.softDelete = true;

await repository.save(user);

softDelete 속성이 true로 설정된 엔티티는 데이터베이스에서 물리적으로 삭제되지 않고, deleted_at 필드에 삭제 날짜가 설정

 

const user = await repository.findOne(1);

user.softDelete = false;

await repository.save(user);

 User 엔티티의 softDelete 속성을 false로 설정하고, 엔티티를 저장한다.

 softDelete 속성이 false로 설정된 엔티티는 데이터베이스에서 복구된다.

 

쿼리 빌더과 쿼리 러너

쿼리 빌더는 쿼리를 생성하는 도구이다. 

쿼리 빌더를 사용하면 SQL 쿼리를 작성하지 않고도 쿼리를 생성할 수 있다.

[빌더의 장점]

SQL 쿼리를 작성하지 않아도 쿼리를 생성할 수 있다.
쿼리 빌더를 사용하여 쿼리를 재사용할 수 있다.
쿼리 빌더를 사용하여 쿼리를 동적으로 생성할 수 있다.

 

쿼리 러너는 쿼리를 실행하는 도구. 

쿼리 러너를 사용하면 쿼리 빌더가 생성한 쿼리를 실행할 수 있다.

[러너의 장점]
쿼리 빌더가 생성한 쿼리를 실행할 수 있다.
쿼리 러너를 사용하여 쿼리 결과를 가져올 수 있다.
쿼리 러너를 사용하여 쿼리 결과를 처리할 수 있습니다.

 


쿼리 빌더와 쿼리 러너는 함께 사용하면 쿼리를 쉽고 효율적으로 생성하고 실행할 수 있다.

 

nestjs의 typeorm에서 트래잭션을 할 경우 querybuilder 을 쓰면 안되는 이유


QueryBuilder는 SQL 쿼리를 동적으로 생성하는 도구이다. 

트랜잭션은 SQL 쿼리를 실행하는 단위이기 때문에 QueryBuilder를 사용하면 트랜잭션을 제대로 관리할 수 없다.

예를 들어, 다음은 QueryBuilder를 사용하여 트랜잭션을 실행하는 코드이다.

 

const queryBuilder = this.connection.createQueryBuilder();

queryBuilder.insert()
  .into(User)
  .values([user]);

try {
  await queryBuilder.withTransaction();
  await queryBuilder.execute();
} catch (error) {
  await queryBuilder.rollback();
  throw error;
}

 

 코드는 트랜잭션을 사용하지만, 트랜잭션이 제대로 관리되지 않는다.

 QueryBuilder는 SQL 쿼리를 동적으로 생성하기 때문에 트랜잭션을 시작하고 종료하는 시점을 정확하게 알 수 없다. 

따라서 트랜잭션에 포함된 쿼리 중 일부가 실패하더라도 나머지 쿼리는 실행될 수 있다.

 

그래서 트랜젝션의 시작과 종료 시점을 정확히 알수 있는 

1.Transaction 객체

2. Transaction 함수 

3.queryRunner사용

 

 

Transaction 객체

const transaction = this.connection.createTransaction(); // Transaction 객체를 생성

try {
  await transaction.begin();  //begin() 메서드를 호출
  await this.repository.save(user);  // 트랜잭션 내 쿼리 실행
  await transaction.commit();//트랜잭션이 완료되면 Transaction 객체에서 commit() 메서드를 호출
} catch (error) {
  await transaction.rollback();//트랜잭션에 실패하면 Transaction 객체에서 rollback() 메서드를 호출
  throw error;
}

 

Transaction 함수 

await this.dataSource.transaction(async manager => {
      const user = new UserEntity();
      user.id = ulid();
      user.name = name;
      user.email = email;
      user.password = password;
      user.signupVerifyToken = signupVerifyToken;

      await manager.save(user);

      // throw new InternalServerErrorException();
    })

 

 

queryRunner 사용

const queryRunner = this.dataSource.createQueryRunner();

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

    try {
      const user = new UserEntity();
      user.id = ulid();
      user.name = name;
      user.email = email;
      user.password = password;
      user.signupVerifyToken = signupVerifyToken;

      await queryRunner.manager.save(user);

      // throw new InternalServerErrorException(); // 일부러 에러를 발생시켜 본다

      await queryRunner.commitTransaction();
    } catch (e) {
      // 에러가 발생하면 롤백
      await queryRunner.rollbackTransaction();
    } finally {
      // 직접 생성한 QueryRunner는 해제시켜 주어야 함
      await queryRunner.release();
    }

 

 

트랜잭션 테스트 방법

테스트 하는 법은 차후 TDD공부를 먼저 한뒤 마저 공부를 해야할것같다. 

우선은 직접 손으로 두세명이 모여 테스트를 해보는법이 베스트일것 같다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'외부 활동 > immersion' 카테고리의 다른 글

NEST 트랜잭션 부하테스트 [JMeter]  (0) 2023.08.07
NestJS TypeORM 트랙젝션 (queryRunner)  (0) 2023.07.31