메인 프로젝트할 때의 이야기다.
당시 나는 Transaction 이라는 개념 자체를 모르던 시절이었다. 그러다 같이 사이드 프로젝트를 하는 선배가 Transaction 이라는 키워드를 던져주셔서 이에 대해서 공부를 하고 동아리에서 매 2주마다 하는 발표(당시 영상 자료)의 주제로 사용했었다.
Transaction에 대해서 알아 보고 메인 프로젝트의 코드를 보았다.
문제가 없을리가 없다. 정말 많은 문제가 있었지만 대표적인 예시를 몇개 들어보면
PK를 FK로 갖고 있는 로우의 데이터를 수정해야 함. 근데 만약 해당 로우가 존재하지 않는다면 서버 에러.이 모든걸 종합해보면 해당 예시에 있는 하나의 큰 작업과 그 뒤에 따라오는 추가적인 처리들은 하나의 작업 단위, 즉 하나의 Transaction으로 묶여야 하는 작업임.
흔히 Transaction이 설명되는 대표적인 예시는 계좌 이체를 하는 상황이다.
아래에서 간단한 예시를 들어 보겠다.
A와 B가 있다.
A는 B에게 3천원을 빌렸었고, 지금 돈을 갚기 위해 계좌 이체를 한다.
둘의 잔고 상황은 다음과 같다.
A 잔고: 10,000
B 잔고: 10,000
- A --> B 3천원 송금.
이 때 A 잔고는 -3,000이 되어 7,000원이 남음.- A의 잔고는 -3,000이 된 상태. 이제 B의 잔고에 +3,000을 해줘야 한다.
- 근데 B의 잔고에 +3,000을 하는 과정에서 에러가 발생.
- 하나의
Transcation으로 묶이지 않았기에 A의 돈만 빠져나가고 B의 계좌에는 해당 돈이 들어오지 않는 대참사 발생.
이 정도면 Transaction의 중요성과 간단한 설명 정도는 된 것 같다. 딱 Transaction의 특징까지만 알아보자.
Transaction의 특징(ACID)이 정도까지만 하고 NestJS에서 TypeORM을 이용해 Transaction을 관리하는 방법을 알아보자.
TypeORM의 Transaction 관리 방법은 크게 두가지가 있다.
1. transaction 메서드를 직접 이용.
이 방법은 EntityManager의 transaction 메서드를 사용하는 방법이다.
await this.dataSource.manager.transaction(
async (transactionalEntityManager) => {
await transactionalEntityManager
.withRepository(this.repository)
.save({ userId, ...postProps });
QueryRunner를 통해 트랜잭션 제어QueryRunner를 이용해 단일 DB 커넥션 상태를 생성하고 관리NestJS공식문서에는 QueryRunner가 Transaction을 완전하게 제어할 수 있으므로 QueryRunner 사용을 권장한다.그럼 나는 공식문서의 추천에 따라 QueryRunner를 통해 트랜잭션을 제어하는 방법을 알아보겠다.
괜히 코드블럭으로 설명하는 것보단 발표때 사용된 사진을 사용하는게 더 이해가 쉬울 것 같아서 사진을 통해 설명하겠다.
일단 가장 먼저


실제 queryRunner를 통해 트랜잭션을 제어하는 예시 코드 하나를 작성해보겠다.
@Injectable()
export class PostsService {
constructor(
// dataSource 객체 주입
private readonly dataSource: DataSource,
private readonly postRepository: PostRepository,
private readonly postHistoryRepository: PostHistoryRepository
) {}
async create(
userId: number,
title: string,
description: string
) {
// queryRunner 생성
const queryRunner = this.dataSource.createQueryRunner();
// queryRunner 연결 및 transaction 시작
await queryRunner.connect();
await queryRunner.startTransaction();
// try catch 문법 사용
try {
const entityManager = queryRunner.manager;
// 생성한 queryRunner의 entityManager를 통해 쿼리를 실행.
// 이 부분이 중요하다. 해당 entityManager를 통해 쿼리를 날려야 함.
// 그렇지 않으면 한 트랜잭션으로 묶이지 않음.
const newPost = await entityManager
.withRepository(this.postRepository)
.save({
userId,
title,
description
});
// 마찬가지로 같은 entityManager를 통해 쿼리를 날린다.
await entityManager
.withRepository(this.postHistoryRepository)
.save({
postId: newPost.id,
userId,
title,
description
});
// DB 작업이 성공적으로 수행됐다면 commit으로 영속화 완료.
await queryRunner.commitTransaction();
} catch (err) {
// 만일 에러가 발생 시, rollback 시켜준다.
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction();
}
console.error(err);
} finally {
// 모든 작업을 마쳤다면, 반드시 queryRunner를 relase 해줘야 함.
if (!queryRunner.isReleased) {
await queryRunner.release();
}
}
}
}
대충 게시글 하나를 생성하고 해당 게시글의 history를 쌓는 로직이다.
주석을 보면 알다시피 중요한 점이 몇 가지 있다.
- 생성한 queryRunner의 entityManager를 통해 DB 작업을 수행.
- 작업을 마쳤다면 queryRunner를 release 해준다.
만약 queryRunner를 release 시켜주지 않는다면 다음과 같은 문제가 발생한다.
이 점 숙지하도록 하고, 여기까지만 알아보도록 하자.
Transaction을 적절히 잘 관리 하는 것은 굉장히 중요하다.
끝