Zettelkasten

트랜잭션 격리 수준

·수정 2026.04.24·수정 2

1. 트랜잭션

1.1 정의

  • 어플리케이션 내에서 복수의 읽기와 복수의 쓰기를 하나의 논리적 단위로 묶는 방법을 의미한다.
  • 한 트랜잭션 내의 모든 읽기와 쓰기는 한 연산으로 실행 ⇒ 전체 성공/전체 실패

1.2 목적

  • 프로그래밍 모델을 단순화하려는 목적
  • 동시성 관련 문제를 어플리케이션단에서 푸는게 아닌 데이터베이스에서 해결 ⇒ 어플리케이션에서는 잠재적인 오류 시나리오와 동시성 문제에 대해 무시 가능

1.3 ACID

트랜잭션은 아래와 같은 것을 보장해야한다고 알려져 있음(ACID Compliant)

  1. 원자성(Atomicity)
    • 전체 성공/전체 실패만 가능하다.
  2. 일관성(Consistency)
    • 트랜잭션 수행 전, 후에 데이터 모델의 모든 제약 조건을 만족해야 함
  3. 격리성(Isolation)
    • 동시에 실행되는 트랜잭션은 격리된다.
  4. 지속성(Durability)
    • 한번 반영된 트랜잭션은 유지된다.
  • 노노 원자성과 격리성은 한 트랜잭션 내에서 여러번의 쓰기를 했을 때 데이터베이스가 어떤식으로 동작해야하는지 정의 다중 객체 트랜잭션은 어떤 읽기 연산과 쓰기 연산이 동일한 트랜잭션에 속하는지 알아낼 수단이 있어야함 관계형 데이터 베이스에서는 데이터 베이스 - 서버 사이의 TCP 연결을 기반 비관계형 데이터 베이스에서는 반드시 트랜잭션 시멘틱을 지원하지는 않음 직렬성 격리는 성능 비용이 상당함 따라서 완화된 격리 수준을 사용하는 시스템이 흔함 완화된 격리 수준으로 인해 발생한 문제
    1. 금전적인 손실
    2. 재무 감사원의 조사
    3. 고객 데이터 오염

2. 트랜잭션 격리 수준

  • 트랜잭션 격리 수준은 트랜잭션 사이의 데이터 가시성 정도를 나타내는 것

2.1 커밋 후 읽기(Read Committed)

  • 트랜잭션이 격리수준(read uncommitted) 이 없다고 가정했을 때 발생할 수 있는 동시성 문제
    1. 더티 읽기
    2. 더티 쓰기

2.1.1 커밋 후 읽기

  • 커밋된 데이터만 쓰거나 읽을 수 있음 ⇒ 더티 읽기더티 쓰기를 막아줌
    1. Dirty Read
    2. 더티 쓰기 : 두 트랜잭션이 데이터 베이스에 있는 동일한 객체를 동시에 갱신하려고 했을 때 발생하는 문제 ex) 중고차 판매 웹 사이트에서 두 사람(앨리스, 밥) 이 동시에 같은 차를 사는 경우
      1. 판매 목록에 구매자를 업데이트하는 연산
      2. 송장을 업데이트 하는 연산
        더티 쓰기를 막지 않으면 구매자는 밥, 송장은 앨리스로 될 수 있음

2.1.2 구현 방법

  1. 로우 수준 잠금
    • 로우를 변경하는 트랜잭션이 끝나기 전까지 읽기를 막음 ⇒ 읽기 잠금의 경우 응답 시간에 큰 영향을 미침
  2. 커밋 후 읽기
    • 과거에 커밋된 값과 새로운 트랜잭션의 쓰기 값을 모두 기억한다. 트랜잭션이 진행중인 동안에는 과거의 값을 읽고 새 값이 커밋되면 다른 트랜잭션이 새 값을 읽을 수 있음

2.1.3 Read Committed 에서 발생가능한 동시성 문제

  • 영례는 은행에 1,000달러의 저축이 있고 두 계좌에 500달러씩 나눠 놓았음
  • 계좌1 에서 2로 100달러를 전송하는 트랜잭션을 실행
  • 만약 그녀가 운이 없어 서 트랜잭션이 처리되고 있는 순간에 계좌 잔고를 보게 되면
  • 계좌2는 입금이 되기 전 상태(잔고가 500달러있는)를 보고 계좌1은 출금이 된 후 상태(잔고가400달러로바뀐)를 볼 수 있음
  • 영례에게는 현재 계좌 총액이 900달러만 있는 것처럼 나온다. 100달러는 연기처럼 사라져 버린 것 처럼보인다. ⇒ 비반복 읽기(nonrepeatable read) or 읽기 스큐(read skew) ⇒ 스냅숏 격리 수준으로 해결

2.2 스냅숏 격리(Snapshot Isolation)

“”” 데이터 갱신 시점을 기록해 트랜잭션 시점과 비교해 데이터를 보여주는 방식”””

  • 지원하는 DB마다 부르는 이름이 다름 ⇒ 오라클에서는 Serializable, Postgresql, mysql에서는 Repeatable Read
  • 더티 쓰기를 방지하기 위해 쓰기 잠금을 사용한다.
  • 쓰기는 쓰기만 차단한다.

2.2.1 MVCC(Multi Version Concurrency Control, MVCC)

  • 커밋 후 읽기의 일반화된 방법

  • 커밋후 읽기는 쿼리마다 독립된 스냅샷을 사용

    • 쿼리마다 커밋전, 커밋 후 값을 갖는다.
  • 스냅샷 격리는 하나의 트랜잭션에 대해 동일한 스냅숏(트랜잭션 Id, 트랜잭션 시점) 활용

  • 알고리즘

    1. 트랜잭션이 시작하면 계속 증가하는 트랜잭션 ID를 할당 받음

    2. 테이블의 각 로우에는 그 로우를 테이블에 삽입한 트랜잭션의 ID를 갖는 created_by 필드와 처음에는 비어 있는 deleted_by 필드도 있음

      • 생성과 삭제를 나타내는 플래그 필드
    3. 트랜잭션이 로우를 삭제하면 실제로 데이터베이스에서 지우지 않고 deleted_by 필드를 삭제를 요청한 트랜잭션의 ID 로 설정함으로써 지워졌다고 표시한다.

      • 아무 트랜잭션도 삭제된 데이터에 접근하지 않는게 확실해지면 데이터베이스의 GC 프로세스가 지워졌다고 표시된 로우들을 삭제해서 사용량을 줄임
    4. 갱신은 내부에서 삭제와 생성으로 변환됨.

      1. 위 그림에서 트랜잭션 13계좌 2에서 100달러를 출금하는 트랜잭션

      2. 이 TR로 인해 잔고 가 500달러에서 400달러로 바뀐다.

      3. 이제 accounts 데이터베이스에서 계좌 2는 실제로 두 개의 로우를 갖게 된다.

        created_by deleted_by amount
        1 3 13 500
        2 13 null 400
        1. 트랜잭션 13이 삭제한 것으로 표시한 잔고가 500달러인 로우
        2. 트랜잭션 13이 생성한 잔고가 400달러인 로우다.

    스냅숏 격리 가시성 규칙

    • 하나의 트랜잭션은 DB에서 객체를 읽을 때 트랜잭션 ID을 기준으로 어떤것은 읽을 수 있고 어떤것은 읽을 수 없음
      1. 하나의 트랜잭션이 시작할 때 진행중(커밋, 어보트 되지 않은)인 모든 트랜잭션을 가져온다. 이 트랜잭션들이 쓰는 데이터는 모두 무시
      2. 어보트 된 트랜잭션이 쓴 데이터는 무시
      3. 트랜잭션 ID가 큰 트랜잭션이 쓴 아이디는 무시됨
      4. 위 1~3을 제외하면 모두 읽을 수 있음
    • 색인
      • MVCC DB에서 색인은 어떻게 동작하는가?
        1. 색인이 객체의 모든 버전을 가리키고 색인 질의를 통해 오래된 버전을 필터링 구현 세부 사항
      1. postgresql
        1. 동일 객체의 다른 버전이 같은 페이지에 저장될 수 있으면 색인 갱신을 회피하는 최적화
      2. CouchDB, Daytomic, LMDB에서는 추가 전용 B 트리를 사용
        • (append-only/copy-on-write) 변종 사용

