이전 글에서 트랜잭션 격리 수준 4가지를 정리했다. 이번에는 실제로 MySQL에서 각 문제를 재현해보면서 격리 수준이 어떻게 동작하는지 확인해보려고 한다.
-- 실습용 테이블 생성
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(50),
stock INT
);
-- 초기 데이터 삽입
INSERT INTO products (id, name, stock) VALUES (1, '한정판 운동화', 10);
INSERT INTO products (id, name, stock) VALUES (2, '싱싱한 고등어', 25);
-- 데이터 확인
SELECT * FROM products;

실습을 위해 MySQL 터미널을 2개 열어둔다.
커밋되지 않은 "더러운" 데이터를 다른 트랜잭션이 읽는 현상
예를 들어:
1. 트랜잭션 A가 재고를 10개에서 5개로 수정 (아직 커밋 전)
2. 트랜잭션 B가 재고를 조회 → 5개로 보임 😱
3. 트랜잭션 A가 롤백 → 재고는 원래대로 10개
4. 트랜잭션 B는 존재하지 않았던 '5개'를 읽은 것 👻
두 터미널 모두 실행:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
[캡쳐 자리: 격리 수준 설정 결과]
🖥️ 터미널 1:
-- 트랜잭션 시작
START TRANSACTION;
-- 재고 수정 (커밋 안 함)
UPDATE products SET stock = 5 WHERE id = 1;
-- 현재 값 확인
SELECT stock FROM products WHERE id = 1;
[캡쳐 자리: 트랜잭션 A에서 수정 후 조회 결과 - 5]
🖥️ 터미널 2:
-- 트랜잭션 시작
START TRANSACTION;
-- 재고 조회 (커밋 안 된 데이터 읽음)
SELECT stock FROM products WHERE id = 1;
[캡쳐 자리: 트랜잭션 B에서 조회 결과 - 5 (Dirty Read 발생!)]
💥 Dirty Read 발생! 커밋되지 않은 데이터를 읽었다!
🖥️ 터미널 1:
-- 롤백
ROLLBACK;
🖥️ 터미널 2:
-- 다시 조회
SELECT stock FROM products WHERE id = 1;

롤백 후:

👻 아까 읽은 5는 유령 데이터였다! 실제로는 10개였던 것!
두 터미널 모두 실행:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
🖥️ 터미널 1:
START TRANSACTION;
UPDATE products SET stock = 5 WHERE id = 1;
🖥️ 터미널 2:
START TRANSACTION;
SELECT stock FROM products WHERE id = 1;

✅ 커밋 전이어서 터미널2(console_1)에서 확인했을 때 stock이 10이 나온 것을 확인할 수 있다!
🖥️ 터미널 1:
COMMIT;
🖥️ 터미널 2:
SELECT stock FROM products WHERE id = 1;

✅ 터미널1(console)을 커밋 후 데이터가 5로 바뀐 것을 확인할 수 있다!
🎯 Dirty Read 방지 성공!
한 트랜잭션 내에서 같은 데이터를 두 번 읽었는데 값이 다른 현상
예를 들어:
1. 트랜잭션 A가 재고 조회 → 10개
2. 트랜잭션 B가 재고를 5개로 수정하고 커밋 🔧
3. 트랜잭션 A가 다시 조회 → 5개 😵
UPDATE products SET stock = 10 WHERE id = 1;
COMMIT;
SELECT * FROM products;

두 터미널 모두 실행:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
🖥️ 터미널 1:
START TRANSACTION;
SELECT stock FROM products WHERE id = 1;

📊 초기화된 15가 잘 나오고 있다.
🖥️ 터미널 2:
START TRANSACTION;
UPDATE products SET stock = 5 WHERE id = 1;
COMMIT;

🔧 터미널2(console_1)에서 업데이트 후 커밋을 했더니 데이터가 5로 바뀐 것을 확인할 수 있었다.
🖥️ 터미널 1:
SELECT stock FROM products WHERE id = 1;

💥 Non-Repeatable Read 발생! 터미널1에서 select를 했을 때 터미널2의 변경 내역이 그대로 반영되었고, 터미널1의 처음 select 했을 때와 두 번째 select 했을 때의 값이 변경되어 Non-Repeatable 문제가 발생했다.
COMMIT;
두 터미널 모두 실행:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
🖥️ 터미널 1:
START TRANSACTION;
SELECT stock FROM products WHERE id = 1;

