[Real MySQL, 4장 - 트랜잭션과 잠금] MySQL에서의 트랜잭션
Database

[Real MySQL, 4장 - 트랜잭션과 잠금] MySQL에서의 트랜잭션

1. 알고자 하는 것

  • 트랜잭션(Transaction)이란
  • 트랜잭션이 필요한 이유
  • 트랜잭션 적용 시 주의사항

 

 

2. 알게된 것

트랜잭션(Transaction)이란

  • 데이터베이스에서 작업의 완전성을 보장하기 위한 개념
  • 작업을 더 이상 쪼갤 수 없는 하나의 원자적인 단위로 묶음
  • 이러한 논리적인 작업셋이 모두 성공하거나, 하나라도 실패 시 모두 적용하지 않고 되돌리는 것 (All or Nothing)
    • 이는 단지 여러 개의 쿼리를 묶는 것만을 의미하지는 않음
    • 단 하나의 쿼리라도 원자적인 작업으로 묶어, 실패 시 되돌린다.
  • 다음과 같은 테이블이 있을 때, unique로 지정된 uid 컬럼이 중복되었을 때 트랜잭션이 어떻게 동작하는지 확인해본다.
create table member
(
    id bigint auto_increment primary key,
    uid bigint unique
);

  • 초기 테이블에는 uid 1을 가지는 row가 하나 존재한다.
  • 해당 테이블에 아래와 같이 uid를 각각 1,2,3으로 가지는 member를 insert 하는 동작을 수행하고, 그 결과를 확인한다.
insert into member(uid) values(1), (2), (3);
  • 쿼리 수행 시 uid가 1인 entry가 중복된다는 [23000][1062] Duplicate entry '1' for key 'member.uid' 오류가 발생한다.

  • 테이블은 중복되지 않는 uid 2, 3에 대한 insert 동작까지도 모두 수행되지 않음을 확인할 수 있다. 
  • 즉, uid 1,2,3에 해당하는 insert 작업이 하나의 트랜잭션으로 묶였고, uid 1이 중복되어 실패함으로써 나머지 동작이 모두 rollback 되었다.
  • (+) MyISAM 엔진의 경우, 트랜잭션을 지원하지 않으므로 위 동작의 결과로 uid 2, 3인 row는 정상적으로 insert 되었을 것이다.
  • 이러한 현상을 부분 업데이트(Partial Update) 라고 한다.

 

 

트랜잭션이 필요한 이유

  • MyISAM의 경우 트랜잭션을 지원하지 않는 경우 작업의 일부만 적용되는 부분 업데이트(Partial Update)가 발생할 수 있다.
  • 이는 곧 데이터 정합성에 문제가 발생할 수 있다.
  • 커머스 서비스에서 다음과 같은 주문-결제 비즈니스 로직이 있다고 가정해보자.
1. (결제) 사용자가 상품을 결제한다.
2. (재고 차감) 구매한 상품의 재고를 1 감소시킨다.
3. (상품 상태) 상품의 남은 재고를 판단해 상품의 상태를 (판매중 or 판매완료)로 변경한다.
  • 사용자가 구매한 상품의 재고는 1개이고, 1~3의 과정을 수행 중 2번 동작에서 오류가 발생하여 2, 3번 동작이 정상적으로 수행되지 않았다고 가정해보자.
  • 이 때, 트랜잭션이 지원되지 않는다면 사용자가 상품을 결제한 1번 과정은 되돌려지지 않는다. (Partial Update)
  • 상품의 재고가 감소되지 않고 상품의 상태가 업데이트 되지 않아 사용자가 결제를 완료하고 이미 판매된 상품임에도 불구하고 상품의 재고는 그대로 1로 남아있고 상품의 상태 역시 판매중으로 남아있게 된다.
  • 재고가 1개인 상품에 대해 복수의 사용자가 제품을 구매하는 상황이 벌어지게 되고, 이는 곧 데이터의 부정합이 일어남을 의미한다.
  • 이러한 부정합을 막기위해 트랜잭션 지원 없이 비즈니스 로직을 짠다면 다음과 같이 매우 복잡해질 것이다.
order() {
    pay(); // 1. 사용자 결제
    if (is_pay_succeed) {
      update_product_quantity();   // 2. 결제 성공 시 상품 재고 차감
      if (is_update_quantity_succeed) {
          update_product_status();    // 3. 재고 차감 성공 시 상품 상태 업데이트
          if (is_update_status_succeed) {
              ... // 3-1. 상품 상태 업데이트 성공 시 이후 로직
          } else {
             rollback_quantity(); // 3-2. 상품 상태 업데이트 실패 시 롤백
             rollback_pay();
          }
      } else {
         rollback_pay(); // 2-1. 재고 차감 실패 시 롤백
      }
    } else {
       throw error // 1-1. 결제 실패 시 에러
    }
}
  • 트랜잭션은 이러한 데이터 정합성을 유지하기 위한 안전한 방법이다.
  • 1~3의 과정을 하나의 원자적인 작업으로 묶어 과정 중 하나라도 실패 시 모든 작업을 rollback 함으로써 데이터 정합성을 유지한다.

 

 

 

