본문 바로가기
백엔드/ORM

[TypeORM] TypeORM에서 트랜잭션 다루는 방법

by eess 2024. 8. 5.

 

 

TypeORM 0.3 이상 버전을 기준으로 트랜잭션을 다루는 방법을 정리했습니다.

 

Transaction

  • 트랜잭션은 데이터베이스에서 한꺼번에 수행되어야 하는 작업 단위를 말합니다.
  • 트랜잭션의 중요한 특징 중 하나는 트랜잭션이 데이터베이스에 모두 반영되든지, 아니면 전혀 반영되지 않아야 하며 작업이 부분적으로 실행되거나 중단되지 않는 것을 보장하는 것입니다. (ACID 중 Atomicity, 원자성)

 

DataSource & EntityManager / QueryRunner

  • TypeORM에서는 DataSource & EntityManager 또는 QueryRunner를 통해 트랜잭션을 생성하고 사용할 수 있습니다.
  • 두 가지 방법을 설명하기에 앞서, TypeORM에서의 DataSource, EntityManager, QueryRunner 세 가지 용어를 먼저 정리해 보겠습니다.

 

DataSource

  • 데이터베이스 연결 정보를 저장하고, connection과 connection pool을 설정하고 관리하는 클래스입니다.
  • DataSource 객체를 생성하여 initialize를 실행하면 connection과 connectioin pool 관리를 위한 설정이 이루어집니다.
  • 이는 하나의 DBMS에 대해 전역적으로 하나의 인스턴스로 관리됩니다.
import { DataSource } from "typeorm";

const AppDataSource = new DataSource({
  type: "mysql",
  host: "localhost",
  port: 3306,
  username: "test",
  password: "test",
  database: "test",
});

AppDataSource.initialize()
  .then(() => {
    console.log("Data Source has been initialized!");
  })
  .catch((err) => {
    console.error("Error during Data Source initialization", err);
  });

 

EntityManager

  • 모든 엔티티에 대해 쿼리를 수행하는 API와, 한꺼번에 수행되어야 하는 작업들을 트랜잭션으로 래핑하는 메서드(= transaction)를 제공합니다.
  • 이는 모든 엔티티의 repository를 한 곳에 모은 것과 같은 역할을 합니다.
    • (참고) 특정 엔티티에 대해 쿼리를 수행하려면 EntityManager가 아니라, dataSource.getRepository(User).findOneBy({ id: 1 }); 와 같은 방식으로 접근해야 합니다. 
import { DataSource } from "typeorm";
import { User } from "./entity/User";

const myDataSource = new DataSource(/*...*/);
const user = await myDataSource.manager.findOneBy(User, {
  id: 1,
});
user.name = "Umed";
await myDataSource.manager.save(user);
  • EntityManager는 EntityMAnagerFactory로부터 생성됩니다.
    • DataSource 인스턴스를 생성하여 초기화할 때 EntityManagerFactory의 create 메서드가 실행되고, 이를 통해 새로운 EntityManager 인스턴스가 DataSource 인스턴스의 manager 프로퍼티에 할당됩니다.

EntityManagerFactory 클래스

 

QueryRunner

  • 쿼리를 수행하거나 개발자가 트랜잭션을 직접적으로 제어할 수 있도록 하는 API를 제공합니다.
  • connection pool을 지원하는 RDBMS의 경우, connection pool에서 하나의 connection을 가져와서 사용하는 인터페이스입니다. QueryRunner 객체를 생성할 때마다 connection pool에서 connection을 가져와서 사용합니다.
  • connection pool을 지원하지 않는 DBMS의 경우 DataSource 전체에서 하나의 connection을 사용하여, QueryRunner 인스턴스가 같은 connection을 공유합니다.
  • QueryRunner는 클래스가 아니라 인터페이스이고, DBMS 마다의 구현체 클래스들이 존재하고 있습니다.
  • 주의할 점은 connection pool에서 다시 사용할 수 있도록 쿼리 수행 후에 connection을 반납해야 한다는 것입니다.

 

TypeORM의 Transaction 전략

1. DataSource & EntityManager Transaction

  • 앱을 부트스트랩 할 때 생성한 DataSource 인스턴스의 1) transaction 메서드 또는 2) manager 프로퍼티의 transaction 메서드로 트랜잭션을 수행할 수 있습니다.
await myDataSource.transaction(async (transactionalEntityManager) => {
  await manager.save(users[0]);
  await manager.save(users[1]);
});

// or

await myDataSource.manager.transaction(async (transactionalEntityManager) => {
  await manager.save(users[0]);
  await manager.save(users[1]);
});
  • TypeORM의 GitHub 코드를 살펴보면, 두 메서드는 사실 같은 일을 수행한다는 것을 알 수 있습니다.
    • DataSource 인스턴스를 생성할 때 수행되는 생성자 함수 내부에서, 새로운 EntityManager를 생성하여 manager 프로퍼티에 할당합니다.
    • DataSource 클래스의 transaction 메서드에서는, 현재 DataSource 인스턴스의 manager 프로퍼티의 trasaction 메서드를 수행합니다.
    • myDataSource.transaction()으로 접근하든 myDataSource.manager.transaction으로 접근하든, 결국 myDataSource.manager.transaction()을 수행하는 것입니다. (EntityManager 클래스의 transaction 메서드)

