CS/Database

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

jinsang-2 2025. 2. 18. 16:46

https://jinsang-2.tistory.com/119

트랜잭션의 특징 4가지, ACID중에 Isolation Level에 대해 이야기 해보려 한다. 

 

트랜잭션(Transaction)

트랜잭션(Transaction)이란??트랜잭션은 데이터베이스에서 수행되는 "작업의 논리적 단위"를 말한다.“더 이상 분할이 불가능한 업무처리의 단위”라고도 말한다. 하나의 작업을 위해 더 이상 분할

jinsang-2.tistory.com

ACID - I = 독립성(Isolation)

둘 이상의 트랜잭션이 동시에 실행되고 있을 경우 어떤 하나의 트랜잭션이라도, 다른 트랜잭션의 연산에 끼어들 수 없다. 하나의 트랜잭션이 다른 트랜잭션에게 영향을 주지 않도록 격리되어 수행되어야 한다. 

  • 예시 : 다른 트랜잭션이 계좌 A의 잔액을 읽는 경우, 트랜잭션 완료 전에는 변경 사항을 볼 수 없어야 한다.

트랜잭션 수행 중간에 다른 트랜잭션이 끼어들 수 없다면 모든 트랜잭션은 순차적으로 처리될 것이다. 그러면 데이터 정확성은 보장되지만, 결국 준수한 처리 속도를 위해서는 트랜잭션의 완전한 격리가 아닌 완화된 수준의 격리가 필요하다. 이처럼 속도와 데이터 정확성에 대한 트레이드 오프를 고려하여 트랜잭션의 격리성 수준을 나눈것이 바로 트랜잭션의 격리수준이다.

 

트랜잭션 격리 수준에 들어가기 전 배경 지식

https://jinsang-2.tistory.com/121

 

[MySQL] 스토리지 엔진 수준의 잠금(LOCK)의 종류

https://jinsang-2.tistory.com/120 [MySQL] 트랜잭션 격리 수준(Isolation Level)https://jinsang-2.tistory.com/119트랜잭션의 특징 4가지, ACID중에 Isolation Level에 대해 이야기 해보려 한다.  트랜잭션(Transaction)트랜잭션

jinsang-2.tistory.com

 

격리 수준에 따른 발생하는 문제점

1) Dirty Read (더티 리드)

  • 다른 트랜잭션이 아직 커밋되지 않은 데이터를 읽는 문제
  • 즉, 트랜잭션 A에서 데이터를 수정했는데, 트랜잭션 B가 이를 읽고 사용했다가 A가 롤백(Rollback) 하면 데이터 정합성이 깨짐
  • 예제
    1. A 사용자가 잔액 = 10,000원을 잔액 = 5,000원으로 변경
    2. B 사용자가 잔액을 조회 → 5,000원이라고 확인
    3. 하지만 A 사용자가 롤백하여 다시 잔액 = 10,000원으로 되돌림
    4. B 사용자는 잘못된 데이터를 봤음

2) Non-Repeatable Read (반복 불가능한 읽기)

  • 같은 트랜잭션에서 동일한 데이터를 조회했을 때 값이 달라지는 문제
    • 한 트랜잭션 내에서 같은 데이터를 두 번 조회했을 때 값이 다르면, 데이터의 정합성이 깨질 수 있음
  • 예제
    1. A 사용자가 트랜잭션을 시작하고 잔액 = 10,000원을 조회
    2. 이 데이터를 기반으로 로직 수행 (예: 8,000원 결제 가능 여부 확인)
    3. 그 사이에 B 사용자가 잔액 = 5,000원으로 변경하고 커밋
    4. A 사용자가 동일한 데이터를 다시 조회했더니 잔액 = 5,000원
    5. A 사용자는 처음에 조회한 값(10,000원)을 믿고 8,000원을 결제하려고 했지만, 실제로는 5,000원뿐이어서 문제 발생!
    6. 트랜잭션 내에서 동일한 조회 결과가 보장되지 않음

