[DataBase] 트랜잭션 격리수준 알아보기

klmin·2024년 11월 10일

트랜잭션 격리수준 이란

데이터베이스 트랜잭션에서 격리 수준은 여러 트랜잭션이 동시에 실행될 때 각각의 트랜잭션이 다른 트랜잭션의 중간 결과에 얼마나 영향을 받을지 결정하는 중요한 설정이다.
이를 통해 데이터의 일관성을 유지하고 잠재적인 동시성 문제를 방지할 수 있다.

예를 들어, 두 개 이상의 트랜잭션이 동시에 동일한 데이터를 읽거나 쓰려 할 때 발생할 수 있는 문제를 격리 수준으로 조정하여, 각 트랜잭션이 수행되는 동안 다른 트랜잭션의 영향을 받지 않도록 한다.

트랜잭션 격리 수준의 종류

낮은 수준에서 높은 수준으로 데이터 일관성은 증가하지만 성능이 낮아질 수 있다.

1. Read Uncommitted

  • 가장 낮은 수준의 격리 수준으로 커밋되지 않은 데이터도 읽을 수 있다.

  • Dirty Read가 허용되며 트랜잭션이 완료되지 않은 데이터를 다른 트랜잭션에서 읽을 수 있다.

  • 성능이 높지만 데이터 일관성이 떨어진다.

  • 예시 : 트랜잭션 B가 balance를 1000에서 500으로 변경 중일 때 트랜잭션 A가 이를 읽으면 500으로 조회하게 된다. 이후 트랜잭션 B가 롤백되면 트랜잭션 A는 잘못된 데이터를 읽은 것이 된다.

  • 문제점 : Dirty Read, Non-repeatable Read, Phantom Read 발생 가능.

2. Read Committed

  • 커밋된 데이터만 읽을 수 있다.

  • Dirty Read는 방지되지만 Non-repeatable ReadPhantom Read는 발생할 수 있다.

  • 예시: 트랜잭션 A가 특정 계좌의 balance를 1000으로 조회한 후 트랜잭션 B가 balance를 500으로 변경하고 커밋한다. 트랜잭션 A가 다시 조회하면 balance가 500으로 바뀌어 있다.

  • 문제점 : Non-repeatable Read, Phantom Read 발생 가능.

3. Repeatable Read

  • 트랜잭션이 시작된 이후 조회한 데이터가 변경되지 않도록 보장한다.

  • MVCC와 Undo 로그를 사용하여 트랜잭션 시작 시의 데이터 스냅샷을 유지하고 이를 통해 다른 트랜잭션이 데이터를 수정하더라도 트랜잭션 내에서는 언제나 동일한 데이터를 조회할 수 있다.

  • MVCC와 Undo 로그의 역할

    • MVCC : 트랜잭션 시작 시점의 데이터 스냅샷을 생성하여 일관된 조회를 보장한다.

      • 스냅샷 생성 : MVCC는 트랜잭션이 시작될 때 해당 시점의 데이터를 고정하여 트랜잭션 종료 시까지 동일한 데이터 버전을 읽을 수 있도록 한다. 이를 통해 다른 트랜잭션이 데이터를 변경해도 트랜잭션 시작 시점의 상태가 유지된다.
      • Undo로그와 연계하여 이전 버전 관리 : MVCC는 스냅샷을 위해 Undo 로그에 기록된 이전 데이터 버전을 참조하여 트랜잭션 시작 시점의 데이터를 일관되게 유지한다.
        예를 들어, 트랜잭션 ID가 10인 트랜잭션은 이후의 변경 사항(예: 트랜잭션 ID 11)을 무시하고 트랜잭션 시작 시점의 데이터를 조회한다.
    • Undo 로그 : 데이터 변경 전의 상태를 기록하여 데이터 복구와 일관된 조회를 지원한다.

      • 변경 전 데이터 상태 저장: 트랜잭션이 데이터를 수정할 때 Undo 로그는 수정 전의 데이터를 기록한다. 이를 통해 다른 트랜잭션이 이전 버전을 조회하여 트랜잭션 시작 시점의 일관성을 유지할 수 있다.

      • 스냅샷 조회 시 버전 제공: 트랜잭션이 스냅샷 읽기를 수행할 때 Undo 로그에 저장된 이전 데이터 버전을 참조하여 해당 트랜잭션 시작 시점의 상태를 제공한다.

      • 불변의 데이터 버전 관리: Undo 로그에 기록된 이전 데이터 버전은 다른 트랜잭션이 조회할 때 항상 동일하게 유지되어 시작 시점의 일관된 조회가 가능하도록 한다.

  • Dirty Read와 Non-repeatable Read를 방지할 수 있지만 Phantom Read는 여전히 발생할 수 있다.

  • 예시 : 트랜잭션 A가 balance를 1000으로 조회하고, 트랜잭션 B가 balance를 500으로 변경한 후 커밋해도, 트랜잭션 A는 계속해서 balance를 1000으로 읽게 된다. 그러나 트랜잭션 B가 새로운 레코드를 삽입하면 이 새로운 레코드는 트랜잭션 A에서 조회 시 포함될 수 있어 Phantom Read가 발생할 수 있다.

  • Phantom Read가 발생하는 이유

    • Repeatable Read 격리 수준에서 MVCC와 Undo 로그는 기존 레코드의 데이터 버전을 관리하여 이미 존재하는 데이터가 수정되더라도 트랜잭션 시작 시점의 스냅샷을 기반으로 일관된 조회 결과를 보장한다.
    • 그러나 새로 삽입된 레코드는 이전에 존재하지 않았기 때문에 Undo 로그에 기록된 이전 버전이 없고 MVCC 스냅샷에도 포함되지 않는다.
    • 따라서 다른 트랜잭션이 조건에 맞는 새로운 레코드를 삽입하면 이 새로운 레코드는 현재 트랜잭션의 스냅샷과 무관하게 조회 결과에 포함될 수 있다. 이로 인해 Phantom Read 문제가 발생하게 된다.