(좌) DataSource 클래스의 생성자 함수 내부 / (우) DataSource 클래스의 transaction 메서드 내부

  • 주의해야 할 점은 콜백 함수의 매개변수로 주어지는 EntityManager를 사용하여 쿼리를 수행해야 한다는 것입니다.
    • EntityManager 클래스의 tranasction 메서드 내부를 살펴보면, 새로운 QueryRunner를 생성하고 있습니다.
    • 또한 DataSource 클래스의 createQueryRunner 메서드를 따라가보면, 현재 DBMS 드라이버에 맞는 QueryRunner를 생성하고 있습니다. 또한, 생성된 QueryRunner의 manager 프로퍼티에 새로운 EntityManager 객체를 할당하고 있습니다.
    • dataSource.manager.trasaction()의 콜백 함수로 전달되는 EntityManager는, DataSource의 manager 프로퍼티에 할당된 전역 EntityManager가 아니라 QueryRunner의 EntityManager인 것입니다. (서로 다름!)

EntityManager 클래스의 tranasction 메서드 내부
DataSource 클래스의 createQueryRunner 메서드 내부

 

  • DataSource 또는 DataSource의 EntityManager의 트랜잭션 메서드로 트랜잭션을 수행할 경우, 내부적으로 QueryRunner를 생성하여 commit, rollback, connection 반납까지 해주고 있는 것입니다.

 

2. QueryRunner Transaction

  • 수동으로 QueryRunner를 생성하여
    • 직접 SQL 쿼리를 수행하거나, 
    • EntityManager API로 쿼리를 수행하거나,
    • 트랜잭션을 직접 제어할 수 있습니다.
const queryRunner = dataSource.createQueryRunner();

await queryRunner.connect();
// 1. 직접 SQL 쿼리를 수행하거나
await queryRunner.query("SELECT * FROM users");
// 2. EntityManager API로 쿼리를 수행하거나
const users = await queryRunner.manager.find(User);

// 3. 트랜잭션을 제어할 수 있다.
await queryRunner.startTransaction();

try {
  // 트랜잭션 작업들...
  await queryRunner.manager.save(user1);
  await queryRunner.manager.save(user2);

  // commit
  await queryRunner.commitTransaction();
} catch (err) {
  // 에러가 발생하면 rollback
  await queryRunner.rollbackTransaction();
} finally {
  // 생성한 QueryRunner 해체(connection 반납)
  await queryRunner.release();
}

 

 

그래서 어떤 걸 쓰라는 것인가 !?

  • DataSource의 transaction 메서드는 상대적으로 코드가 간결해지는 효과가 있지만, 직접적으로 트랜잭션을 다룰 수 없습니다.
  • QueryRunner는 직접 트랜잭션을 제어할 수 있지만, 중복되는 코드가 많아지고 트랜잭션 관리에 대한 책임이 따릅니다.
  • NestJS 프레임워크에서는 TypeORM의 트랜잭션을 다루는 전략 중 직접 트랜잭션을 제어할 수 있는 QueryRunner를 사용하는 것을 권장하고 있습니다. [참고]
  • 개인적은 생각으로는 QueryRunner로 트랜잭션을 직접 제어하되, 중복되는 코드를 줄일 수 있는 리팩터링 방법을 찾는 것이 좋을 것 같습니다.

 

(번외) TypeORM은 Connection을 왜 DataSource로 바꿨을까?

  • TypeORM 0.2.x 버전에서 Connection은 0.3.0 버전부터 deprecated 되고, 지금의 DataSource로 바뀌었습니다. [참고]
  • TypeORM 개발자가 올린 issue를 보면, Connection 이라는 이름이 적절하지 않으며, DataSource 로 변경해야 한다고 이야기 합니다.
  • 이전의 Connection 이라는 명칭은 실제로 connection을 의미하지 않으며, 그저 DBMS 연결 설정에 관한 객체일 뿐이라고 합니다. 실제 connection을 의미하는 것은 QueryRunner 입니다.

 

그럼 왜 DataSource 일까?

  • Java에서 데이터베이스에 접근할 수 있도록 하는 API인 JDBC(Java Database Connectivity)에 DataSource라는 인터페이스가 있습니다. DataSource 인터페이스로 데이터베이스 연결과 connection을 얻는 과정을 추상화하는데, DataSource 구현체를 만들어서 사용합니다. 대표적으로 Spring에서는 HikariCP connection pool을 기반으로 한 HikariDataSource를 제공합니다.
    • (참고) JPA에도 EntityManager라는 개념이 있습니다. connection을 얻어서 데이터베이스와 상호작용하고 영속성 컨텍스트를 관리하는 역할입니다. TypeORM에도 영속성 컨텍스트, 1차 캐시의 개념이 있는지는 잘 모르겠습니다. 공식문서 상에서는 영속성 컨텍스트의 역할(1차 캐시에 엔티티 영속화)을 하는 개념은 찾을 수 없었습니다.
  • TypeORM의 여러 개념들은 상대적으로 역사가 더 오래된 Java와 JPA에서 많은 부분 차용해서 만들어졌다고 생각이 듭니다. NodeJS에서 사용할 수 있는 또 다른 ORM 기술 중 하나인 Prisma에서도 JPA, TypeORM에서의 DataSource와 같은 의미로 DataSource API가 있습니다.

 

참고