📊 동일하게 처음 조회는 15가 나오는 걸 확인할 수 있다.
🖥️ 터미널 2:
START TRANSACTION;
UPDATE products SET stock = 5 WHERE id = 1;
COMMIT;

🔧 터미널2에서 업데이트 + 커밋을 진행하였고, 1번 데이터의 stock가 5로 바뀐 것을 확인할 수 있었다.
🖥️ 터미널 1:
SELECT stock FROM products WHERE id = 1;

✅ 스냅샷 덕분에 값이 유지됐다! 하지만 처음 터미널1로 돌아가서 select를 실행하면 트랜잭션이 시작될 때와 동일하게 15를 보여주고 있는 것을 확인할 수 있었다.
COMMIT;
🎯 Non-Repeatable Read 방지 성공!
한 트랜잭션 내에서 같은 조건으로 조회했는데 없던 데이터가 나타나는 현상
예를 들어:
1. 트랜잭션 A가 stock > 10 조회 → 2개
2. 트랜잭션 B가 새 상품 추가 (stock = 15)하고 커밋 ➕
3. 트랜잭션 A가 다시 조회 → 3개 👻
DELETE FROM products WHERE id = 3;
UPDATE products SET stock = 15 WHERE id IN (1, 2);
COMMIT;
SELECT * FROM products WHERE stock > 10;

두 터미널 모두 실행:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
🖥️ 터미널 1:
START TRANSACTION;
SELECT * FROM products WHERE stock > 10;

📊 2개의 행이 조회됐다.
🖥️ 터미널 2:
START TRANSACTION;
INSERT INTO products (id, name, stock) VALUES (3, '유령 신상품', 15);
COMMIT;

➕ 새로운 상품 추가 완료!
🖥️ 터미널 1:
SELECT * FROM products WHERE stock > 10;

COMMIT;
🤔 어라? 여기 보면 업데이트된 행까지 3개의 행이 나와야 되는데 2개의 행만 나온 것을 확인할 수 있다.
💡 강사님께서 이거는 MySQL의 최신 버전에서는 REPEATABLE READ 단계에서도 스냅샷을 지원해줘서 트랜잭션이 시작될 때 스냅샷했던 데이터를 불러와서 잘 나오는 거라고 한다. 하지만 다른 DB에서는 지원하지 않기 때문에 알고 있어야 한다고 한다.
DELETE FROM products WHERE id = 3;
COMMIT;
두 터미널 모두 실행:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
🖥️ 터미널 1:
START TRANSACTION;
SELECT * FROM products WHERE stock > 10;

🖥️ 터미널 2:
START TRANSACTION;
INSERT INTO products (id, name, stock) VALUES (3, '유령 신상품', 15);


⏳ 사진을 보면 터미널2(console_1)이 DB에 들어가지 않고 계속 대기 중으로 돌고 있는 것을 확인할 수 있다.
🔒 범위 잠금(Range Lock)이 걸려서 대기 중!
🖥️ 터미널 1:
COMMIT;
🖥️ 터미널 2에서 대기하던 INSERT가 실행됨:
-- 자동으로 실행됨
COMMIT;

✅ 터미널 1이 커밋이 완료된 시점으로 터미널 2의 대기가 끝나고 커밋된 것을 확인할 수 있다.
🎯 Phantom Read 방지 성공!
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | ❌ 발생 | ❌ 발생 | ❌ 발생 |
| Read Committed | ✅ 방지 | ❌ 발생 | ❌ 발생 |
| Repeatable Read | ✅ 방지 | ✅ 방지 | ❌ 발생 |
| Serializable | ✅ 방지 | ✅ 방지 | ✅ 방지 |
Read Uncommitted:
Read Committed:
Repeatable Read:
Serializable:
직접 재현해보니 각 격리 수준의 차이가 명확히 이해됐다. 특히 Repeatable Read에서도 Phantom Read가 발생하는 걸 보고 놀랐다 (MySQL에서는 방지되지만!).
실무에서는 대부분 Read Committed를 쓰고, 필요하면 애플리케이션 레벨에서 락을 거는 게 나을 것 같다.
긴 글 읽어주셔서 감사합니다! 😊
궁금한 점이나 경험 있으시면 댓글로 공유해주세요! 💬