MySQL의 InnoDB 엔진에서 Repeatable Read 격리 수준에서 Phantom Read를 방지

MySQL의 InnoDB 엔진은 Repeatable Read 격리 수준에서 Phantom Read 문제를 방지하기 위해 다음과 같은 잠금 메커니즘을 사용한다.

레코드 락: 특정 레코드가 명확히 지정될 때 해당 레코드의 수정과 삭제를 방지한다.

  • 잠금 방식: InnoDB는 레코드 락을 걸 때 해당 레코드의 인덱스 항목에 락을 설정한다. 즉, 인덱스를 기반으로 레코드를 잠그는 방식이다.
    만약 인덱스가 없는 컬럼을 대상으로 쿼리가 실행되면 InnoDB는 개별 레코드에 락을 걸 수 없으므로 테이블 전체에 락(테이블 락)을 설정하게 된다.
  • 사용 목적: 특정 레코드를 명확히 지정할 때 해당 레코드에 대한 독점 잠금을 설정하여 다른 트랜잭션이 이를 수정하거나 삭제하지 못하도록 한다.
  • 예시 : SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
  • 설명: id = 1인 특정 레코드에 대한 락을 설정하지만 실제로는 해당 레코드의 인덱스 항목(id)에 대해 락이 걸리는 방식으로 작동한다.
  • 적용 상황: 정확히 특정 레코드를 지정하는 경우(예: id = 1)에 레코드 락을 설정할 수 있으며 이때 실제로는 해당 레코드의 인덱스 항목에 잠금이 걸린다.