2.2.2 발생할 수 있는 동시성 문제

  • 커밋후 읽기스냅샷 격리는 동시에 실행되는 쓰기 작업이 있을 때 다른 읽기 전용 트랜잭션이 무엇을 볼 수 있는지에 대한 격리 수준
  • 쓰기-쓰기 충돌 문제중에서는 더티 쓰기 에 대해서만 이야기 했음
  • 더티 쓰기 외에도 아래와 같은 것들이 있음
    • 갱신 손실
    • 쓰기 스큐
  1. 갱신 손실(lost update)

    • DB에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때 발생(read-modify-write)
    • 두 트랜잭션(modify, write)가 동시에 발생하면 하나는 손실될 수 있음
      1. 카운터 증가, 계좌 잔고 갱신
        • 트랜잭션 발생 순서에 따라 결과가 달라짐
      2. 두명의 사용자가 동시에 같은 페이지 편집
    • 해결책
      1. 원자적 쓰기 연산
        • 객체에 독점적인 잠금을 획득해서 구현 ⇒ 커서 안정성
        • 모든 원자적 연산을 단일 스레드에서 실행되도록 강제
      2. 명시적인 잠금
        • DB에서 원자적 연산을 제공하지 않을 때 어플리케이션 단에서 명시적으로 잠금
      3. 갱신 손실 자동 감지
        • DB내부의 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 어보트 시키고 read-modify-write 주기를 재시도하도록 강제하는 방법
        • 병렬 실행을 통해 성능에서 이득을 본다.
        • PostGresql repeatable Read, Oracle Serializable, SQL Server 스냅샷 격리 수준에서는 지원
        • MYSQL/InnoDB Repeatable Read에서는 지원하지 않음
  2. 쓰기 스큐(write skew) 예시)

    • 최소 한 명의 의사는 대기상태로 있어야하는 호출 관리 어플리케이션
    • 앨리스와 밥이 호출 대기 상태에 있다가 거의 동시에 호출 대기 상태 종료 버튼을 누름
    • 각 트랜잭션에서 어플내에 대기중인 의사의 수가 2명인지 확인함, 2명 존재 빠져도 괜찮다고 판단
    • 두 트랜잭션 모두 성공해서 대기중인 의사가 없게된다.
    • 위의 예시에서 두 트랜잭션이 차례대로 일어났다면 요구 사항을 만족 시켰을 것임
    1. 여러 객체가 관련되므로 원자적 단일 객체 연산을 도움되지 않음

    2. 갱신 자동 손실 감지도 도움이 안됨

    3. 특정 데이터 베이스에서는 제약 조건을 걸 수 있음

    4. Row를 잠그는 방법

      BEGIN TRANSACTION;
      
      SELECT * FROM doctors where on_call = true AND shift_id=1234 FOR UPDATE
      
      UPDATE doctors SET on_call = false WHERE name = "ALICE" AND shift_id = 1234;
      
      COMMIT;
      
    • 쓰기 스큐와 관련된 다른 예시

      • 회의실 예약 시스템
      • 사용자 명
      • 이중 사용 방지
    • 위에 열거한 예시는 다음과 같은 패턴을 갖는다.

      1. SELECT 질의가 어떤 검색 조건에 부합하는 로우를 검색해 특정한 요구사항을 만족하는지 확인 ⇒ 호출 대기인 의사가 2명 이상인지 확인하는 쿼리
      2. 질의 결과에 따라 어플리케이션 코드가 어떤식으로 진행할지 결정 ⇒ 2명 이상이면 자신의 호출 대기를 해제 할 수 있음
      3. 계속 처리하기로 결정했다면 DB에 쓰고 트랜잭션을 커밋
        1. 이 트랜잭션으로 인해 2번의 조건이 변경됨 ⇒ 자신으 호출 대기 해제, 1번 쿼리에 변경이 일어남
    • 이처럼 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 쿼리 결과를 바꾸는 효과를 팬텀이라고 한다.

    • 해결 방법

      1. 충돌 구체화(Materializing Conflicts)
      2. Serializable 직렬성