트랜잭션(Transaction)

윤태규·2024년 1월 8일

01. 트랜잭션(Transaction) (1)

  • 1) 트랜잭션(Transaction)이란 무엇인가 💡 **트랜잭션(Transaction)**은 **작업의 완전성**을 보장해주기 위해 사용되는 개념입니다. 특정한 작업을 **전부 처리**하거나, **전부 실패**하게 만들어 **데이터의 일관성을 보장**해주는 기능입니다. **트랜잭션(Transaction)**을 사용하는 대표적인 이유는 작업의 단위를 하나의 쿼리에 종속하는 것이 아닌, **여러개의 작업(쿼리)**을 묶어 **하나의 작업 단위**로 **그룹화**하여 처리하는 작업을 뜻합니다. 대부분의 데이터베이스가 트랜잭션의 특징을 이용하고 있으며, MySQL뿐만 아니라 [AWS DynamoDB](https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/transaction-apis.html), [MongoDB](https://www.mongodb.com/docs/manual/core/transactions/), [CockroachDB](https://www.cockroachlabs.com/docs/stable/transactions.html#sql-statements) 등 다양한 데이터베이스에서도 트랜잭션의 개념을 확인할 수 있습니다. 😊
  • 2) 트랜잭션을 왜 사용해야하는가? 여러분들은 트랜잭션을 이용해 다양한 문제 상황들을 해결할 수 있게됩니다. 예를들어 은행에서 계좌이체를 하게 될 경우 아래와 같은 상황이 발생하게 됩니다. 1️⃣  A 고객의 계좌에서 1,000원을 차감합니다. 2️⃣  B 고객의 계좌에 1,000원을 추가합니다. 여기서 1️⃣ 번 작업 이후 2️⃣ 번 작업을 수행하던 중 에러가 발생하게 될 경우 A 고객의 계좌에서 1,000원만 차감되기만 하는 문제점이 발생하게됩니다. 만약 순서가 반대로 되었다면, B 고객의 계좌에 1,000원이 증가되기만 하는 문제가 발생하겠죠? 🙂 이런 부분 업데이트(Partial Update)와 같은 상황을 방지하기 위해 트랜잭션(Transaction)이라는 개념을 도입하게 되었습니다. 단순히 위와 같은 상황 외에도 작업한 내역을 저장하는 로깅작업, 영화관의 예매 시스템, 은행의 결제 시스템데이터의 일관성을 유지해야하는 다양한 상황에서 사용하게 됩니다. 🔥 결국, **트랜잭션(Transaction)**을 이용한다면 사용자가 **항상 어플리케이션 실행을 완료**하도록 구성할 수 있게되고, 실행을 중단할 만한 **치명적인 오류**가 발생하더라도, DB에 피해가 가지않아 더욱 안전하게 어플리케이션을 구성할 수 있게 됩니다.
  • 3) 트랜잭션의 특징(ACID) 💡 **트랜잭션의 특징(ACID)**은 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 특징들을 나열해 놓은 개념입니다. - **ACID**는 **트랜잭션**을 이용하여 데이터베이스를 더욱 **안전하게 구성**할 수 있게 도와주는 **트랜잭션 특징** 4가지의 **맨 앞단어**를 하나씩 가져와 만든 것 입니다. ![출처: [databricks](https://www.databricks.com/kr/glossary/acid-transactions)](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e57e75c0-2960-4a62-ba57-ce18c64c85ba/transaction_ACID.png) 출처: [databricks](https://www.databricks.com/kr/glossary/acid-transactions) **ACID**는 트랜잭션의 4가지 특징을 나타냅니다. - **원자성(Atomicity)** - **일관성(Consistency)** - **격리성(Isolation)** - **지속성(Durability)**
  • 4) 원자성(Atomicity) 💡 **원자성(Atomicity)**은 트랜잭션 내에서 실행되는 명령들을 하나의 묶음으로 처리하여, 내부에서 실행된 명령들이 **전부 성공**하거나, 아니면 **모두 실패**해야한다는 특징입니다. → 여기서, “**원자성**”이란, **나눠질 수 없는 단일 작업**이라는 것을 의미합니다. 원자성(**Atomicity**)은 트랜잭션의 가장 대표적인 특징입니다. 트랜잭션에서 실행되는 쿼리마다 하나의 작업 단위로 보지않고, **여러개의 작업들을 묶어 하나의 작업단위**로 보게됩니다. 📌 **계좌이체의 비즈니스 로직** 1️⃣  **A 계좌**의 금액을 **차감**합니다. 2️⃣  **B 계좌**의 금액을 **증가** 시킵니다. 만약 여러분들은 트랜잭션을 사용하게 될 경우 **2단계**로 구분된 비즈니스 로직인 고객의 **계좌 금액 차감**과 **금액 증가**를 **하나의 계좌이체라는 기능**으로 묶어서 관리할 수 있게 될 것입니다. 결국, **원자성**이라는 특징을 이용해 여러분들은 각각의 쿼리를 별도로 실행하는 것이 아니라, **동시에 실행해야하는 여러개의 쿼리를 묶어서 관리**할 수 있게 될 것입니다. 이렇게 한다면, 여러개의 비즈니스 로직을 상세하게 알지 않더라도 트랜잭션을 활용하여 **비즈니스 로직을 전체적으로 관리**할 수 있게 될 것입니다. 😊
  • 5) 일관성(Consistency) 💡 **일관성(Consistency)**은 트랜잭션 내부에서 처리되는 **데이터의 일관성**을 **유지**해야하는 특징입니다. 만약 작업이 **성공**할 경우 **아무런 문제가 발생하지 않고**, **실패**하더라도 작업을 진행하던 도중 **실패한 상태로 데이터를 방치하지 않는 특징**입니다. **일관성(Consistency)**의 특징은 트랜잭션 내의 **데이터**는 **일관**되어야하며, 에러가 발생하더라도 **데이터의 상태가 일관성을 유지**해야 한다는 특징입니다. 만약 **일관성**이 지켜지지 않을 경우 여러분들은 트랜잭션을 이용하더라도 **언제 데이터가 파손될 지** 모르는 불안감을 가진 체 작업을 해야할 것 입니다. 🥲 📌 **강의 등록의 비즈니스로직** 1. **강의(Courses)** 테이블에서 강의를 생성합니다. 2. 생성된 강의 데이터를 참조하는 **강의 목록(CourseUnit)** 테이블에 강의 영상을 **업로드**합니다. 3. **강의 목록(CourseUnit)** 테이블에 모든 강의 영상을 업로드 하였다면 `COMMIT` **첫번째** 강의 테이블에 **강의를 생성**하는것은 **성공**하였지만 **두번째** 강의 목록 테이블에서 **모든 강의 영상을 삽입**하는데 **실패**하였다면, `ROLLBACK`이 실행되어 **강의(Courses)** 테이블과 **강의 목록(CourseUnit)** 테이블에는 **트랜잭션 시작 전 상태로 복구**됩니다. 이렇게, 데이터는 **강의 자체가 생성되지 않거나**, **모든 강의가 업로드 되는** 상황이 발생하게 되어야, 일관성을 유지할 수 있을 것이고, 데이터베이스를 믿고 작업을 진행할 수 있게 될 것 입니다.
  • 6) 격리성(Isolation) 💡 **격리성(Isolation)**은 트랜잭션이 실행 중인 경우 다른 트랜잭션에 의해 **데이터가 변경되는 것을 방지**하는 특징입니다. 트랜잭션이 **완전히 수행되거나 완전히 수행되지 않은 상태**를 외부에서 **참조**할 수는 있지만, 트랜잭션의 **중간 과정이나 중간 결과를 볼 수 없도록** 하는 특징입니다. **격리성(Isolation)**의 경우, MySQL에서는 사용중인 DB 오브젝트에 **락(Lock)**을 걸어 격리성을 구현하게 됩니다. 여기서 **락(Lock)**을 건 상태는 DB에 접속한 **또다른 클라이언트**가 해당 DB 오브젝트를 **읽거나, 사용**할 수 없도록 방지하여, **데이터 무결성을 보장**하게 됩니다. **→ 격리성**이란 특징에서 **동시성(Concurrency)**과 **격리 수준(Isolation Level)**라는 두 가지 중요한 개념이 나타나게되었습니다. - **❓ 동시성(Concurrency)이란 무엇일까요?** 💡 **동시성(Concurrency)**은 **여러** 클라이언트가 동시에 **하나의 데이터**를 **사용**및 **공유** 하는 것을 뜻합니다. 동시성은 **다수의 사용자가 동일한 시스템을 공유**하면서 발생하는 동시 접근 문제를 해결해야 합니다. **[동시성 문제(Concurrency Issue)](https://www.ibm.com/docs/en/db2/11.1?topic=design-concurrency-issues)**는 여러 클라이언트가 동시에 같은 데이터를 접근하려고 할 때 발생합니다. 📌 2개의 계좌이체가 발생한다고 가정해보겠습니다. 1. 1️⃣  **트랜잭션**에서 A 계좌에서 **천원을 차감**하여 `10,000` → `9,000`으로 수정하였습니다. 2. 2️⃣  **트랜잭션**에서 아직 커밋 되지 않은 1️⃣ 트랜잭션의 데이터를 바탕으로 A 계좌에서 **천원을 차감**하여 `9,000` → `8,000`으로 수정하였습니다. 3. 1️⃣  **트랜잭션**에서 에러가 발생하여, 트랜잭션을 `ROLLBACK`하게 되었습니다. ㄷ따라서, A 계좌의 잔고는 원래대로 `10,000`이 되었습니다. 4. 그런데 2️⃣  **트랜잭션**에서 잘못된 데이터를 바탕으로 작업을 수행한 후 트랜잭션을 `COMMIT` 하게 되었습니다. 그로인해 A 계좌의 잔고가 `8,000`으로 잘못 표시되는 문제가 발생하였습니다. 위의 예시에서 볼 수 있듯이, **동시에** 여러 트랜잭션이 **동일한 데이터에 접근**할 때는 데이터의 일관성을 유지하기 어려울 수 있습니다. 이러한 문제를 해결하기위해, 자원을 사용하는 **하나의 클라이언트**만 해당 자원을 **점유**할 수 있도록 하여, 다른 사용자가 접근할 수 없도록 만들어 **자원을 공유하는 원인을 제거**하면 됩니다. 이것을 **[자원 잠금(Resource Locking)](https://www.ibm.com/docs/en/zvse/6.2?topic=SSB27H_6.2.0/fa2sf_resource_locking.htm)**라고 부르며, **락(Lock)**이라는 개념이 나오게 된 것 입니다. **락(Lock)**은 하나의 트랜잭션에서 **사용 중인 데이터를 잠그는 방식**으로, 다른 트랜잭션들이 그 데이터에 접근하지 못하도록 합니다. 이 방식을 사용한다면, 어떤 트랜잭션도 다른 트랜잭션의 **중간 상태를 볼 수 없게 되므로 데이터 일관성을 유지**할 수 있게 될 것입니다! 😎 **→ 락(Lock)에 대한 개념은 다음 섹션에서 확인해보도록 하겠습니다!**
  • 7) 지속성(Durability) 💡 **지속성(Durability)**은 트랜잭션이 성공적으로 커밋된 후, 해당 트랜잭션에 의해 생성 또는 수정된 데이터가 **어떠한 상황에서도 보존되는 특징**입니다. 다시 말해, 트랜잭션이 완료되면 결과는 **데이터베이스에 영구적으로 저장**되며, 이후 시스템에 어떠한 문제가 생기더라도 데이터는 손상되지 않습니다. **지속성(Durability)**은 트랜잭션의 안전성을 보장하며, 데이터 손실 없이 시스템의 안정성을 유지하는데 중요한 역할을 담당합니다. 트랜잭션이 성공적으로 완료되면, 해당 트랜잭션에 의해 생성 또는 변경된 데이터는 데이터베이스에 `COMMIT` 명령을 통해 **영구적으로 저장**됩니다. 하지만, 트랜잭션 수행 도중 시스템이 **비정상 종료**되더라도, 시스템은 **트랜잭션 로그(Transaction Log)**를 통해 아직 커밋되지 않은 트랜잭션을 복구할 수 있습니다.

