[NestJS] - 15. TypeORM 트랜잭션(Transaction)
이번엔 TypeORM에서의 트랜잭션 적용을 해보겠다.
트랜잭션 (Transaction)
하나의 로직을 처리하는 SQL 질의들을 집합으로 묶어, 도중에 예외가 발생할 경우 Rollback 처리를, 모두 성공할 경우 Commit 처리하는 실행 단위이다.
쇼핑몰에서 주문할 때의 과정을 생각해보자. (주문 요청 -> 결제 처리 -> 성공 시 주문 완료 데이터 저장)
만약에 다음 과정에서 결제는 완료되었는데 주문이 들어가지 않았다면 사용자 입장에서는 어이가 없을 것이다.
또 결제 처리는 실패되었는데 주문 완료 데이터는 저장되어있다면 해당 데이터베이스는 더이상 믿을 수 없게 된다.
즉, 성공하려면 모두 성공하고, 실패하려면 모두 실패해야 한다.
자세한 내용은 해당 포스팅을 참고해도 좋을 것 같다. https://sjh9708.tistory.com/40
기존 비즈니스 로직의 문제점
https://sjh9708.tistory.com/38
async create(input: CreateProductInput): Promise<Product> {
const { productSalesLocation, productCategoryId, productTags, ...product } = input;
// 상품 거래위치 등록...
// 태그 등록...
// 상품 등록...
return result;
}
기존 로직에서 위치 거래위치 등록, 상품태그 등록, 상품 등록의 과정을 거쳤다.
그렇지만 태그 등록까지는 성공했으나 상품 등록에는 실패한다면 데이터베이스의 일관성이 깨져버리게 된다.
차라리 해당 로직과 같은 기본적인 웹 비즈니스 로직의 경우 양반이고, 금융 시스템과 같은 민감한 데이터를 다루는 로직이라면 크리티컬한 타격을 입게 될 것이다.
따라서 해당 로직에 트랜잭션을 적용시켜보는 것을 해보려고 한다.
기본적인 트랜잭션 동작 작성
constructor(
//...
private readonly connection: Connection, //Typeorm 지원
) {}
async create(input: CreateProductInput): Promise<Product> {
const { productSalesLocation, productCategoryId, productTags, ...product } =
input;
const queryRunner = await this.connection.createQueryRunner();
try {
// T. 트랜잭션 사용을 위해서 queryRunner 연결
await queryRunner.connect();
// T. 트랜잭션 시작
await queryRunner.startTransaction();
// 비즈니스 로직..
// T. 트랜잭션 커밋
await queryRunner.commitTransaction();
return result;
} catch (e) {
// T. 트랜잭션 롤백
await queryRunner.rollbackTransaction();
} finally {
// T. queryRunner 연결 종료
await queryRunner.release();
}
}
- 트랜잭션 사용을 위해서 QueryRunner 객체를 사용하는 방법이 있다. 해당 모듈은 TypeORM에서 지원하며 생성자에서 주입시켜주었다.
- 트랜잭션 시작 시 QueryRunner를 생성하여 Connect() 연결, 그리고 startTransaction()으로 트랜잭션을 시작한다.
- 트랜잭션 성공 시 commitTransaction()으로 커밋 처리한다.
- 트랜잭션 실패 시 rollbackTransaction()으로 롤백 처리한다.
- 트랜잭션 종료 시 release()를 통해 연결을 종료한다.
- 예외 처리를 위해 try-catch-finally로 코드를 묶어주었다.
await queryRunner.startTransaction('SERIALIZABLE');
- 트랜잭션 시작 시 격리 수준을 설정해 줄 수도 있다.
트랜잭션에서의 CRUD
트랜잭션에서는 쿼리가 트랜잭션 작업 안에서 이루어져야 한다. 따라서 Repository가 아닌 queryRunner의 메서드를 이용하여 질의를 하는 방법이 있다.
Select
const existTag = await queryRunner.manager.findOne(ProductTag, {
name: tagName,
});
- queryRunner.manager에서 find, findOne등을 작성한다.
- 차이점은 Repository를 사용하지 않으므로 인수로 어떤 엔티티를 사용할 지를 명시해주어야 한다. 위에서는 ProductTag 엔티티를 타겟으로 한다고 명시했다.
const existTag = await queryRunner.manager.findOne(ProductTag, {
where: {
name: tagName,
},
lock: { mode: 'pessimistic_write' },
});
- Transaction 격리 수준에 따라서 lock을 걸어 다른 트랜잭션에서의 접근을 제어할 수 있다.
const existTag = await queryRunner.manager
.createQueryBuilder(ProductTag, 'pt')
.where('pt.name = :name', { name: tagName })
.setLock('pessimistic_write')
.getOne();
- 트랜잭션 안에서 QueryBuilder를 사용
Insert / Update / Delete
const location = await this.productSalesLocationRepository.create({
...productSalesLocation,
});
await queryRunner.manager.save(location);
- Repository에서 엔티티 인스턴스를 생성한 후 QueryBuilder를 통해 Save하는 형태로 사용할 수 있다.
const deelete = await queryRunner.manager.softDelete(Product, {
id: id,
});
- 삭제도 마찬가지로 querryRunner에서 이루어질 수 있다.
리팩토링
async create(input: CreateProductInput): Promise<Product> {
const { productSalesLocation, productCategoryId, productTags, ...product } =
input;
const queryRunner = await this.connection.createQueryRunner();
try {
// T. 트랜잭션 사용을 위해서 queryRunner 연결
await queryRunner.connect();
// T. 트랜잭션 시작
await queryRunner.startTransaction();
//위치 저장
const location = await this.productSalesLocationRepository.create({
...productSalesLocation,
});
await queryRunner.manager.save(location);
//태그 등록
const productTagList = await Promise.all(
productTags.map((el) => {
return new Promise(async (resolve, reject) => {
const tagName = el.replace('#', '');
const existTag = await queryRunner.manager
.createQueryBuilder(ProductTag, 'pt')
.where('pt.name = :name', { name: tagName })
.getOne();
if (existTag) {
resolve(existTag);
} else {
const newTag = await this.productTagRepository.create({
name: tagName,
});
await queryRunner.manager.save(newTag);
resolve(newTag);
}
});
}),
);
const products = await this.productRepository.create({
...product,
productSalesLocation: location,
productCategory: {
id: productCategoryId,
},
productTags: productTagList,
});
const result = await queryRunner.manager.save(products);
// T. 트랜잭션 커밋
await queryRunner.commitTransaction();
return result;
} catch (e) {
// T. 트랜잭션 롤백
await queryRunner.rollbackTransaction();
} finally {
// T. queryRunner 연결 종료
await queryRunner.release();
}
}
- 기존의 소스코드를 바꾸어보았다. 트랜잭션을 사용했다는 점 빼고는 기존 로직과 동일하다.
- Postman 실행 결과 Transaction 작업이 실행되는 것을 확인할 수 있었다.
- 도중 Exception이 발생했을 때 Rollback되는것을 확인할 수 있다.
'Backend > Node.js (NestJS)' 카테고리의 다른 글
[NestJS] - 17. TypeORM에 여러개의 데이터베이스 연결하여 사용하기 (0) | 2023.02.25 |
---|---|
[NestJS] - 16. TypeORM View와 Raw Query, Index/Unique 제약조건 걸기 (0) | 2023.02.25 |
[NestJS] - 14. TypeORM JOIN과 QueryBuilder (0) | 2023.02.25 |
[NestJS] - 13. TypeORM 관계 테이블 Insert/Update (1) | 2023.02.23 |
[NestJS] - 12. TypeORM Select (0) | 2023.02.23 |