3) Phantom Read (팬텀 리드)

  • 새로운 레코드가 추가/삭제되면서 조회 결과가 달라지는 문제
  • 예제
    1. A 사용자가 특정 조건(예: 잔액 > 5000원)으로 데이터를 조회 → 2건이 조회됨
    2. B 사용자가 조건에 맞는 새로운 데이터를 INSERT하고 커밋
    3. A 사용자가 같은 조건으로 다시 조회했을 때 → 3건이 조회됨
    4. 트랜잭션 내에서 조회한 결과가 변함

트랜잭션의 격리 수준(Transaction Isolation Level)

트랜잭션 격리 수준이란 여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 여부를 결정하는 것이다. 트랜잭션의 격리 수준은 격리(고립) 수준이 높은 순서대로 SERIALIZABLE , REPEATABLE READ, READ Committed, READ Uncommited가 존재한다. 아래 예제들은 모두 자동 커밋(Auto Commit)이 false 인 상태에서만 발생

1. SERIALIZABLE (직렬화)

  • 가장 엄격한 격리 수준으로, 모든 트랜잭션을 순차적으로 진행
  • 이 격리 수준에서는 여러 트랜잭션이 동일한 레코드에 동시 접근할 수 없기 때문에, 데이터 일관성을 완벽하게 유지할 수 있다.
  • 트랜잭션이 순차적으로 처리되어야 하므로 동시 처리 성능이 매우 떨어진다.

MySQL에서 SERIALIZABLE 동작 방식

MySQL에서 SELECT FOR SHARE/UPDATE는 대상 레코드에 각각 읽기/쓰기 잠금을 거는 방식이다. 하지만 일반적인 순수한 SELECT 문은 잠금 없이 실행된다. 이를 “잠금 없는 일관된 읽기(Non-locking consistent read)”라고 한다. 그러나 SERIALIZABLE 격리 수준에서는 단순한 SELECT 작업에서도 대상 레코드에 넥스트 키 락(Next-Key Lock)을 걸며, 이는 읽기 잠금(공유락, Shared Lock)으로 작용한다.

즉, 한 트랜잰션이 넥스트 키 락을 설정한 경우, 다른 트랜잭션에서는 해당 레코드를 추가/수정/삭제할 수 없다.

SERIALIZABLE 장단점

장점: 데이터 일관성을 완벽히 보장

단점: 성능 저하로 인해 대부분의 시스템에서 비효율적

🚀 사용 예시: 금융 시스템, 회계 프로그램 등 극단적인 데이터 정합성이 필요한 경우


2. REPEATABLE READ (반복 가능한 읽기)

REPEATABLE READ는 한 트랜잭션 내에서 동일한 데이터를 여러 번 조회할 때, 항상 같은 결과를 보장하는 격리 수준이다. 이는 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)를 이용하여 구현된다.

( RDBMS 변경 전의 레코드를 언두 공간에 백업해두기 때문에 변경 전/후 데이터가 모두 존재하므로 동일한 레코드에 대해 여러 버전의 데이터가 존재한다고 해서 MVCC(다중 버전 동시성 제어)라고 부름 )

MVCC 동작 방식

  • 데이터가 변경될 때, 기존 데이터는 언두 로그(Undo Log)에 백업된다.
  • 트랜잭션이 진행 중일 때, 기존 데이터를 조회하면 해당 트랜잭션보다 이후에 변경된 데이터는 무시된다.
  • 트랜잭션이 롤백되면, 언두 로그를 이용하여 기존 데이터를 복원한다.

REPEATABLE READ는 MVCC를 이용해 한 트랜잭션 내에서 동일한 결과를 보장하지만, 새로운 레코드가 추가되는 경우에 부정합이 생길 수 있다.

REPEATABLE READ 동작 방식

  1. 사용자 B가 id ≥ 50 인 이름을 SELECT 한다. (MangKyu)
  2. 사용자 A는 50번에 MangKyu 이름을 → MinKyu로 변경한다(MVCC를 통해 기존 데이터는 변경되지만, 백업된 데이터가 언두 로그에 남게 된다)

이 상태에서 이전에 사용자 B가 데이터를 조회했던 트랜잭션은 아직 종료되지 않은 상황에서, 사용자 B가 다시 동일한 SELECT문을 실행한다면..??

사용자 B의 트랜잭션은 사용자 A의 트랜잭션이 시작하기 전에 이미 시작된 상태이다. 이 때 REPEATABLE READ는 트랜잭션 번호를 참고하여 자신보다 먼저 실행된 트랜잭션의 데이터만 조회한다. 만약 테이블에 자신보다 이후에 실행된 트랜잭션의 데이터가 존재한다면 언두 로그(바뀌기 이전의 이름)를 참고해서 데이터를 조회한다.

따라서 사용자 A의 트랜잭션이 시작되고 커밋까지 되었지만, 해당 트랜잭션는 현재 트랜잭션보다 나중에 실행되었기 때문에 조회 결과로 기존과 동일한 데이터를 얻게 된다. 즉, REPEATABLE READ는 어떤 트랜잭션이 읽은 데이터를 다른 트랜잭션이 수정하더라도 동일한 결과를 반환할 것을 보장해준다.

 

MySQL의 REPEATABLE READ에서는 MVCC 덕분에 일반적인 SELECT 문에서 Phantom Read가 발생하지 않는다.

일반적인 RDBMS에서는 REPEATABLE READ 새로운 레코드의 추가까지는 막지 않기에 Phantom Read가 발생하지만, MySQL에서는 일반적인 SELECT 문에서는 MVCC 덕분에 Phantom Read가 발생하지 않는다. 하지만 SELECT FOR UPDATE 와 같은 잠금(LOCK)이 걸린 SELECT 문을 사용하면 언두 로그(Undo Log)를 조회하는 것이 아니라 테이블의 실제 데이터를 조회하게 되어, Phantom Read가 발생할 수 있다.

📝 MySQL에서 Phantom Read가 발생하는 경우

  • `SELECT FOR UPDATE` 이후 `SELECT FOR UPDATE` 실행 시 → ❌ (갭 락이 걸려서 방지됨)
  • `SELECT FOR UPDATE` 이후 일반 `SELECT` 실행 시 → ❌ (갭 락으로 방지됨)
  • 일반 `SELECT` 이후 일반 `SELECT` 실행 시 → ❌ (MVCC 덕분에 방지됨)
  • 일반 `SELECT` 이후 `SELECT FOR UPDATE` 실행 시 → ✅ (Phantom Read 발생 가능)

그림으로 REPEATABLE READ에서 Phantom Read 발생하는 경우 설명

  1. 일반 SELECT 이후 일반 SELECT 실행 시 → ❌ (MVCC 덕분에 방지됨)

  • 사용자 B의 트랜잭션은 사용자 A의 트랜잭션이 시작하기 전에 이미 시작된 상태이다. 이 때 REPEATABLE READ는 트랜잭션 번호(T-ID)를 참고하여 자신보다 먼저 실행된 트랜잭션의 데이터만을 조회한다. 만약 테이블에 자신보다 이후에 실행된 트랜잭션의 데이터가 존재한다면 언두 로그를 참고해서 데이터를 조회한다.
  • 사용자 A의 트랜잭션이 시작되고 커밋까지 되었지만, 해당 트랜잭션(12)는 현재 트랜잭션(10)보다 나중에 실행되었기 때문에 조회 결과로 기존과 동일한 데이터를 얻게 된다.
    • REPEATABLE READ는 어떤 트랜잭션이 읽은 데이터를 다른 트랜잭션이 수정하더라도 동일한 결과를 반환할 것을 보장해주기 때문

Q) MVCC가 막아주면 그럼 언제 유령 읽기가 발생하는 것인가?

A) 잠금(LOCK)이 사용되는 경우! 👇아래를 확인해보자 !

잠금이 사용되는 경우에 Phantom Read가 발생한다 ! 

일반적인 RDBMS

  • SELECT … FOR UPDATE 구문으로 배타적 잠금(쓰기 잠금)을 걸었다. (읽기 잠금을 걸려면 SELECT FOR SHARE 구문을 사용해야 함) = 결과는 1건
    • 락은 트랜잭션이 커밋 또는 롤백될 때 해제된다.
  • 일반적인 DBMS에서는 갭락이 존재하지 않으므로 id=50인 레코드만 잠금이 걸리고 사용자 A의 요청은 잠금 없이 즉시 실행 된다.
  • 사용자 B가 다시 SELECT … FOR UPDATE 로 id가 50 이상인 애들을 찾으면 결과가 2건이 나온다.
    • 이렇게 동일한 트랜잭션 내에서 새로운 레코드가 추가되는 경우에 조회 결과가 달라지는 현상을 Phantom Read라고 한다.
  • 이 경우에도 MVCC를 통해 해결될 것 같지만, 두번째의 SELECT … FOR UPDATE 때문에 그럴 수 없다. 잠금이 있는 읽기는 데이터 조회가 언두 로그(Undo Log)가 아닌 테이블에서 수행되기 때문
    • LOCK을 걸고 읽기를 실행하면 테이블에 변경이 일어나지 않도록 테이블에 잠금(LOCK)을 걸고 테이블에서 데이터를 조회한다..
    • 그러면 잠금이 없는 경우처럼 언두 로그를 바라보고 언두 로그를 잠그면 되지 않나?? 생각할 수 있지만, 언두 로그는 append only 형태이므로 잠금 장치가 없다.
    • SELECT FOR UPDATE나 SELECT FOR SHARE로 레코드를 조회하는 경우에는 언두 영역의 데이터가 아니라 테이블의 레코드를 가져오게 되고, 이로 인해 Phantom Read가 발생하는 것
MySQL

 

MySQL에서는 갭 락이 존재하기에 바로 위에 일반적인 RDBMS의 문제가 발생하지 않는다.

사용자 B가 SELECT … FOR UPDATE로 데이터를 조회한 경우, MySQL은 id가 50인 레코드에는 레코드 락, id가 50보다 큰 범위에는 갭 락으로 넥스트 키 락을 건다. 따라서 사용자 A가 51인 member에 INSERT를 시도한다면, B의 트랜잭션이 종료(커밋 및 롤백)될 때까지 기다린 이후 COMMIT된다. (대기를 지나치게 오래하면 락 타임아웃이 발생한다.)

즉, MySQL의 REPEATABLE READ에서는 Phantom Read가 거의 발생하지 않는다. 거의 유일한 케이스는 아래와 같다. 👇👇

MYSQL에서 Phantom Read 발생시키기

  • 일반 `SELECT` 이후 `SELECT FOR UPDATE` 실행 시 → Phantom Read 발생 가능

잠금 없는 SELECT문으로 데이터를 조회 → 사용자 A가 INSERT문을 사용해 데이터를 추가(잠금이 없으니 바로 COMMIT) → 사용자 B가 SELECT … FOR UPDATE로 잠금(LOCK)을 걸면 언두 로그가 아닌 테이블로부터 레코드를 조회하므로 Phantom Read가 발생!!

  • 하지만 이러한 케이스는 거의 존재하지 않는다!

3. READ COMMITTED

READ COMMITTED는 커밋된 데이터만 읽을 수 있도록 보장하는 격리 수준이다. 이 격리 수준에서는 트랜잭션이 진행 중일 때, 다른 트랜잭션이 커밋한 데이터를 즉시 반영하여 조회할 수 있다.

READ COMMITTED의 특징

Dirty Read 방지: 다른 트랜잭션에서 변경했지만 아직 커밋되지 않은 데이터는 조회할 수 없음 ❌ Non-Repeatable Read 발생 가능: 동일한 SELECT 문을 실행하더라도, 조회 시점에 따라 데이터가 달라질 수 있음

❌ Phantom Read 발생 가능.