갭 락: 레코드 사이의 공백(갭)에 잠금을 설정하여 새로운 레코드의 삽입을 방지하는 데 사용된다. 이 잠금은 읽기 잠금과 쓰기 잠금으로 설정할 수 있으며 두 가지 방식에 따라 다르게 동작한다.

  • 사용 목적: 범위 내 특정 갭을 잠가서 새로운 레코드가 삽입되는 것을 방지하여 트랜잭션 간 일관성을 유지하고 Phantom Read 문제를 완화한다.
  • 설명: 여러 트랜잭션이 특정 범위 내에서 데이터를 조회할 수 있지만 조회 중에 다른 트랜잭션이 갭 내에 새로운 레코드를 삽입하지 못하게 방지한다.
    갭 락은 읽기 잠금과 쓰기 잠금으로 나뉜다.
  • 공유락(읽기 잠금)
    • 범위 내 데이터를 조회할 수는 있지만 수정, 삭제, 삽입은 방지된다.
    • 예시 : SELECT * FROM accounts WHERE balance BETWEEN 100 AND 200 LOCK IN SHARE MODE;
    • 특징: LOCK IN SHARE MODE를 사용하여 해당 갭에 읽기 잠금을 설정한다. 이로 인해 다른 트랜잭션은 동일 범위를 조회할 수 있지만 삽입, 수정, 삭제를 할 수 없다.
    • 적용 상황: 범위 조건 내에 레코드가 없을 경우 공백 구간에 순수 갭 락만 설정된다.
  • 배타락(쓰기 잠금)
    • 해당 범위 내에서 조회, 수정, 삭제, 삽입을 모두 방지한다.
    • 예시 : SELECT * FROM accounts WHERE balance BETWEEN 100 AND 200 FOR UPDATE;
    • 적용 상황 : FOR UPDATE를 사용하면 해당 범위 내에서 조회, 수정, 삭제, 삽입이 모두 방지된다.
      범위 내 레코드가 없을 경우 공백 구간에 순수 갭 락만 설정된다.
  • 넥스트 키 락과 갭 락
    • 갭 락은 단독으로 사용되기보다는 넥스트 키 락의 일부로 사용된다.
    • 갭 락은 넥스트 키 락에서 범위 내 레코드와 그 앞뒤 갭을 잠그는 데 사용된다.

넥스트 키 락: 레코드 락과 갭 락을 결합한 잠금 방식으로 조회된 레코드와 그 앞뒤 갭에 걸쳐 잠금을 설정하여 레코드의 수정, 삭제 및 갭 내 삽입을 모두 방지한다.

  • 사용 목적: 범위 조회 시 레코드와 갭 전체를 잠가서 트랜잭션 간 일관성을 유지하고 Phantom Read 문제를 방지한다.
  • 예시 : SELECT * FROM accounts WHERE balance >= 300 FOR UPDATE;
  • 적용 상황: 범위 조건 내에 레코드가 있을 경우 넥스트 키 락이 설정되어 레코드와 그 주변 갭을 포함하여 모두 잠금이 걸린다.

따라서 InnoDB의 Repeatable Read 격리 수준에서는 넥스트 키 락을 사용해 조회 범위 내 레코드의 삽입, 삭제, 수정을 모두 막음으로써 Phantom Read 문제를 효과적으로 방지한다.

간단 요약

잠금 종류설명사용 목적예시적용 상황
레코드 락특정 레코드의 인덱스 항목에 락을 걸어 해당 레코드 수정과 삭제 방지특정 레코드를 명확히 지정하고 독점 잠금을 설정하여 일관성 유지SELECT * FROM accounts WHERE id = 1 FOR UPDATE;인덱스 컬럼을 기준으로 명확히 특정 레코드를 지정할 때 해당 인덱스 항목에 락을 설정.
갭 락레코드 사이의 공백 구간(갭)에 잠금을 걸어 새로운 레코드의 삽입 방지범위 내 새로운 레코드 삽입을 방지하여 트랜잭션 간 일관성 유지SELECT * FROM accounts WHERE balance BETWEEN 100 AND 200 LOCK IN SHARE MODE;범위 내에 레코드가 없을 경우 공백 구간에만 락을 걸어 삽입 방지. 조회는 가능.
넥스트 키 락레코드 락과 갭 락을 결합하여 레코드와 그 주변 갭 전체에 잠금 설정조회 범위 전체의 삽입, 수정, 삭제를 막아 Phantom Read 방지SELECT * FROM accounts WHERE balance >= 300 FOR UPDATE;범위 내 레코드가 존재하는 경우, 레코드와 그 앞뒤 갭에 대해 전체적으로 잠금이 설정되어 삽입, 수정, 삭제가 방지.

