CodeGym /행동 /SQL SELF /트랜잭션 실행 시 발생하는 이상 현상: Dirty Read, Non-Repeatable Read, ...

트랜잭션 실행 시 발생하는 이상 현상: Dirty Read, Non-Repeatable Read, Phantom Read

SQL SELF
레벨 40, 레슨 3
사용 가능

트랜잭션의 종류를 한 번 더 정리해보자! 데이터베이스 트랜잭션 세계에는 하루를 망칠 수 있는 세 명의 "악당"이 있어: Dirty Read, Non-Repeatable Read, 그리고 Phantom Read. 이런 "이상 현상"들은 트랜잭션 격리 수준이 충분하지 않을 때 나타나. 오늘은 이 악당들이 누구인지, 어떻게 나타나는지, 그리고 — 제일 중요한 — 어떻게 싸워야 하는지 알아볼 거야.

예시로 들어가기 전에, 각각이 뭔지 다시 한 번 짚고 가자.

  • Dirty Read (더러운 읽기):
    아직 커밋(COMMIT)되지 않았거나, 심지어 롤백(ROLLBACK)될 수도 있는 트랜잭션이 수정한 데이터를 읽는 거야. 예를 들어, 친구한테 돈을 보냈다고 생각하고 잔고를 확인했더니 완전 거지야. 근데 갑자기 마음이 바뀌어서 돈을 다시 돌려받았어. 마법이지!

  • Non-Repeatable Read (반복 불가능한 읽기):
    같은 트랜잭션 안에서 같은 데이터를 두 번 읽었는데, 그 사이에 다른 트랜잭션이 데이터를 바꿔서 두 번 읽을 때 결과가 달라지는 거야. 예를 들어, 여권에서 생년월일을 확인했는데, 친구가 몰래 숫자를 바꿔서 다시 봤더니 생일이 달라졌어.

  • Phantom Read (팬텀 읽기):
    같은 쿼리를 두 번 했는데, 두 번째에는 다른 트랜잭션이 새 행을 추가해서 추가된 행이 보이는 거야. 예를 들어, 방에 있는 사람 수를 세고 있는데 누군가 몰래 친구를 더 데려온 거지.

Dirty Read 문제

예를 들어, accounts 테이블이 있다고 해보자:

CREATE TABLE accounts (
    account_id SERIAL PRIMARY KEY,
    owner TEXT NOT NULL,
    balance NUMERIC(10, 2) NOT NULL
);
INSERT INTO accounts (owner, balance) VALUES ('Alice', 1000), ('Bob', 500);

트랜잭션 1이 잔고를 바꾸고 있는데 아직 끝나지 않았고, 트랜잭션 2가 동시에 그 데이터를 읽으려고 해.

트랜잭션 1:

BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE owner = 'Alice';
-- Alice의 잔고가 800이 됐지만, 트랜잭션은 아직 끝나지 않았어.

트랜잭션 2:

BEGIN;
SELECT balance FROM accounts WHERE owner = 'Alice'; -- 잔고: 800 (더러운 읽기).
ROLLBACK; -- 트랜잭션 1이 롤백됨.

이제 트랜잭션 2는 잘못된 데이터를 보고 있는 거야, 왜냐면 트랜잭션 1이 변경사항을 취소했으니까. 이런 상황은 READ COMMITTED 격리 수준을 쓰면 막을 수 있어. 이 수준에서는 커밋되지 않은 트랜잭션의 변경사항을 볼 수 없어.

Non-Repeatable Read 문제

트랜잭션 1이 데이터를 읽고, 다른 트랜잭션이 그 데이터를 바꾼 다음, 트랜잭션 1이 다시 읽으면 값이 달라지는 상황을 생각해보자.

트랜잭션 1:

BEGIN;
SELECT balance FROM accounts WHERE owner = 'Bob'; -- 잔고: 500.

트랜잭션 2 (동시에):

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Bob';
COMMIT;

트랜잭션 1 다시:

SELECT balance FROM accounts WHERE owner = 'Bob'; -- 잔고: 400.
COMMIT;

같은 트랜잭션 안에서 데이터가 바뀐 걸 볼 수 있지. 실제로 이런 일이 금융 보고서 같은 데서 일어나면 큰일이야. 이 문제는 REPEATABLE READ 같은 더 높은 격리 수준을 쓰면 해결돼.

Phantom Read 문제

이번엔 orders 테이블이 있다고 해보자:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer TEXT NOT NULL,
    total NUMERIC(10, 2) NOT NULL
);
INSERT INTO orders (customer, total) VALUES ('Alice', 100), ('Bob', 200);

트랜잭션 1이 주문 개수를 세고, 다른 트랜잭션이 새 주문을 추가한 다음, 트랜잭션 1이 다시 세면 결과가 달라져.

트랜잭션 1:

BEGIN;
SELECT COUNT(*) FROM orders; -- 2개.

트랜잭션 2 (동시에):

BEGIN;
INSERT INTO orders (customer, total) VALUES ('Charlie', 300);
COMMIT;

트랜잭션 1 다시:

SELECT COUNT(*) FROM orders; -- 3개. 새로운 "팬텀" 주문이 생겼어!
COMMIT;

팬텀 읽기를 막으려면 SERIALIZABLE 격리 수준이 필요해. 이 수준에서는 결과에 영향을 주는 병렬 변경을 완전히 막아버려.

이상 현상 방지 방법

격리 수준 vs. 이상 현상

격리 수준 Dirty Read Non-Repeatable Read Phantom Read
Read Uncommitted ❌ 있음 ❌ 있음 ❌ 있음
Read Committed ✅ 없음 ❌ 있음 ❌ 있음
Repeatable Read ✅ 없음 ✅ 없음 ❌ 있음
Serializable ✅ 없음 ✅ 없음 ✅ 없음

적절한 격리 수준을 어떻게 고를까?

  1. 데이터 접근 속도가 중요하고 이상 현상이 크게 문제되지 않는다면(예: 오래된 데이터로 분석만 할 때) — READ COMMITTED를 써.
  2. 한 트랜잭션 안에서 데이터가 변하지 않아야 한다면 — REPEATABLE READ를 써.
  3. 최고 수준의 격리와 일관성이 필요하다면 — SERIALIZABLE을 선택해. 단, 성능이 떨어질 수 있다는 점은 기억하자.

실전 팁

상황에 맞는 격리 수준으로 트랜잭션을 사용해. 예를 들어:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT ...;
COMMIT;

인덱스를 추가해서 테이블 락을 최소화하고 쿼리 속도를 높여봐.

특히 SERIALIZABLE 수준에서는 오래 걸리는 락을 피하려고 쿼리를 최적화하는 게 좋아.

특정 오류와 예방법

가끔 잘못된 격리 수준 선택 때문에 충돌이나 성능 저하가 생길 수 있어. 예를 들어, SERIALIZABLE을 병렬 트랜잭션이 많은 시스템에 쓰면 락이 많이 걸리고 트랜잭션이 "굶주림" 상태가 될 수도 있어.

이런 걸 막으려면 쿼리를 분석하고, 여러 격리 수준에서 성능을 테스트해보고, 적절한 인덱스를 사용해봐.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION