kimyu0218
  • [DB] 트랜잭션 (ACID/격리 수준)
    2024년 02월 05일 03시 09분 44초에 업로드 된 글입니다.
    작성자: @kimyu0218

    트랜잭션이란?

    트랜잭션은 작업의 단위를 의미한다. 트랜잭션은 일관적이어야 하며 다른 트랜잭션에 고립적이어야 한다.

    • 시스템 장애가 발생해도 올바르게 복구하여 데이터베이스의 일관성을 유지해야 한다.
    • 데이터베이스에 동시에 접근하는 프로그램들을 격리시켜야 한다.
    트랜잭션은 논리적인 작업의 단위로, 다수의 연산(쿼리)으로 구성될 수 있다.

     

    ACID

    트랜잭션은

    • 완전히 반영되거나 아예 반영되지 않아야 하고 (원자성)
    • 기존 제약조건을 변경하지 않아야 하며 (일관성)
    • 다른 트랜잭션에 영향을 미치지 않아야 한다. (고립성)
    • 그리고 한 번 변경된 사항은 영구적으로 유지되어야 한다. (지속성)
    ACID 속성을 보장하기 위해서는 강력한 로깅 시스템이 트랜잭션을 적절하게 처리할 수 있어야 한다.

     

    atomicity

    트랜잭션 내의 모든 연산은 성공하거나 (커밋) 하나라도 실패한 경우 취소되어야 (롤백) 한다. 두 개의 연산으로 구성된 트랜잭션을 가정해보자. 

    💸 A계좌에서 B계좌로 100만원 송금하기
    - A계좌에서 100만원 인출하기
    - B계좌로 100만원 입금하기

    송금 작업에서 원자성은 매우 중요하다. 원자성을 유지하지 않으면, 첫번째 연산만 성공한 경우 A계좌에서 돈이 사라졌지만 B계좌로 전달되지 않아 불일치가 발생하게 된다.

    트랜잭션의 패턴
    1. 트랜잭션을 시작한다.
    2. 쿼리를 실행하여 데이터를 조작한다.
    3. 에러가 없다면 트랜잭션을 커밋한다.
    4. 에러가 발생한 경우, 트랜잭션을 롤백한다.

     

    consistency

    트랜잭션은 데이터의 일관성을 유지해야 한다. 앞에서 살펴 본 송금 예시에서 두 계좌의 합산 금액은 일정해야 한다.
     

    isolation

    트랜잭션의 고립성은 여러 트랜잭션이 동시에 실행될 때, 각각의 트랜잭션이 다른 트랜잭션에 영향을 미치지 않아야 함을 의미한다. 이를 이해하기 위해 다음 예시를 살펴보자.

    T1 : A계좌에서 B계좌로 100만원 이체
    T2 : B계좌에서 C계좌로 50만원 이체

    *초기 상태 : A 100만원 / B 100만원 / C 100만원

    두 트랜잭션이 동시에 실행되는 상황에서, T1의 결과로 B계좌의 잔액이 200만원으로 갱신되고, 곧바로 T2가 완료되면서 50만원으로 갱신된다. 트랜잭션의 고립성을 보장하지 않으면 데이터베이스의 일관성이 깨지므로 트랜잭션이 다른 트랜잭션에 영향을 미치지 않도록 조치가 필요하다.

    `A + B + C = 0 + 50 + 150 = 200`으로, 100만원이 증발해버렸다! (트랜잭션 전후의 잔액 불일치)

     

    durability

    트랜잭션의 결과가 데이터베이스에 반영된 후에는 시스템에 장애가 발생해도 해당 결과가 영구적으로 유지되어야 한다. 이를 보장하기 위해서는 데이터 백업, 복구 등의 메커니즘이 필요하다.


    isolation level

    트랜잭션은 데이터베이스의 일관성을 유지한다. 일관성을 보장하기 위해서는 여러 트랜잭션이 동작할 때, 다른 트랜잭션에 영향을 미치지 않도록 하는 것이 중요하다. 고립성을 보장하는 간단한 방법은 한 번에 하나의 트랜잭션만 실행하는 것이지만, 이는 처리 속도를 저하시킨다.
     
    격리 수준은 성능과 일관성 사이 균형을 찾는 데 중요한 역할을 한다. 따라서 어떤 수준의 격리를 제공할지 결정하는 것이 중요하다.

    높은 격리 수준은,,,
    • 데이터 안정성을 보장하지만
    • 교착상태가 발생할 가능성이 높아진다.
    낮은 격리 수준은,,,
    • 높은 동시성으로 성능이 향상되지만
    • 다른 트랜잭션의 변경사항이 반영될 수 있어 일관성 문제가 발생할 수 있다.
    격리 수준 : serializable > repeatable read > read committed > read uncommitted

     

    serializable

    가장 보수적인 락 전략을 이용하여 현재 트랜잭션이 완료될 때까지 다른 트랜잭션이 이 트랜잭션이 읽은 레코드를 변경하지 못하게 막는다. 즉, 현재 트랜잭션이 읽은 데이터에 락을 걸어 다른 트랜잭션에서 동시에 해당 데이터를 수정하지 못하도록 보장한다.
     

    repeatable read

    InnoDB의 기본 격리 수준

    같은 트랜잭션에서 연속된 읽기 작업이 발생하는 경우, 처음 읽을 때의 스냅샷을 계속 활용한다. 즉, 동일한 트랜잭션 내에서 다수의 `SELECT`문을 실행하더라도 일관된 결과를 반환한다.
     
    serializable과 비슷하지만 락의 범위에서 차이가 나타난다. 직원 정보를 저장하는 테이블이 있다고 가정하자. 각 레코드는 자신이 속한 부서를 나타내는 `department_id` 열을 갖고 있다.

    부서 ID가 100인 직원들 조회하기

    serializable은 부서 ID가 100인 직원들을 수정하려는 시도를 모두 차단하고, gap*에도 락을 걸어 해당 부분에 새로운 직원을 추가하는 시도도 차단 (범위 잠금) 한다. 그에 비해 repeatable read는 인덱스에만 락을 걸어 다른 트랜잭션이 현재 트랜잭션에서 선택된 레코드들만 변경하지 못하도록 제한한다.

    *gap : 특정 범위에 위치한 빈 공간

    serializable은 위와 같이 높은 격리 수준을 제공하여 phantom read*가 발생하지 않지만, repeatable read는 phantom read가 발생할 수 있다.

    *phantom read : 한 트랜잭션이 동일한 쿼리를 두 번 실행했을 때, 두 번의 실행 사이에 다른 트랜잭션이 레코드를 삽입/수정/삭제하여 두 번의 실행 결과가 다르게 나타나는 현상

     

    read committed

    다른 트랜잭션이 커밋한 데이터는 읽을 수 있지만 다른 트랜잭션에서 변경 중인 데이터 (더티 데이터) 는 확인할 수 없다. 하지만 동일한 트랜잭션 중에 다른 트랜잭션이 완료되면, 동일한 읽기 작업에 대해 동일한 결과를 보장하지 않는다.
     

    read uncommitted

    read committed와 달리, 다른 트랜잭션에서 수정 중인 데이터를 읽을 수 있다. 이로 인해 데이터의 일관성이 보장되지 않기 때문에 주의해서 사용해야 한다.


    lost update problem

    dirty read

    더티 리드는 다른 트랜잭션의 아직 커밋되지 않은 데이터를 읽어오는 것을 의미한다. 이는 주로 read uncommitted 격리 수준에서 발생한다. 더티 리드는 ACID 원칙을 준수하지 않는, 아주 위험한 동작이다. 왜냐하면 데이터가 롤백되거나 커밋되기 전에 변경될 수 있기 때문에 정확성이 확인되지 않은 데이터를 사용하기 때문이다.
     

    non-repeatable read problem

    반복 불가능한 읽기는 한 트랜잭션이 동일한 행을 두 번 이상 조회할 때 그 사이 다른 트랜잭션이 해당 행을 변경하고 커밋하여 결과가 바뀌는 현상을 의미한다. 즉, 한 트랜잭션이 동일한 쿼리를 실행했을 때 결과가 일관되지 않는 경우를 의미한다. 이는 read committed나 read uncommitted 같은 낮은 격리 수준에서 발생한다.
     

    phantom* read problem

    한 트랜잭션이 동일한 쿼리를 두 번 이상 실행할 때, 그 사이 다른 트랜잭션이 새로운 행을 삽입하거나 제거하여 결과가 달라지는 현상을 의미한다. 즉, 동일한 쿼리를 실행했을 때 예상치 못한 새로운 레코드가 나타나는 경우를 말한다.

    *phantom : 이전 쿼리에서는 조회되지 않았지만 이번에 새롭게 나타난 레코드

    참고자료

    댓글