Transaction에 대해서 알고 어떻게 사용하면 좋을지 고찰하기

어쩌다·2023년 2월 17일
0

transaction에 대한 정의

먼저 transaction에 대한 사전적 정의를 알아보자.

transaction트랜잭션

DB에 따른 정의는 '처리'에 더 가깝다.
따라서 트랜잭션은 데이터의 처리 단위라고 할 수 있겠다.
DB의 INSERT, UPDATE, DELETE와 같은 DNL이 트랜잭션 안에 들어간다고 볼 수 있다.


왜 transaction을 사용하는가?

위에서 언급했듯이 트랜잭션은 하나의 처리 단위이다.
따라서 단위라는 것은 시작과 끝이라는 게 있지 않은가? 그렇기 때문에 트랜잭션도 하나의 처리 단위가 시작하고 끝이 나야 해당 트랜잭션이 마무리 될 수 있는 것이다.

그렇다면 마무리 된 데이터는 RAM에만 있다가 HD에 저장이 되어야 할 텐데, 이를 TCL(Transaction Control Language)을 통해 제어한다.
대표적인 예시가 commitrollback이다.
트랜잭션이 이루어졌을 때, 중도에 실패하게 되면 RAM이나 HD를 잡아먹지 않도록 rollback시키는 게 올바르다.
이러한 control language가 있는 이유 또한 I.O를 줄이기 위한 것이고, 동기화가 중요한 DB에선 처리 단위를 정하는 편이 데이터가 일관적이지 않겠는가.


ACID(Atomicity, Consistency, Isolation, Durability)

그럼 트랜잭션에 대한 특징에 대해서 알아보자.
원자성, 일관성, 고립성, 지속성이란 4가지 특징을 줄여서 ACID라고 한다.
트랜잭션이 안전하게 수행되는 것을 보장하기 위한 성질을 가리킨다.

간단하게 풀어서 얘기해보자면, read가 아닌 write에 해당되는 DNL의 트랜잭션이 수행될 때, 매우 중요하고 (되도록이면)완벽한 처리가 이루어져야 할 것이다.
그렇게 되려면 트랜잭션은 실행 중 중도에 중단되는 일은 없어야 할 것이며 Atomicity , 트랜잭션이 성공적으로 완수되었다면 해당 데이터는 일관성 consistency 있게 유지durability 되어야 할 것이다. 그리고 더더욱이 트랜잭션을 수행할 때, 다른 트랜잭션이 끼어들게 되면 유연성 있게 데이터 처리를 하지 못할 뿐더러 다른 성질을 위반할 수 있는 가능성이 난무해진다isolation .


그러면 코딩할 때 트랜잭션은 어떻게 다루는 게 좋을까?

TypeScript와 Seqeulize로 예를 들어보겠다.

async create(data: string) {
  	/** 트랜잭션 선언 */
    const t = await this.seqeulize.transaction();
    try {
    	...
      ...
      await t.commit();
    } catch (error) {
      Logger.error(error);
      await t.rollback();
      throw new InternalServerErrorException("faield to create data");
    }
  }

CRUD 중 CREATE에서 기본적인 구조라고 할 수 있겠다.
sevice단에서 로직 실행 후 성공하면 commit을 하고 로직 실행 중 catch에 걸렸다면, rollback을 하는 것이 일반적이다.
함수의 stack 시작과 끝이 하나의 transaction이라고 할 수 있다.

이렇게 간단한 CRUD라면 크게 신경을 쓸 부분은 없지만, 나쁜 예가 있다.

async create(data: string) {
  	/** 트랜잭션 선언 */
    const t = await this.seqeulize.transaction();
    try {
      ...
      ...
      /** 트랜잭션을 선언 후 다른 함수에 넘겨준다. */
    	await anyFuntion(t);
    } catch (error) {
      Logger.error(error);
      await t.rollback();
      throw new InternalServerErrorException("faield to create data");
    }
  }

async anyFuntion(t: any) {
  try {
    ...
    ...
    /** 넘겨서 다른 함수에 commit을 한다. */
    await t.commit();
  } catch (error) {
    Logger.error(error);
    await t.rollback();
    throw new InternalServerErrorException("faield to create data");
  }
}

코드를 보면 트랜잭션 객체를 다른 함수에 넘겨서 해당 함수에서 commit을 하는 로직이다.
코드는 실행될지 몰라도 트랜잭션을 잘 사용했다고 볼 수는 없다.
트랜잭션은 데이터의 처리 단위이다. 하나의 단위가 진행 중(function stack)인데, 이를 다른 stack에 넘겨 트랜잭션을 진행하는 것은 예외가 발생할 수 있는 가능성이 대폭 높아진다.

되도록이면 트랜잭션이 먼저 선언이 된 함수에서 CRUD 처리를 마무리 할 수 있는 게 좋다.
또 다른 예시를 보자.

async create(data: string) {
  	/** 트랜잭션 선언 */
    const t = await this.seqeulize.transaction();
    try {
      ...
      ...
      /** 트랜잭션을 걸지 않음 */
     	await user.create(data);
      /** 트랜잭션을 걸었음 */
      await alram.create(data, { transaction: t });
      
      await t.commit();
    } catch (error) {
      Logger.error(error);
      await t.rollback();
      throw new InternalServerErrorException("faield to create data");
    }
  }

CRUD를 진행할 때, 트랜잭션 선언을 했다면 해당 트랜잭션 안에 있는 데이터 처리를 한다는 것을 명시해줘야 한다.
위에서 다룬 ACID에 따르면, 하나의 트랜잭션 연산에서 다른 트랜잭션이 끼어드는 일은 없어야 한다.
그렇게 되려면 하나의 트랜잭션 연산에서 선언된 CRUD만 들어가야 한다.

이를 명시하지 않으면 다른 트랜잭션과 엉키거나 꼬일 가능성이 높다.
반드시 선언된 트랜잭션을 CRUD에서 명시해주자.


여담

아직 경력이 많이 없는 주니어지만, 실무에서 트랜잭션을 잘 다루지 못하여 일어나는 예외들이 많았다.
이번 기회에 정의와 이론에 대해서 짚고 넘어가며 리마인드 할 수 있어서 좋았다.
다음에는 더욱 심도 있는 정리가 필요해 보이고, 이를 위해서 공부에 매진해야겠다.

profile
혼자 공부하는 공간

0개의 댓글