database

트랜잭션과 고립(격리)수준

하리하링웹 2024. 7. 7. 21:53

트랜잭션이란 무엇인가?

DBMS에서 데이터를 다루는 논리적인 작업의 단위

트랜잭션의 필요성

  • 데이터 복구: 데이터베이스 작업 도중 장애가 발생하면 데이터를 복구하기 위한 단위
  • 작업 분리: 여러 작업이 동시에 같은 데이터를 다룰 때 발생할 수 있는 문제를 방지하기 위해 작업을 분리하는 단위

예시 (ACID)

1. A에서 B계좌로 돈을 입금할 때 → B계좌의 입금 관련 쓰기 작업에 실패 시[Atomicity(원자성)]

트랜잭션이 제대로 되어있지 않다면 A계좌에서 출금한 돈은 없어지게 되어버리며 하나의 트랜잭션이서 두 개의 작업을 실행하고 있을 때 하나의 작업이라도 실패 시 모든 작업이 실패로 돌아가야 함 → 기존 데이터로 롤백 시키는 작업을 해야함


2. 잔액이 1000원 밖에 없는 A계좌에서 B,C 계좌로 동시에 1000원을 이체 하려 할 때 [Consistency(일관성)]

계좌의 잔액은 0원 미만일 수 없기에 이를 위반하는 트랜잭션은 모두 중단되어야 한다. 단 트랜잭션 도중 조건을 위반하더라도 결과값이 정상적인 상태라면 이는 올바른 동작으로 간주해야함.

→ 트랜잭션이 일어난 이후 데이터베이스의 제약, 규칙을 반드시 만족해야함


3. A계좌에서 B계좌로 이체되었을 때 이체 작업은 성공했지만 로깅 작업은 실패 했을 때 [Durability(지속성)]

계좌 이체 내역은 반드시 남아야 하기에 로그를 기록하기 전에 오류로 기록에 실패했다면 해당 이체 내역은 실패로 돌아가고 계좌의 내역 또한 롤백되어야 한다. 트랜잭션 성공 이후 시스템 오류가 나더라도 해당 트랜잭션 기록은 기록은 반드시 남아야 한다.

→ 여러가지 이슈로 인해 트랜잭션 이후 동작이 실패하더라도 트랜잭션에 대한 기록은 반드시 남아야하며 유지되어야 함


4. B,C계좌가 동시에 A 계좌에 입금을 하는 동작을 했다고 가정했을 때[Isolation(격리성)]

두 개의 트랜잭션이 동시에 실행되는데서 오는 문제를 해결하기 위한 isolation

여기서 하나의 쿼리는 operation 이라고 말하며 각 operation의 실행 순서를 schedule

이라고 말한다. 두 개의 트랜잭션 실행 시 operation이 충돌 하는 조건은 아래와 같다.

  • 두 개의 operation은 서로 다른 트랜잭션 소속이다.
  • 두 개의 operation이 같은 데이터에 접근한다.
  • 최소 하나는 write operation 이다.

operation이 겹치지 않고 schedule을 잘 지키며 실행되는것이 데이터가 유실될 일 없이 트랜잭션 단위로 잘 실행되는 이상적인 즉 isolation 속성이 잘 지켜지는 상황이라고 말할 수 있음.

 

해결 방식은 간단한데 operation 단위로 나눠 하나의 operation이 종료되면 다음 operation이 실행되는 방식으로 schedule 정하는 것

이러한 방식을 Serial schedule 이라고 말함. 하지만 여기에는 큰 문제가 있음.

 

만약 대규모 서비스에서 DBMS가 엄청나게 많은 수의 트랜잭션을 처리해야 하는 상황이라면 이를 순서대로 처리해야 하기에 성능에 치명적인 문제가 발생하게 되어버림. → 즉 현실에서 사용하기 어려운 방식.

이러한 문제는 각 트랜잭션 사이사이에 다른 트랜잭션 operation 이 실행되는 Nonserial schedule 이라고 부르는 방식으로 해결할 수 있음. 하지만 이 방법 역시 예시의 근본적인 문제인 operation 의 순서에 따라 다른 결과가 나올 수 있다는 문제가 생김.

