biud436 / stingerloom

Node.js Server Framework
6 stars 1 forks source link

feat: 트랜잭션 전파 구현 #36

Closed biud436 closed 1 year ago

biud436 commented 1 year ago
const result = originalMethod.call(targetInjectable, ...args);

트랜잭션 매니저에서 위 코드를 실행하기 전에 QueryRunner에 대한 복원 지점을 만들어야 합니다. 쉽게 말하면 현재 진행되는 물리 트랜잭션을 롤백하고 쿼리가 실행되기 전의 상태까지 재실행해야 합니다. originalMethod가 실행되는 도중에 논리 트랜잭션이 롤백될 수도 있기 때문입니다.

따라서 논리 트랜잭션이 롤백되면 복원 지점으로 QueryRunner를 복구해야 합니다.

  1. 복원 지점 생성
  2. 논리 트랜잭션에서 롤백 발생
  3. 물리 트랜잭션을 롤백한다.
  4. 새로운 물리 트랜잭션을 복원 지점에서 다시 실행한다.

물리 트랜잭션을 두 개 생성해야 하므로 비효율적이라고 할 수 있습니다.

biud436 commented 1 year ago
biud436 commented 1 year ago

TypeORM의 MySQL 드라이버는 중첩 트랜지션을 지원합니다. 이는 다음과 같이 구현되어있습니다. 즉, transactionDepth가 0이 아닌 경우에만 트랜잭션 시작 시, SAVEPOINT를 설정합니다.

https://github.com/typeorm/typeorm/blob/d184d8598c057ce8fa54815e669b567238f3a86e/src/driver/mysql/MysqlQueryRunner.ts#L128

    /**
     * Starts transaction on the current connection.
     */
    async startTransaction(isolationLevel?: IsolationLevel): Promise<void> {
        this.isTransactionActive = true
        try {
            await this.broadcaster.broadcast("BeforeTransactionStart")
        } catch (err) {
            this.isTransactionActive = false
            throw err
        }
        if (this.transactionDepth === 0) {
            if (isolationLevel) {
                await this.query(
                    "SET TRANSACTION ISOLATION LEVEL " + isolationLevel,
                )
            }
            await this.query("START TRANSACTION")
        } else {
            await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`)
        }

        // 트랜잭션 깊이를 1 늘린다.
        this.transactionDepth += 1

        await this.broadcaster.broadcast("AfterTransactionStart")
    }

커밋 코드를 보면 다음과 같습니다.

    /**
     * Commits transaction.
     * Error will be thrown if transaction was not started.
     */
    async commitTransaction(): Promise<void> {
        if (!this.isTransactionActive) throw new TransactionNotStartedError()

        await this.broadcaster.broadcast("BeforeTransactionCommit")

        if (this.transactionDepth > 1) {
            await this.query(
                `RELEASE SAVEPOINT typeorm_${this.transactionDepth - 1}`,
            )
        } else {
            await this.query("COMMIT")
            this.isTransactionActive = false
        }
        this.transactionDepth -= 1

        await this.broadcaster.broadcast("AfterTransactionCommit")
    }

롤백 코드는 다음과 같습니다.

    /**
     * Rollbacks transaction.
     * Error will be thrown if transaction was not started.
     */
    async rollbackTransaction(): Promise<void> {
        if (!this.isTransactionActive) throw new TransactionNotStartedError()

        await this.broadcaster.broadcast("BeforeTransactionRollback")

        if (this.transactionDepth > 1) {
            await this.query(
                `ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth - 1}`,
            )
        } else {
            await this.query("ROLLBACK")
            this.isTransactionActive = false
        }
        this.transactionDepth -= 1

        await this.broadcaster.broadcast("AfterTransactionRollback")
    }

따라서 TypeOrm에서 MySQL을 사용할 경우에는 중첩 트랜지션이 가능하다는 것을 알 수 있습니다.

이를 통해 스프링의 트랜잭션 전파 속성인 Propagation.NESTED를 프레임워크에서 구현할 수 있습니다.