[Database] 트랜잭션 격리 수준(Transaction Isolation Level)

이 글은 최범균 님의 유튜브 “프로그래밍 초식 : DB 트랜잭션 조금 이해하기 02 격리”
시청하고 작성되었습니다.

 

같은 데이터에 동시 접근?

  • 동시성은 초심자가 놓치기 쉬운 문제를 발생시킴
    • ex) 당직 담당자를 최소 1명은 유지해야 한다. 2명의 담당자 A, B가 존재한다.
      • A가 담당자 수를 SELECT ⇒ 결과: 2
      • B가 담당자 수를 SELECT ⇒ 결과: 2
      • A는 자신이 빠져도 최소 1명을 만족하므로 자신을 당직에서 제거하도록 UPDATE
      • B가 자신이 빠져도 최소 1명을 만족하므로 자신을 당직에서 제거하도록 UPDATE
      • 결과적으로 남은 당직자는 0명. 최소 1명을 유지해야 한다는 비즈니스 로직 위반
      • 왜? A와 B가 동시에 같은 데이터(당직자 수)에 접근했기 때문
  • 따라서 트랜잭션을 격리하여 이러한 동시성 문제를 해결해야 함
  • 가장 간단한 방법은 각 트랜잭션이 순서대로 실행되도록 하는 것
    • 그러나 이 방법은 성능(처리량) 저하
  • 격리 정도와 성능은 Trade-off이기 때문에, 적절한 격리 수준을 설정하는 것이 중요하다.
    • 사용하는 DB에서 Default로 설정된 격리 수준이 무엇인지, 어떤 격리 수준을 지원하는지 아는 것 또한 중요하다.
  • 격리 수준은 다음의 4가지가 지원된다.
    • Read Uncommitted
    • Read Committed
    • Repeatable Read
    • Serializable

 

동시성과 관련된 다양한 문제들

동시성과 관련하여 발생할 수 있는 주요한 문제로는 아래의 것들이 있다.

  • 커밋되지 않은 데이터 읽기 (dirty read)
    • 트랜잭션 중인 데이터를 누군가 읽은 경우
  • 커밋되지 않은 데이터 덮어쓰기 (dirty write)
    • 트랜잭션 중인 데이터에 누군가 쓴 경우
  • 읽는 동안 데이터 변경 (1)
    • 같은 데이터를 여러 곳에서 읽고 수정하여 읽는 시점에 따라 값이 바뀌는 경우
  • 읽는 동안 데이터 변경 (2)
    • 같은 row는 아니지만 같은 table 내 데이터를 수정해 특정 로직(count, sum)이 비정상적으로 수행되는 경우
  • 변경 유실
    • 누군가 한 데이터에 거의 동시에 쓴 경우 나중에 쓰인 것이 반영되어 먼저 쓰인 데이터가 유실 ex) 위키 내용 수정

이 문제들을 해결하는 데에 다음의 격리 수준을 이용할 수 있다.

  • 커밋되지 않은 데이터 읽기 (dirty read) ⇒ Read Committed
  • 커밋되지 않은 데이터 덮어쓰기 (dirty write) ⇒ Read Committed
  • 읽는 동안 데이터 변경 (1) ⇒ Repeatable Read
  • 읽는 동안 데이터 변경 (2) ⇒ Serializable
  • 변경 유실

 

Read Uncommitted

  • 커밋되지 않은 데이터도 읽을 수 있음
  • 동시성 문제를 전혀 해결할 수 없기 때문에 잘 사용되지 않는다.

 

Read Committed

  • 커밋된 데이터만 읽을 수 있음
    • 커밋된 값, 트랜잭션 진행 중인 값을 별도로 보관
    • 트랜잭션 진행 중에는 커밋된 값만 읽을 수 있도록 함
  • 커밋된 데이터만 덮어쓸 수 있음
    • 행(row) 단위 잠금을 사용
    • 특정 트랜잭션이 수정 중인 행은 수정이 끝날 때까지 잠금 처리
    • 이를 수정하려는 다른 트랜잭션은 해당 트랜잭션의 수정이 끝난 뒤 수행될 수 있음

 

Repeatable Read

  • 트랜잭션 진행 중에는 시작 시점의 데이터만을 읽도록 함
  • 해당 데이터가 트랜잭션 진행 중 외부에서 변경되었다 하더라도 트랜잭션 내에서는 해당 데이터를 트랜잭션 시작 지점의 상태(변경되지 않은 데이터)로 다룸
    • MVCC(Multi-Version Concurrency Control)을 이용
      • 트랜잭션 시작 시 데이터를 특정 버전으로 저장하고, 트랜잭션 중 데이터를 읽게 되면 해당 버전의 데이터를 읽음
      • 다른 트랜잭션에서는 같은 행이라도 다른 버전으로 다루므로 읽는 시점에 따라 데이터가 달라지지는 않음

 

Serializable

  • 정의 그 자체로는 모든 트랜잭션을 순차적으로 실행하는 격리 수준
  • 그러나 이는 사실상 최악의 퍼포먼스를 보이므로 잘 사용하지 않으며, 대신 인덱스 기반 잠금, 조건(WHERE 절) 기반 잠금 등의 방식을 사용
    • 위에서 언급한 당직 담당자 문제(동일 table 내 데이터 수정)의 해결을 위해 당직 일자를 인덱스로 설정하고 인덱스 잠금을 사용하면, A와 B가 동일 인덱스 데이터를 수정하려 하므로 A→B 순서로 실행되게 되고, B는 Lock에 의해 데이터를 변경할 수 없게 함

 

변경 유실 해결

  1. 원자적 연산 사용
    • A = 1; A = 2; 이렇게 증가하도록 반영하면 앞의 할당이 묻히니까 A = A+1; 이렇게 변수에 특정 연산을 하여 할당하는 방식을 사용
  2. 명시적 잠금
    • 누군가 쓰기 트랜잭션을 수행 중이라면 해당 데이터에 접근할 수 없도록 함
  3. CAS (Compare And Set)
    • 수정할 때 값이 반영되어도 되는지 비교 후 반영
    • ex) version이라는 column을 두어 수정 시마다 version을 1씩 증가시킴. A가 조회하고(버전1), B가 조회하고(버전1), A가 수정을 시도하고(WHERE 버전=1인 행을 수정하고 버전=2로 업데이트), B가 수정을 시도(WHERE 버전=1인 행을 수정하지만 버전=2로 업데이트되어 없음)