이를 해결하기 위한 방법은 Serial schedule과 동일한 결과를 보장하는 Nonserial schedule을 실행하는 것이다. Nonserial schedule의 경우 다양한 스케쥴이 나올 수 있지만 여기서 중요한 것은 충돌이 발생하지 않도록 schedule의 결과가 동일한 즉 Conflict serializable한 스케쥴을 만드는 것이다. 이러한 Conflict serializable한 스케쥴들은 Serial schedule과 결과가 동일하기에 동시에 실행되면서도 정합성을 맞출 수 있는 방법이다.

그렇다면 이러한 방식은 어떤 기준으로 어떻게 구현할까? 그것에 대한 정의가 바로 isolation level(격리 수준)이다.

 

병행 제어

isolation level에 대해 설명하기 전에 먼저 병행 제어에 대해 알고가보자

정의

병행 제어란 트랜잭션이 동시에 수행될 때 데이터베이스의 일관성을 해치지 않고, 타 트랜잭션에 영향을 주지 않도록 각 트랜잭션의 상호작용을 제어하는 것이다.

병행의 문제점

병행 제어를 하지 않으면 아래의 문제점이 발생하게 된다.

  • 갱신 분실(Lost Update): 둘 이상의 트랜잭션이 동시에 갱신할 때 갱신 결과의 일부가 없어지는 현상
  • 모순성(Inconsistency): 하나의 트랜잭션이 여러 데이터 갱신 연산을 수행할 때 일관성 없는 상태의 데이터베이스에서 데이터를 가져옴으로써 데이터의 불일치가 발생
  • 연쇄 복귀(Cascading Rollback): 병행 수행되던 둘 이상의 트랜잭션 중 어느 한 트랜잭션이 롤백 하는 경우 다른 트랜잭션들도 함께 롤백되는 현상
  • 비완료 의존성(Uncommitted Dependency): 하나의 트랜잭션 수행이 실패한 후 회복하기 전에 다른 트랜잭션이 실패한 갱신 결과를 참조하는 현상

병행제어 기법

이런 병행의 문제점을 해결하기 위한 기법들이 있다.

로킹

트랜잭션이 접근하려는 데이터를 다른 트랜잭션이 접근하지 못하도록 잠그는 기법

즉 상호 배제 기능을 제공하여 데이터를 독점적으로 사용할 수 있다. 한 번에 로킹 가능한 데이터의 크기를 로킹 단위라고 말하며 필드, 레코드, 테이블, 파일, 데이터베이스 등 모두 단위가 될 수 있다.

당연하게도 로킹 단위에 따라 성능의 차이가 발생한다.

 

로킹 규약은 아래와 같다.

  1. 트랜잭션 T가 공유데이터 x에 접근하려면 먼저 lock(x)을 해야한다.
  2. 공유데이터를 사용한 T는 반드시 unlock(x)을 해야한다.
  3. 다른 트랜잭션에 의해 lock(x)가 실행되었다면, 트랜잭션 T는 lock(x) 실행이 불가능하다.
  4. 트랜잭션 T가 lock(x) 한 것을 다른 트랜잭션이 unlock(x)할 수 없다.

로킹 기법은 Dead lock이 발생할 수 있다는 문제가 있다.

ex) A,B 트랜잭션이 x,y 데이터에 각각 lock을 건 상태에서 서로 반대의 데이터에 접근하려 할 때

 

이러한 로킹 기법은 읽기만 하는 경우에는 동시에 접근해도 문제가 없기에 이런 상황에 효율적이지 못한 동작을 한다. 이를 해결하는 방식이 2단계 로킹 규약이다.

 

2단계 로킹 규약

2단계 로킹 규약은 트랜잭션 내의 모든 lock 연산이 첫 번째 unlock 연산 이전에 위치해야 하는 규칙이다.

만족 (왼쪽)            |  만족하지 않음 (오른쪽)
-----------------------|-----------------------
T1: lock(X)            |  T1: lock(X)
T1: lock(Y)            |  T1: unlock(X)
T1: ...                |  T1: lock(Y)
T1: unlock(Y)          |  T1: ...
T1: unlock(X)          |  T1: lock(Z)
                       |  T1: unlock(Y)

 

타임스탬프 순서(Timestamp ordering) 기법

비직렬적인 트랜잭션들을 타임스탬프 순서에 따라 직렬화 시키는 기법이다.