02. 트랜잭션(Transaction) (2)

👉 이번 섹션은 트랜잭션의 **원론적인 개념**에 대해 다룹니다.

따라서 깊게 고민 하지 않고, “아 이런 내용이 있구나” 정도로만 간단히 이해하고 넘어가셔도 좋습니다. 😉

  • 1) MySQL의 트랜잭션 살펴보기 MySQL에서 트랜잭션은 아래와 같은 명령어로 사용합니다.
    -- 트랜잭션을 시작합니다.
    **START TRANSACTION;**
    
    -- 성공시 작업 내역을 DB에 반영합니다.
    **COMMIT;**
    
    -- 실패시 START TRANSACTION이 실행되기 전 상태로 작업 내역을 취소합니다.
    **ROLLBACK;**
    트랜잭션을 사용하는 문법을 확인해보았으니,
    실제로 MySQL에서 트랜잭션을 사용하기 위한 간단한 예시를 실행해볼까요? 🙂
    • [코드스니펫] MySQL의 트랜잭션 예시코드

      ```sql
      -- SPARTA 테이블을 생성합니다.
      CREATE TABLE IF NOT EXISTS SPARTA
      (
          spartaId      INT(11)      NOT NULL PRIMARY KEY AUTO_INCREMENT,
          spartaName    VARCHAR(255) NOT NULL,
          spartaAddress VARCHAR(255) NOT NULL
      );
      
      -- 1번째 트랜잭션을 실행합니다.
      START TRANSACTION;
      
      -- SPARTA 테이블에 더미 데이터 3개를 삽입합니다.
      INSERT INTO SPARTA (spartaName, spartaAddress)
      VALUES ('SPARTA1', 'SEOUL'),
             ('SPARTA2', 'BUSAN'),
             ('SPARTA3', 'DAEGU');
      
      -- 1번째 트랜잭션을 DB에 적용합니다.
      COMMIT;
      
      -- 2번째 트랜잭션을 실행합니다.
      START TRANSACTION;
      
      -- SPARTA 테이블에 더미 데이터 3개를 삽입합니다.
      INSERT INTO SPARTA (spartaName, spartaAddress)
      VALUES ('SPARTA4', 'SEOUL'),
             ('SPARTA5', 'BUSAN'),
             ('SPARTA6', 'DAEGU');
      
      -- 2번째 트랜잭션을 롤백합니다.
      ROLLBACK;
      
      -- 테이블의 상태를 확인합니다.
      SELECT * FROM SPARTA;
      ```

      스크린샷 2023-02-02 오후 6.36.26.png

      예시 코드를 실행할 경우, 2번째 트랜잭션에서 수행하는 INSERT INTOROLLBACK되었기 때문에 실제 SPARTA 테이블은 6개가 아닌, 3개의 데이터만 삽입되어 있는 상태로 존재하게 됩니다.

      📌 현재 예시코드는 단순하게 트랜잭션의 `COMMIT`과 `ROLLBACK`이 어떤식으로 동작하는지를 확인하기 위해 사용해보았습니다. 다음부터는 트랜잭션의 구성요소와 트랜잭션을 사용했을 때 발생하는 문제에대해 하나씩 알아보도록 하겠습니다. 😊
  • 2) 락(Lock) 💡 **락(Lock)**은 **동시성을 제어**하기 위해 사용하는 기능입니다. 해당하는 **데이터를 점유**하여 다른 트랜잭션의 접근을 막아 **동시성**과 **일관성**의 **균형**을 맞추기 위해 사용합니다. **하나의 데이터**를 **여러 사용자**들이 **동시에 변경**하려고 할 때, **락**이 존재하지 않다면, 한번에 여러번의 수정이 발생하게되고, 최종 수정된 **결과값을 인지할 수 없게 되는 상황**으로 인해 데이터베이스의 일관성이 깨지게 됩니다. 이런 상황을 방지하기 위해 데이터베이스에서는 **락(Lock)**이라는 기능을 지원하게 되었습니다.
  • 3) 락(Lock)의 종류
    • 공유 락(Shared Locks) | 읽기 락(READ Locks)
      • 다른 트랜잭션이 데이터를 읽는 것은 허용하지만, 수정하는 것을 금지합니다.
      • **READ 전용 락**이라고 불리기도 하며, 해당 락을 사용하는 트랜잭션이 모든 작업을 수행하였다면 공유 락은 해제됩니다.
      • 예시 SQL
        # 트랜잭션을 시작합니다.
        START TRANSACTION;
        
        # SPARTA 테이블을 조회할 때, 해당 데이터들에 공유 락을 설정합니다.
        SELECT * FROM SPARTA LOCK IN SHARE MODE;
    • 배타 락(Exclusive Locks) | 쓰기 락(WRITE Locks)
      • 다른 트랜잭션이 데이터를 읽거나, 수정하는 것을 금지합니다.
      • **WRITE 전용 락이라고 불리며, 트랜잭션이 해당하는 데이터를 점유한 후 다른 트랜잭션이 해당 데이터에 접근 할 수 없도록** 만듭니다.
      • 예시 SQL
        # 트랜잭션을 시작합니다.
        START TRANSACTION;
        
        # SPARTA 테이블을 조회할 때, 해당 데이터들에 배타 락을 설정합니다.
        SELECT * FROM SPARTA FOR UPDATE;
  • 4) 락킹 수준(Locking Level)
    • 글로벌 락(Global Locks) | 데이터베이스 락(Database Locks)

      • 데이터베이스의 모든 테이블에 락을 걸어, 현재 트랜잭션을 제외한 나머지 트랜잭션들이 모든 테이블을 사용할 수 없도록 만듭니다.
      • 가장 높은 수준의 락을 가지고 있으며, 가장 큰 범위를 가지고 있습니다.
      • 예시 SQL
        # 글로벌 락을 획득합니다.
        # MySQL 서버에 존재하는 모든 테이블에 락을 겁니다.
        FLUSH TABLES WITH READ LOCK;
    • 테이블 락(Table Locks)

      • 다른 사용자가 작업중인 테이블을 동시에 수정하지 못하도록 합니다.
      • 예시 SQL
        # SPARTA 테이블에 테이블 락을 설정합니다.
        LOCK TABLES SPARTA READ;
    • 네임드 락(Named Locks)

      • 테이블이나 테이블의 행과같은 DB 오브젝트가 아닌, 특정한 문자열점유합니다.
      • 예시 SQL
        # sparta_name 문자열을 획득합니다.
        # 만약, 10초 동안 획득 하지 못한다면, NULL을 반환합니다.
        SELECT GET_LOCK('sparta_name', 10);
    • 메타데이터 락(Metadata Locks)
      - 다른 사용자가 작업중인 테이블의 동일한 행 및 동일한 데이터베이스의 객체를 동시에 수정하지 못하도록 합니다.
      - 예시 SQL

          ```sql
          # 테이블 구조를 변경할 때, MySQL은 내부적으로 메타데이터 락을 설정합니다.
          ALTER TABLE SPARTA ADD COLUMN Age Int;
          ```
          
      📌 락은 다양한 **락킹 수준(Locking Level)**을 가지고 있는데, 잘못된 락 설정을 하게 될 경우 여러분들은 모든 API가 동작하지 않는 **교착 상태(Dead Lock)**가 발생하게 되어, 프로그램이 멈춰버리는 문제가 발생하게 될 수 있습니다.
    • ❓ 교착 상태(Dead Lock)를 해결하기 위해선 어떻게 구성해야할까요?

    💡 **교착 상태(Dead Lock)**는 여러 테이블에 **락(Lock)**을 적용하여, 다른 작업이 처리되지 못하게 점유하고 있는 작업이 있을 때, 다른 작업을 끝나는 것을 무한정 기다리는것을 나타냅니다.
    

    ![출처: [sqlhack.com](https://www.sqlshack.com/understanding-the-deadlock-definition-in-sql-server/)](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/88e5dd69-0c9a-4d1f-a027-57830e276d0f/deadlock.png)
    
    출처: [sqlhack.com](https://www.sqlshack.com/understanding-the-deadlock-definition-in-sql-server/)
    

    💡 **교착 상태**의 경우 아래와 같은 **2개의 트랜잭션이 동시에 실행**된다고 가정해보겠습니다.
    
     1️⃣  A→ B 테이블을 순차적으로 사용하는 트랜잭션
    
     2️⃣  B→ A 테이블을 순차적으로 사용하는 트랜잭션
    

    가장 먼저 1️⃣ 은 처음 **A 테이블**을 점유하기 위해 **락**을 걸어놓을 것 입니다. 그리고 동시에 2️⃣  또한 **B 테이블**을 점유하기 위해 **락**을 걸어놓을 것 입니다.
    
    그러면 1️⃣ 은 **B 테이블**의 락이 풀리기를 기다리는 상태가 발생하게 될 것이고, 2️⃣  또한 **A 테이블**의 락이 풀리기를 기다리는 상태가 발생하게 될 것 입니다. 이처럼 리소스를 접근하려고 할 때, 서로가 서로의 리소스를 점유하고 있을 때 발생하는 것을 **교착 상태(Dead Lock)**이라고 부릅니다.
    
    해당하는 상황을 해결하기 위해 여러분들은 트랜잭션에서 사용하는 **락(Lock)**의 수준을 명확하게 이해하고, 적재적소에 필요한 락의 수준을 설정하여 트랜잭션을 구성해야합니다.
    
  • 5) 트랜잭션의 격리 수준 (Isolation Level)

💡 트랜잭션의 격리 수준 (Isolation Level)은 여러 트랜잭션이 동시에 처리될 때 다른 트랜잭션에서 변경 및 조회하는 데이터를 읽을 수 있도록 허용하거나 거부하는 것을 결정하기 위해 사용하는 것 입니다.

→ 여기서, 중요한 점은 **‘데이터의 일관성’**과 **‘동시성 처리 성능’** 사이에서 **균형**을 잡는 것입니다.


트랜잭션의 격리 수준은 대표적으로 4가지로 나타냅니다.

`**READ UNCOMMITTED**`

- **커밋 되지 않은 읽기(Uncommitted Read)**를 허용하는 격리 수준입니다.
- 가장 낮은 수준의 격리수준이며, 락을 걸지 않아 동시성이 높지만 일관성이 쉽게 깨질 수 있습니다.

`**READ COMMITTED**`

- **커밋 된 읽기(Committed Read)**만을 허용하고, `SELECT` 문을 실행할 때 **공유락**을 겁니다.
- 다른 트랜잭션이 데이터를 **수정하고 있는 중**에는 **데이터를 읽을 수 없어** 커밋되지 않은 읽기현상이 발생하지 않습니다.

**`REPEATABLE READ`**

- 읽기를 마치더라도 **공유락**을 **풀지 않으며**, 트랜잭션이 **완전히 종료될 때 까지 락을 유지**합니다.
- 공유락이 걸린 상태에서 데이터를 수정하는 것은 불가능하지만, 데이터를 삽입하는 것이 가능해집니다. 그로인해 **팬텀 읽기**가 발생할 수 있는 문제점이 있습니다.

`**SERIALIZABLE**`

- 데이터를 읽는 동안 다른 트랜잭션이 해당 데이터를 읽거나 삽입할 수 없고, 새로운 데이터를 추가하는 것 또한 불가능합니다.
- **가장 높은 수준의 격리 수준**이므로, 동시성이 떨어지는 문제점이 존재합니다.

- **❓ 커밋되지 않은 읽기(Uncommitted Read)는 무엇일까요?**
    
    💡 **커밋되지 않은 읽기(Uncommitted Read)**는 다른 트랜잭션에 의해 작업중인 데이터를 읽게 되는 것을 나타냅니다. 만약 커밋되지 않은 읽기가 발생할 경우, **의도치 않은 데이터를 참조**하게 되어 **데이터의 일관성이 깨지게 되는 상황**이 발생하게됩니다.
    

- **❓ 팬텀 읽기(Phantom Read)란 무엇일까요?**
    

    💡 트랜잭션을 수행하던 중 **다른 트랜잭션**에 의해 **삭제된 데이터**를 **팬텀행(Phantom Rows)**이라고 합니다. 여기서, **팬텀행**에 해당하는 데이터를 읽는 것을 **팬텀 읽기(Phantom Read)**라고 부릅니다.
profile
끝까지 가자

0개의 댓글