백엔드/ORM
[TypeORM] TypeORM에서 트랜잭션 다루는 방법
eess
2024. 8. 5. 11:42
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 프로퍼티에 할당됩니다.
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 메서드)
- 주의해야 할 점은 콜백 함수의 매개변수로 주어지는 EntityManager를 사용하여 쿼리를 수행해야 한다는 것입니다.
- EntityManager 클래스의 tranasction 메서드 내부를 살펴보면, 새로운 QueryRunner를 생성하고 있습니다.
- 또한 DataSource 클래스의 createQueryRunner 메서드를 따라가보면, 현재 DBMS 드라이버에 맞는 QueryRunner를 생성하고 있습니다. 또한, 생성된 QueryRunner의 manager 프로퍼티에 새로운 EntityManager 객체를 할당하고 있습니다.
- dataSource.manager.trasaction()의 콜백 함수로 전달되는 EntityManager는, DataSource의 manager 프로퍼티에 할당된 전역 EntityManager가 아니라 QueryRunner의 EntityManager인 것입니다. (서로 다름!)
- 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가 있습니다.
참고
- https://github.com/typeorm/typeorm/issues/8010
- https://typeorm.io/data-source
- https://typeorm.io/working-with-entity-manager
- https://typeorm.io/query-runner
- https://typeorm.io/transactions
- https://taler.tistory.com/12
- https://opentutorials.org/module/3569/21223
- https://tecoble.techcourse.co.kr/post/2023-06-28-JDBC-DataSource/
- https://shuu.tistory.com/130
- https://dangdangee.tistory.com/entry/DB-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80Connection-Pool%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%86%8C%EC%8A%A4DataSource#google_vignette
- https://www.prisma.io/docs/orm/prisma-schema/overview/data-sources