데이터에 접근하는 시간이 정해져 있기에 lock을 사용하지 않아 Dead lock이 발생하지 않는다. 다만 Rollback 발생률이 높고 연쇄 복귀를 초래할 확률이 있다.

  1. 각 트랜잭션은 고유의 타임 스탬프를 받고 각 데이터 항목마다 마지막 Read, Write를 한 트랜잭션의 타임스탬프를 기록한다.
  2. 트랜잭션이 데이터 항목에 접근할 때, 접근하려는 데이터 항목의 타임스탬프와 트랜잭션의 타임스탬프를 비교해 순서를 검증한다.

읽기 롤백

  • 트랜잭션 T1의 타임스탬프가 데이터 A의 쓰기 타임스탬프보다 작을 경우 (T1<A) T1 트랜잭션이 시작된 이후 A가 이후에 시작된 트랜잭션에 의해 수정된 것이기에 T1은 롤백되어야 한다.

쓰기 롤백

  • 트랜잭션 T1의 타임스탬프가 데이터 A의 읽기 타임스탬프보다 작은 경우 다른 트랜잭션이 트랜잭션 T1이 시작된 이후 데이터 A를 읽은 것이기 때문에 트랜잭션 T1은 롤백되어야 한다.
  • 트랜잭션 T1의 타임스탬프가 데이터 A의 쓰기 타임스탬프보다 작은 경우, 다른 트랜잭션이 T1이 시작된 이후에 A를 수정한 것이기에 T1은 롤백되어야 한다.

낙관적 병행 제어(Optimistic Concurrency Control)

트랜잭션 수행 동안은 어떠한 검사를 하지 않고 트랜잭션 종료 이후 일괄 검사하는 방식이다.

 

동작 순서

  1. 읽기 단계: 트랜잭션이 데이터베이스에서 데이터를 읽고 로컬 변수에 저장한다.
  2. 검증 단계: 트랜잭션이 완료되기 전에 다른 트랜잭션과의 충돌이 없는지 확인한다. 작업이 일관성이 있는지, 충돌은 없는지 확인한다.
  3. 쓰기 단계: 검증 단계를 충돌이 없으면 데이터베이스에 쓰고 있으면 롤백되거나 다시 시도한다.

이는 읽기 작업이 많은 환경에서 유리하나 충돌이 자주 발생하는 환경에서는 롤백이 많아 문제가 될 수 있다.