READ COMMITTED의 문제 발생 예제

  1. 사용자 A가 트랜잭션을 시작하고 데이터 조회 (아직 커밋 안 됨)
  2. 사용자 B가 해당 데이터를 변경 후 커밋(commit)
  3. 사용자 A가 다시 동일한 데이터를 조회 → 변경된 값이 조회됨 (즉, Non-Repeatable Read 발생)

🚀 사용 예시: 대부분의 RDBMS에서 기본적으로 사용되는 격리 수준 (ex. Oracle, PostgreSQL)

-- 트랜잭션 A
BEGIN;
SELECT balance FROM accounts WHERE id = 1; 
-- 결과: 10000

-- 트랜잭션 B (수정 및 커밋)
BEGIN;
UPDATE accounts SET balance = 5000 WHERE id = 1;
COMMIT;

-- 트랜잭션 A (다시 조회)
SELECT balance FROM accounts WHERE id = 1;
-- 결과: 5000 (Non-Repeatable Read 발생)

4. READ UNCOMMITTED

READ UNCOMMITTED는 트랜잭션이 커밋되지 않은 데이터도 읽을 수 있는 격리 수준이다. 이는 가장 낮은 수준의 격리이며, Dirty Read(더러운 읽기) 문제가 발생할 수 있다.

READ UNCOMMITTED의 특징

가장 높은 성능: 읽기 작업에 대한 락을 거의 사용하지 않음 ❌ Dirty Read 발생 가능: 다른 트랜잭션에서 변경했지만 아직 커밋되지 않은 데이터를 읽을 수 있음

Non-Repeatable Read 발생 가능

❌ Phantom Read 발생 가능

READ UNCOMMITTED의 예제

  1. 사용자 A가 데이터를 수정하고 트랜잭션을 커밋하지 않음
  2. 사용자 B가 해당 데이터를 조회커밋되지 않은 데이터가 보임 (Dirty Read 발생)
  3. 사용자 A가 롤백 → 사용자 B가 읽은 데이터는 유효하지 않음

🚀 사용 예시: 빠른 성능이 중요한 경우 (ex. 로그 수집, 분석 시스템 등)

-- 트랜잭션 A (수정)
BEGIN;
UPDATE accounts SET balance = 5000 WHERE id = 1;
-- 아직 COMMIT 하지 않음

-- 트랜잭션 B (조회)
SELECT balance FROM accounts WHERE id = 1;
-- 결과: 5000 (Dirty Read 발생)

-- 트랜잭션 A (롤백)
ROLLBACK;
-- B 트랜잭션은 잘못된 데이터 조회함

위와 같은 문제를 방지하기 위해 4가지 트랜잭션 격리 수준 이 존재하며, 격리 수준이 높을수록 동시성이 낮아지고, 성능이 떨어질 수 있음

격리 수준 Dirty Read 방지 Non-Repeatable Read 방지 Phantom Read 방지 성능

READ UNCOMMITTED ❌ (X) ❌ (X) ❌ (X) 👍 (높음)
READ COMMITTED ✅ (O) ❌ (X) ❌ (X) 🟢 (보통)
REPEATABLE READ ✅ (O) ✅ (O) ❌ (X) (MySQL 논외) 🔵 (낮음)
SERIALIZABLE ✅ (O) ✅ (O) ✅ (O) 🔴 (가장 낮음)

각 트랜잭션 격리 수준은 데이터 정합성과 성능의 트레이드오프를 고려하여 선택해야 한다. 일반적으로 MySQL에서는 REPEATABLE READ를 기본적으로 사용하며, 데이터 정합성이 더 중요하다면 SERIALIZABLE을, 성능이 더 중요하다면 READ COMMITTED를 고려할 수 있다.

 

 

 

 

 

[reference]

https://mangkyu.tistory.com/299

 

'CS > Database' 카테고리의 다른 글

Database Replication  (0) 2025.02.24
[MySQL] 스토리지 엔진 수준의 잠금(LOCK)의 종류  (0) 2025.02.18
트랜잭션(Transaction)  (0) 2025.02.18