트랜잭션의 종류를 한 번 더 정리해보자! 데이터베이스 트랜잭션 세계에는 하루를 망칠 수 있는 세 명의 "악당"이 있어: 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 |
✅ 없음 | ✅ 없음 | ✅ 없음 |
적절한 격리 수준을 어떻게 고를까?
- 데이터 접근 속도가 중요하고 이상 현상이 크게 문제되지 않는다면(예: 오래된 데이터로 분석만 할 때) —
READ COMMITTED를 써. - 한 트랜잭션 안에서 데이터가 변하지 않아야 한다면 —
REPEATABLE READ를 써. - 최고 수준의 격리와 일관성이 필요하다면 —
SERIALIZABLE을 선택해. 단, 성능이 떨어질 수 있다는 점은 기억하자.
실전 팁
상황에 맞는 격리 수준으로 트랜잭션을 사용해. 예를 들어:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT ...;
COMMIT;
인덱스를 추가해서 테이블 락을 최소화하고 쿼리 속도를 높여봐.
특히 SERIALIZABLE 수준에서는 오래 걸리는 락을 피하려고 쿼리를 최적화하는 게 좋아.
특정 오류와 예방법
가끔 잘못된 격리 수준 선택 때문에 충돌이나 성능 저하가 생길 수 있어. 예를 들어, SERIALIZABLE을 병렬 트랜잭션이 많은 시스템에 쓰면 락이 많이 걸리고 트랜잭션이 "굶주림" 상태가 될 수도 있어.
이런 걸 막으려면 쿼리를 분석하고, 여러 격리 수준에서 성능을 테스트해보고, 적절한 인덱스를 사용해봐.
GO TO FULL VERSION