Transaction

·2022년 5월 1일
0

TIL

목록 보기
32/36

Transaction이란?

트랜잭션이란 데이터베이스에 접근하여 작업을 수행하기 위한 최소 단위를 이야기한다.

예를 들자면 내가 만든 시스템에서는 이렇게 설명할 수 있다.

  1. 유저가 장바구니에 있는 물건을 결제한다.
  2. 장바구니에 있던 목록들이 주문내역에 쌓이게 된다.
  3. 목록 중 구매된 수량 만큼 물건상세탭에서 재고가 줄어들게 된다.

내용이야 더 있겠지만, 이것 자체가 한개의 트랜잭션으로 구성이 된다.

그리고 트랜잭션에는 4가지 원칙이 존재한다.

바로 ACID이라고 불리는 것인데 설명은 아래와 같다. -> https://ko.wikipedia.org/wiki/ACID

이게 디비에만 적용이 되는 줄 알았는데,
디비가 아니라 컴퓨터 그 자체에도 관련이 있는 것이라 연계해서 이야기를 해볼까 한다.


일단 한가지 예를 들어서 어떤 식의 문제가 있는지 알아보도록 하자.

  1. 내가 편의점에서 과자를 사기 위해 과자를 사들고 결제를 위해 직원에게 카드를 내밀었다.
  2. 근데 카드에 잔액이 없어서 결제가 안됐다.
  3. 그러자 내가 카드를 낚아채서 도망갔다.

분명 결제가 되지 않았을 경우에는 다시 물건을 제자리에 놓는 과정이 필요할 것인데
그것을 스킵하고 그냥 카드를 낚아채서 도망가버렸다.

실행을 했을 경우 한가지가 실패했다면, 실행했던 모든 것들을 취소해야할 상황이 존재할 것이다. (대부분 결제관련)
그것때문에 트랜잭션을 적용시켜야한다.

그럼 이것을 TypeORM에서 구현을 해보도록 하자.

TypeORM에서의 구현

일단 구현하기 위해서는 객체값을 주입받아오고, 사용 준비를 해야한다.

constructor(
    private readonly connection: Connection,
  ) {}
  const queryRunner = await this.connection.createQueryRunner();
    await queryRunner.connect();

이렇게 할 경우 트랜잭션을 생성할 수 있게 된다.

그리고 시작을 시켜줘야한다.

 await queryRunner.startTransaction('SERIALIZABLE');
 try{
 // ~진행되는 코드들~
 await queryRunner.commitTransaction(); // 커밋을 시켜줘야 트랜잭션이 종료된다.
 } catch (error) {
      await queryRunner.rollbackTransaction(); // 에러가 날 경우에는 실행했던 모든 것을 초기화시켜준다.
    } finally {
      await queryRunner.release(); // 성공일 경우 최종적으로 연결을 끊어준다.
    }

여기서 중요한 것은 4개지만, 3개를 먼저 언급을 하려고 한다.

트랜잭션의 원칙에는 ACID이 있고, 그중 관련된 것은 원자성 Atomicity와 지속성 Durability가 있다.

  1. 문제가 발생하지 않을 경우 연결을 끊어준다.
  2. 문제가 발생을 할 경우 진행된 모든 것들을 실행 취소를 시킨다.

이 두가지가 정말 중요한데
1번을 위하여 커밋트랜잭션과 릴리즈가 존재하고
2번을 위해서 롤백트랜잭션이 존재한다.

그리고 위에 거추장스럽게 사용하지 않고 정말 짧게 쓸 수 있다는 트랜잭션 라이브러리가 있는데 이것에 대해서는 조금 더 사용법을 알아봐야할 것 같다.
어짜피 결국은 사용하게 되는 개념일테니까

https://www.npmjs.com/package/typeorm-transactional-cls-hooked


그렇다면 위에 언급하지 않았던 1개에 대해서 이야기를 해보려고 한다.

그것은 독립성과 일관성에 관련된 내용인데, 예시를 보고 이해를 해보도록 하자.

현실의 경우에서

친구들과 함께 술을 마실 경우 내가 전부 낸 후 잔액을 결제가 끝난 후 받기로 했다.
그럴 경우에는 내가 가지고 있는 돈은 확실하기에 친구들이 준 돈을 모두 더하면 될 것이다.

하지만 컴퓨터의 경우는 조금 다르게 된다.

컴퓨터의 경우에서

컴퓨터에서 더하기를 할 경우에는 원래 있는 값을 불러온 후, 그 값에 더하는 식으로 진행되게 된다.
하지만 이런 것이 발생할 수도 있다.
내 통장에 돈이 5천원이 있고 친구1이 3천원을 줬다고 한다면
결과적으로 통장에 돈은 8천원이 될 것이다.

그런데 이 상태에서 친구 2,3이 3천원 한번에 보냈다고 생각을 해보자
그럴 경우
내 잔액 : 8000원 , 친구 2가 준 돈 : 3000원 = 11000원
내 잔액 : 8000원 , 친구 2가 준 돈 : 3000원 = 11000원

14000원이 되어야하는데, 11000원이 되는 기묘한 과정을 볼 수 있다.

이것때문에 ACID중 일관성(isolation)이 중요하게 여겨지는데 이것에 대한 레벨을 둬서 차이를 만들어낼 수 있다.

그럼 이것이 어떤식으로 나눠져있는지 알아보도록 하자.

흔히 격리 레벨(isolation level)이라고도 부른다.

격리레벨이 높아질수록 더욱 더 완벽한 조치가 가능하다.
하지만 그렇기 때문에 속도가 느려지는 문제가 발생한다.

위를 보면 레벨이 4개로 나눠져있고, 3가지로 옵션이 적혀있는 것을 볼 수 있는데 내용은 이러하다.

Dirty-Read : 다른 트랜잭션에 의해 수정됐지만 커밋되지 않은 데이터를 읽는 것.
Non-Repeatable-Read : 같은 쿼리를 두번 수행할 때 값이 변경 될 경우 두개의 결과가 서로 다르게 된 것.
Phantom-Read : 한 트랜잭션 안에서 값을 읽었을 경우, 없던 것이 생겨나는 것.


그럼 여기서 다시 이야기하던 것으로 돌아와서 양쪽에서 접근하는 것을 막아주는 옵션에 대해서 이야기해보자.
위의 표를 보면 Repeatable-Read,Serializable에서만 양쪽에서 수정하는 불상사가 없다는 것을 볼 수 있다.
Phantom-Read는 말그대로 읽는 것만 다르기 때문이니깐.

그래서 여기서 lock 옵션을 걸 수 있게 된다.
락에는 2가지가 존재하는데 비관적 락과 낙관적 락이 존재한다.
또한 이 부분을 운영체제에서 쓰는 용어가 있는데 세마포어와 뮤텍스라는 용어가 있다.
상당히 중요한 부분에 있기 때문에 공부를 해놔야할 것 같다.

하지만 배운 것이 비관적 락이고 낙관적 락에 대해서는 아직 지식이 모자라서 나중에 채워보려고 한다.

비관적 락(pessimistic lock)이란?

트랜잭션이 시작됐을 때 lock을 걸어서 아무것도 접근을 하지 못하게 하는 것을 이야기한다.

다양한 옵션이 존재하는데, 나는 수정할 수 없는 권한을 주기 위해서
pessimistic_write옵션을 사용하여 현재 트랜잭션이 진행중일 경우에는 아무도 접근을 할 수 없게 만들어놓았다.

TypeORM에서의 사용 법(코드)

const userdata = await queryRunner.manager.findOne(
        User,
        { user_email: currentUser.user_email },
        {
          lock: { mode: 'pessimistic_write' },
        },
      );

조금 애매모호한 것은 쿼리빌더를 쓸 경우 트랜잭션이 따로 돌아가서 영향을 받지 않는다는 것인데, 이것도 더... 공부가 필요할 것 같다 (ㅠㅠ)
근데 쿼리빌더로 사용을 하면 트랜잭션 시작된다는 로그를 볼 수 있어서 쿼리러너가 아니라 빌더로 구현을 할 수 있을 것 같다는 생각도 하고 있다.

profile
물류 서비스 Backend Software Developer

0개의 댓글