트랜잭션 적용 시 주의사항

  • 트랜잭션이 데이터 정합성을 보장한다고 해서 무분별하게 사용해선 안된다.
  • 즉, 트랜잭션의 범위를 정말 필요한 부분에만 사용할 수 있도록 최소화 해야한다.
  • 트랜잭션 작업은 하나의 작업셋을 온전하게 commit하거나 작업 중 오류 발생 시 rollback 하기 위해, 트랜잭션 범위 내에서 데이터베이스 커넥션을 계속 소유하고 있는다.
  • 트랜잭션이 완료되고 commit or rollback 완료 시 커넥션을 반납한다.
  • 일반적으로 커넥션 생성비용을 줄이기 위해 커넥션 풀에 일정량의 커넥션을 생성해두고 보관하는데, 이러한 커넥션은 유한하다.
  • 무분별하게 넓은 범위에 대해 트랜잭션을 설정하게 되면 트랜잭션이 완료될 때까지 커넥션을 계속 가지고 있어 커넥션이 부족해지는 상황이 발생할 수 있다.
  • 커넥션이 모두 점유되어 있으면 이후 요청에 대해서는 커넥션이 반납될 때까지 대기해야 하는 상황이 발생한다.
  • 위 주문-결제 로직에서 몇가지 작업이 더 추가된다고 가정해보자.
==== 트랜잭션 시작 ====
1. (상품 조회) db에서 상품 정보를 조회한다.
2. (결제) 사용자가 상품을 결제한다.
3. (재고 차감) 구매한 상품의 재고를 1 감소시킨다.
4. (상품 상태) 상품의 남은 재고를 판단해 상품의 상태를 (판매중 or 판매완료)로 변경한다.
5. (주문정보 저장) 주문 정보를 저장한다.
6. (주문정보 조회) 저장된 주문정보를 조회한다.
7. (알림 발송) 주문 완료 알림을 발송한다.
8. (알림 기록 저장) 주문 완료 알림 발송내역을 저장한다.
==== 트랜잭션 종료 ====
  • 1~8 까지의 작업에 대해 모두 트랜잭션으로 묶는다면 편하다.
  • 그러나, 1~8까지의 작업이 모두 완료될 때까지 커넥션을 계속 점유하고 있어 커넥션이 부족해질 수 있다.
  • 조회성 작업의 경우, 데이터를 변경하지 않으므로 데이터 정합성을 해치지 않는 작업이다. (트랜잭션 X)
  • 결제-상품업데이트-주문정보저장 작업의 경우, 데이터를 변경하고 세가지 작업에 대한 정합성이 중요하다. (트랜잭션 1)
  • 알림 발송 - 알림 기록 저장 작업의 경우, 결제-상품업데이트-주문정보저장 작업이 완료되었다면 두가지 작업이 실패해도 두가지 작업만 rollback 된다면 전체적인 정합성을 해치지 않는다. (트랜잭션 2)
  • 위를 기반으로 다음과 같이 트랜잭션 범위를 최소화 할 수 있다.
1. (상품 조회) db에서 상품 정보를 조회한다.
==== 트랜잭션1 시작 ====
2. (결제) 사용자가 상품을 결제한다.
3. (재고 차감) 구매한 상품의 재고를 1 감소시킨다.
4. (상품 상태) 상품의 남은 재고를 판단해 상품의 상태를 (판매중 or 판매완료)로 변경한다.
5. (주문정보 저장) 주문 정보를 저장한다.
==== 트랜잭션1 종료 ====
6. (주문정보 조회) 저장된 주문정보를 조회한다.
==== 트랜잭션2 시작 ====
7. (알림 발송) 주문 완료 알림을 발송한다.
8. (알림 기록 저장) 주문 완료 알림 발송내역을 저장한다.
==== 트랜잭션2 종료 ====
  • 트랜잭션 범위를 최소화함으로써 커넥션 반납을 빠르게 이루어지게 할 수 있다.
  • 이를 통해 커넥션 부족 현상을 막고, 커넥션 부족으로 인한 요청 대기를 줄일 수 있다.

 

 

3. 정리

  • 트랜잭션은 데이터베이스에서 작업의 완전성을 보장하기 위한 개념이다.
  • 트랜잭션은 원자적/논리적인 작업셋이 모두 성공하거나, 하나라도 실패하면 모두 되돌린다. (All or Nothing)
  • 트랜잭션은 데이터 정합성을 유지하기 위해 필요하다.
    • 데이터가 유기적으로 연결되어 있는 A-B-C 작업에 대해 A 작업만 성공하고 이후 B-C 작업이 실패한다면 데이터 정합성이 맞지 않는다.
    • 이를 부분 업데이트 (Partial Update)라고 한다.
  • 트랜잭션의 범위를 정말 필요한 부분에만 사용할 수 있도록 최소화하자.
    • 트랜잭션 내 작업을 온전하게 commit or rollback 하기 위해 트랜잭션이 끝날 때까지 커넥션을 점유한다.
    • 트랜잭션의 범위가 넓을수록 커넥션을 점유하는 시간이 길어진다.
    • 커넥션의 수는 유한하므로, 커넥션 점유 시간이 길어져 커넥션이 고갈되면 이후 요청에 대해 커넥션이 반납될 때까지 대기해야 한다.

 


Reference