다중 버전 병행 제어(Multi-version Concurrency Control(MVCC)

한 데이터에 대해 여러 버전의 값을 유지하며 관리하는 방식이며 타임스탬프 개념을 사용하기에 다중 버전 타임스탬프 기법이라고도 한다.

마찬가지로 타임스탬프 개념을 사용하기에 연쇄 복귀 발생 가능성이 있다.

 

동작 순서

  1. 타임 스탬프 할당: 트랜잭션이 시작되면 해당 트랜잭션에 고유 타임스탬프(혹은 ID)를 할당한다. 이는 버전을 추적하는데에 사용한다.
  2. 읽기 작업: 데이터를 읽을 떄 여러 버전 중 트랜잭션 타임스탬프보다 작은 가장 최신 버전을 선택해 읽는다.
  3. 쓰기 작업 :데이터 항목을 수정 시 기존 데이터 항목을 수정하는 대신 새 버전을 생성한 뒤 타임스탬프를 할당한다. 다른 트랜잭션은 커밋 전까지 이 새로운 버전을 읽지 못한다.
  4. 커밋: 트랜잭션이 성공하면 트랜잭션이 생성한 모든 데이터 버전을 커밋한다.
  5. 가비지 컬렉션: 주기적으로 오래된 데이터 버전, 필요없는 데이터 버전을 제거한다.

이 방식은 읽기 작업이 많은 환경에서 유리하나 다중 버전을 관리하기 위해 필요 없는 데이터 제거를 위한 가비지 컬렉션 매커니즘, 여러 버전을 관리하기 위한 많은 저장 공간이 요구된다.

격리(고립) 수준

정의

여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 여부를 결정하는 수준, 격리 수준은 높은 순서대로 아래와 같이 정의되어 있다.

  • Serializable
  • Repeatable read
  • Read committed
  • Read uncommited

고립 수준에 따라 발생할 수 있는 문제는 아래와 같다.

  • Dirty Read
  • Non-repeatable Read
  • Phantom Read

Serializable

가장 엄격한 격리 수준으로 위에서 말한대로 트랜잭션을 순차적으로 실행한다. 여러 트랜잭션이 동시에 실행되지 않기에 당연히 어떤 데이터 부정합 문제도 발생하지 않지만 성능이 매우 떨어진다.


Repeatable Read

변경 전의 레코드를 Undo 공간에 백업해둔다. 만약 트랜잭션의 롤백이 필요한 경우 데이터를 복원할 수 있을 뿐 아니라 서로 다른 트랜잭션 간 접근할 수 있는 데이터의 세밀한 제어가 가능하다. 한 트랜잭션 내에서 동일한 결과를 보장해주지만, 새로운 레코드가 추가 되는 경우 부정합이 발생할 수 있다.

동작 순서

  1. 트랜잭션 시작: 트랜잭션 T1과 T2가 각각 시작
  2. 스냅샷 생성: 트랜잭션 T1이 처음 데이터를 읽을 때, 데이터베이스는 현재 상태의 스냅샷을 생성. 이후 T1은 이 스냅샷을 기준으로 데이터를 읽음.
  3. 데이터 읽기: T1이 데이터를 읽음. 예를 들어, T1이 테이블 A의 레코드 r1을 읽음.
  4. 다른 트랜잭션에 의한 데이터 수정: 트랜잭션 T2가 테이블 A의 레코드 r1을 수정하거나 삭제. 그러나 T1이 끝날 때까지 T2의 이러한 변경 사항은 T1에게 보이지 않음.
  5. 트랜잭션 종료: T1이 커밋하거나 롤백할 때까지 T1은 시작 시점의 스냅샷을 기준으로 데이터를 계속 읽음.
     +--------------------+   +--------------------+   +--------------------+
     |   트랜잭션 T1      |   |   트랜잭션 T2      |   |  데이터베이스      |
     +--------------------+   +--------------------+   +--------------------+
     |                    |   |                    |   |                    |
     |  1. 트랜잭션 시작   |   |  1. 트랜잭션 시작   |   |                    |
     |--------------------|   |--------------------|   |                    |
     |  2. 데이터 읽기 r1  |   |                    |   |  r1 = 100          |
     |  (스냅샷 시점 100)  |   |                    |   |                    |
     |--------------------|   |  2. 데이터 수정 r1  |   |                    |
     |                    |   |  r1 = 200          |   |                    |
     |  3. 데이터 읽기 r1  |   |  (수정 대기 중)   |   |                    |
     |  (스냅샷 시점 100)  |   |                    |   |                    |
     |--------------------|   |--------------------|   |                    |
     |  4. 트랜잭션 종료   |   |                    |   |  r1 = 200 (변경 반영)|
     +--------------------+   +--------------------+   +--------------------+

문제점

  • Phantom Read

팬텀리드는 트랜잭션 내에서 같은 쿼리를 여러번 수행할 때 각 쿼리의 결과가 다른것을 말한다.

Repeatable Read는 트랜잭션이 읽은 행의 변경, 삭제를 방지할 수 있지만 새로운 행이 추가되었을 때 문제가 된다. 예를들어 T1이 특정 조건을 만족하는 모든 행을 읽은 뒤 T2가 새로운 행을 삽입하였을 때 T1의 동일한 쿼리에서 새로운 행이 나타날 수 있다.

repeatable read는 행단위로 스냅샷을 찍어 읽기 잠금을 사용하여 기존 데이터들의 수정/삭제는 막아주지만 새로운 데이터는 해당 사항이 없기때문에 이러한 문제가 발생한다. 이러한 문제를 해결하면 Repeatable Read보다 높은 격리 수준인 Serializable 격리 수준을 사용해야 한다.


Read Committed

트랜잭션이 실행되는 동안 커밋된 데이터만 읽을 수 있음. 즉, 다른 트랜잭션이 아직 커밋하지 않은 변경사항은 보이지 않음

동작 순서

  1. 트랜잭션 시작: 트랜잭션 T1과 T2가 각각 시작.
  2. 데이터 읽기:
    • 트랜잭션 T1이 데이터를 읽음. 예를 들어, T1이 테이블 A의 레코드 r1을 읽습니다.
    • 이 시점에서 T1은 r1의 현재 커밋된 값을 가져온다.
  3. 다른 트랜잭션에 의한 데이터 수정:
    • 트랜잭션 T2가 테이블 A의 레코드 r1을 수정. 예를 들어, r1의 값을 100에서 200으로 변경
    • T2는 변경 후 커밋합니다.
  4. 변경 사항 반영:
    • T1이 다시 레코드 r1을 읽으면, T2가 커밋한 변경 사항이 반영된 값을 읽습니다.
    • 예를 들어, r1의 새로운 값 200을 읽습니다.
  5. 트랜잭션 종료:
    • 트랜잭션 T1과 T2가 각각 커밋하거나 롤백하며 종료됩니다.
+--------------------+   +--------------------+   +--------------------+
|   트랜잭션 T1      |   |   트랜잭션 T2      |   |  데이터베이스      |
+--------------------+   +--------------------+   +--------------------+
|                    |   |                    |   |                    |
|  1. 트랜잭션 시작   |   |  1. 트랜잭션 시작   |   |                    |
|--------------------|   |--------------------|   |                    |
|                    |   |  2. 데이터 읽기 r1  |   |  r1 = 100          |
|                    |   |  (읽기 시점 100)   |   |                    |
|--------------------|   |--------------------|   |                    |
|  2. 데이터 수정 r1  |   |                    |   |                    |
|  r1 = 200          |   |                    |   |                    |
|--------------------|   |                    |   |                    |
|  3. 데이터 커밋     |   |                    |   |  r1 = 200 (변경 반영)|
|                    |   |                    |   |                    |
|--------------------|   |  3. 데이터 읽기 r1  |   |                    |
|                    |   |  (읽기 시점 200)   |   |                    |
|--------------------|   |--------------------|   |                    |
|  4. 트랜잭션 종료   |   |  4. 트랜잭션 종료   |   |                    |
+--------------------+   +--------------------+   +--------------------+

문제점

  • Phantom Read
  • Non-repeatable Read

동일한 트랜잭션 내에서 같은 쿼리를 두 번 실행했을 때 중간에 다른 트랜잭션이 데이터를 수정, 삭제하면 결과가 달라질 수 있음


Read Uncommitted

가장 낮은 격리수준으로 하나의 트랜잭션이 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있음.

동작순서

  1. 트랜잭션 시작: 트랜잭션 T1과 T2가 각각 시작.
  2. 데이터 수정:
    • 트랜잭션 T1이 테이블 A의 레코드 r1을 수정. 예를 들어, r1의 값을 100에서 200으로 변경.
    • T1은 아직 커밋하지 않습니다.
  3. Dirty Read:
    • 트랜잭션 T2가 테이블 A의 레코드 r1을 가져옴.
    • Read Uncommitted 격리 수준에서는 커밋되지 않은 데이터도 읽을 수 있으므로, T2는 T1이 변경한 r1의 값 200을 가져옴.
  4. 트랜잭션 T1 롤백:
    • 트랜잭션 T1이 롤백되어 r1의 변경 사항이 취소, r1은 다시 100이 됨.
+--------------------+   +--------------------+   +--------------------+
|   트랜잭션 T1      |   |   트랜잭션 T2      |   |  데이터베이스      |
+--------------------+   +--------------------+   +--------------------+
|                    |   |                    |   |                    |
|  1. 트랜잭션 시작   |   |  1. 트랜잭션 시작   |   |                    |
|--------------------|   |--------------------|   |                    |
|  2. 데이터 수정 r1  |   |                    |   |  r1 = 100          |
|  r1 = 200          |   |                    |   |                    |
|--------------------|   |  2. 데이터 읽기 r1  |   |                    |
|                    |   |  (읽기 시점 200)   |   |                    |
|--------------------|   |--------------------|   |                    |
|  3. 트랜잭션 롤백   |   |                    |   |                    |
|  (r1 변경 취소)     |   |                    |   |  r1 = 100 (변경 취소)|
|--------------------|   |--------------------|   |                    |
|  4. 트랜잭션 종료   |   |  3. 트랜잭션 종료   |   |                    |
+--------------------+   +--------------------+   +--------------------+

문제점

  • Non-repeatable Read
  • Phantom Read
  • Dirty Read

하나의 트랜잭션이 다른 트랜잭션의 커밋되지 않은 데이터를 읽을 수 있음 데이터가 롤백 될 때 문제가 발생할 수 있음

참고