4. Serializable

  • 가장 높은 격리 수준으로 모든 트랜잭션이 순차적으로 직렬화된 것처럼 실행되는 것을 보장하여 동시성 문제를 완벽하게 방지할 수 있다.

  • 트랜잭션 간 동시성 문제를 완전히 방지하지만 모든 트랜잭션을 직렬화하여 처리해야 하기 때문에 동시성 성능이 가장 낮은 격리 수준이다.

  • 넥스트 키 락을 사용하여 트랜잭션 간 데이터 일관성을 유지하지만 이러한 잠금 방식은 성능 저하를 유발할 가능성이 높다.

  • 동시성 문제가 발생하지 않는 이유

    • 모든 데이터 접근을 직렬화
      • Serializable 격리 수준에서는 각 트랜잭션이 실행될 때마다 해당 트랜잭션이 사용하는 모든 데이터에 대한 읽기와 쓰기 작업을 직렬화된 순서로 실행한다.
      • 한 트랜잭션이 특정 데이터를 조회하거나 수정하는 동안 다른 트랜잭션은 해당 데이터에 접근할 수 없다. 즉, 한 트랜잭션이 작업을 완료하고 커밋하기 전까지 다른 트랜잭션은 해당 데이터에 접근하기 위해 대기한다.
      • 이러한 직렬화 방식은 트랜잭션 처리 순서를 보장해 Dirty Read, Non-repeatable Read, Phantom Read와 같은 동시성 문제를 완벽하게 방지한다.
    • 넥스트 키 락을 통한 범위 보호
      • Serializable 격리 수준에서는 조건에 맞는 레코드와 그 앞뒤 갭까지 포함하여 넥스트 키 락을 설정하여 해당 범위에 새로운 레코드를 삽입하거나 기존 레코드를 수정, 삭제하지 못하게 막는다.
      • 예를 들어, 트랜잭션 A가 balance >= 1000 조건으로 데이터를 조회하면 이 조건에 맞는 모든 레코드와 그 앞뒤 갭에 대해 넥스트 키 락이 걸린다.
      • 다른 트랜잭션은 이 범위 내에 새로운 레코드를 삽입하거나 수정할 수 없으므로 이를 통해 Phantom Read, Dirty Read, Non-repeatable Read와 같은 모든 동시성 문제를 원천적으로 방지한다.
  • 예시 : 트랜잭션 A가 balance가 1000 이상인 레코드를 조회하고 있는 동안 트랜잭션 B는 balance 값에 대해 수정하려고 시도한다. 그러나 트랜잭션 A가 종료되고 커밋될 때까지 트랜잭션 B는 대기해야 하고 수정 작업을 진행할 수 없다.

  • 문제점: 성능 저하 가능성 높음.

트랜잭션 간 발생할 수 있는 대표적인 문제

1. Dirty Read

  • 한 트랜잭션이 아직 커밋되지 않은 데이터를 다른 트랜잭션에서 읽는 상황.
  • 예시: 트랜잭션 A가 값을 변경했지만 커밋되지 않은 데이터를 트랜잭션 B가 읽고 있는 상황.

2. Non-repeatable Read

  • 한 트랜잭션 내에서 같은 데이터를 두 번 읽었을 때 그 사이에 다른 트랜잭션이 데이터를 수정하여 두 번 읽은 값이 달라지는 상황.
  • 예시 : 트랜잭션 A가 데이터를 읽었지만 중간에 트랜잭션 B가 값을 변경하고 트랜잭션 A가 값을 다시 읽었을 때 값이 달라져있는 상황.

3. Phantom Read

  • 한 트랜잭션에서 조건에 맞는 데이터 집합을 조회한 후 다른 트랜잭션이 데이터를 추가하거나 삭제하여 조건에 맞는 데이터의 개수가 달라지는 상황.
  • 예시 : 트랜잭션 A가 조건에 맞는 데이터를 읽었지만 중간에 트랜잭션 B가 조건을 만족하는 데이터를 추가하여 결과가 달라지는 상황

트랜잭션 격리 수준 요약표

격리 수준Dirty Read 발생Non-repeatable Read 발생Phantom Read 발생성능
Read Uncommitted가능가능가능매우 높음
Read Committed불가능가능가능높음
Repeatable Read불가능불가능가능(mysql은 불가능)보통
Serializable불가능불가능불가능낮음
profile
웹 개발자

0개의 댓글