온라인 게임을 하고 있는데 누가 치트 써서 자기 캐릭터를 강화하거나, 도서관에서 책 읽고 있는데 누가 갑자기 페이지를 바꾸거나, 새 챕터를 끼워넣거나, 아예 책을 바꿔치기 한다고 상상해봐. 완전 짜증나지? 바로 이런 "깜짝 이벤트"를 막아주는 게 REPEATABLE READ 격리 수준이야.
REPEATABLE READ는 한 트랜잭션 안에서 네가 보는 데이터가 트랜잭션 끝날 때까지 절대 안 바뀐다는 걸 보장해줘. 다른 트랜잭션이 데이터를 바꿔도, 네 트랜잭션은 그런 변화에 영향 안 받아.
주요 특징:
Dirty Read(아직 커밋 안 된 데이터 읽기)를 막아줘.- 그리고 제일 중요한 건
Non-Repeatable Read를 막아준다는 거야. 즉, 트랜잭션 시작할 때 읽은 데이터셋을 다시 읽어도, 누가 바꿨든 똑같은 데이터가 나와.
하지만 REPEATABLE READ는 Phantom Read는 막지 못해. 다른 트랜잭션이 새 행을 추가하면, 네가 다시 쿼리할 때 그 행이 튀어나올 수도 있어. 이걸 막으려면 SERIALIZABLE 수준이 필요하고, 이건 나중에 얘기할게.
REPEATABLE READ 격리 수준 설정하는 법
예제 보기 전에, PostgreSQL에서 이 격리 수준을 어떻게 켜는지부터 알아보자. 방법은 두 가지야:
특정 트랜잭션에만 격리 수준 설정하기:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- 너의 쿼리들 COMMIT;현재 세션 전체에 격리 수준 설정하기:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
두 번째 방법을 쓰면, 그 세션에서 하는 모든 트랜잭션이 REPEATABLE READ를 쓰게 돼.
예제: Non-Repeatable Read 방지하기
예를 들어 accounts라는 테이블이 있다고 해보자. 구조는 이래:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
);
INSERT INTO orders (customer_name, status)
VALUES ('Alice', 'pending'), ('Bob', 'pending');
이제 한 트랜잭션이 데이터를 바꾸고, 다른 트랜잭션이 읽는 기본 시나리오부터 볼게.
REPEATABLE READ 없이 (READ COMMITTED 수준) 시나리오
트랜잭션 1 시작:
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- 결과: 100
이때 트랜잭션 2가 데이터 변경:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
트랜잭션 1 계속 진행:
SELECT balance FROM accounts WHERE account_id = 1;
-- 결과: 150 (데이터가 바뀜!)
COMMIT;
보다시피, READ COMMITTED 수준에서는 한 트랜잭션 안에서 두 번 읽으면 값이 바뀔 수 있어. 이게 바로 Non-Repeatable Read야.
REPEATABLE READ로 시나리오
이번엔 똑같은 예제를 REPEATABLE READ 격리 수준으로 해보자.
트랜잭션 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- 결과: 100
트랜잭션 2:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
트랜잭션 1 계속 진행:
SELECT balance FROM accounts WHERE account_id = 1;
-- 여전히: 100 (데이터 안 바뀜!)
COMMIT;
다른 트랜잭션이 뭘 바꿔도, 트랜잭션 1은 시작할 때의 데이터만 봐. 그래서 Non-Repeatable Read가 안 생기는 거지.
REPEATABLE READ가 어떻게 동작하는지
PostgreSQL은 MVCC (Multi-Version Concurrency Control)라는 메커니즘으로 REPEATABLE READ 격리 수준을 구현해. MVCC의 핵심은, 각 트랜잭션이 자기만의 "스냅샷"을 받아서 트랜잭션 끝날 때까지 그 상태로만 데이터가 보인다는 거야. 이걸 위해 여러 버전의 행을 만들어서 관리하지.
트랜잭션이 시작되면, 그 시점의 데이터만 볼 수 있어. 다른 트랜잭션이 뭔가 바꾸면 PostgreSQL이 새 버전의 행을 만들고, 기존 버전은 그걸 쓰는 트랜잭션한테 계속 보여줘.
그래서 트랜잭션이 느려지고 메모리도 많이 써. 그리고 이게 가장 강력한 격리 수준이라서, 성능이 좀 떨어질 수밖에 없어. 그래서 아무나 이걸 기본으로 쓰진 않아.
REPEATABLE READ의 한계: Phantom Read
아까 말했듯이, REPEATABLE READ는 Phantom Read를 막지 못해. 이게 뭔지 예제로 보자. 범위 쿼리에서 이런 일이 생겨.
예를 들어 orders 테이블이 있다고 해보자:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL
);
INSERT INTO orders (amount)
VALUES (50), (100), (150);
트랜잭션 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- 결과: 2
트랜잭션 2:
BEGIN;
INSERT INTO orders (amount) VALUES (200);
COMMIT;
트랜잭션 1 계속 진행:
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- 결과: 3 (새 행이 쿼리 결과에 나타남!)
COMMIT;
여기서 다른 트랜잭션이 amount = 200인 새 행을 추가했는데, 트랜잭션 1이 다시 쿼리하니까 그 행이 "유령처럼" 튀어나와. 이게 REPEATABLE READ로도 못 막는 Phantom Read야.
Phantom Read까지 막고 싶으면 SERIALIZABLE 수준을 써야 해. 근데 이건 성능이 더 떨어질 수밖에 없어.
REPEATABLE READ의 장점과 단점
REPEATABLE READ 격리 수준은 트랜잭션 중에 데이터가 바뀌지 않는다는 확신이 필요할 때 딱 좋아. 한 번 읽은 값은 COMMIT할 때까지 절대 안 바뀌어. 누가 다른 트랜잭션에서 바꿔도 말이지.
이렇게 하면 dirty read랑 non-repeatable read를 다 막을 수 있어. 트랜잭션 시작할 때 본 데이터만 계속 보니까, 중간에 갑자기 값이 바뀌는 일은 없어. 리포트 만들거나, 일관성이 중요한 의사결정할 때 특히 유용해.
반면에, REPEATABLE READ는 "팬텀" (phantom read) 현상은 못 막아. 이미 했던 쿼리에서 새 행이 튀어나올 수 있다는 거지. 그리고 부하가 많을 때는 트랜잭션끼리 충돌이 자주 나서, 락이 걸리거나 롤백이 생길 수도 있어. 쿼리가 아무리 맞아도 말이야.
결론적으로, REPEATABLE READ는 신뢰성과 성능의 밸런스가 괜찮은 수준이지만, 경쟁이 심한 환경에서는 추가 튜닝이나 주의가 필요해.
꿀팁 & 흔한 실수
- 격리 수준을 뭘로 하냐에 따라 성능이 달라져.
REPEATABLE READ는 데이터 불변성이 진짜 필요할 때만 써. REPEATABLE READ랑SERIALIZABLE헷갈리는 경우 많아. 쿼리 다시 했는데 새 행이 보이면, 그건REPEATABLE READ에선 당연한 거야.- 트랜잭션 오래 끌면 락 충돌이 날 수 있으니까 조심해. 오래 가는 트랜잭션은 다른 작업을 막아버릴 수도 있어.
PostgreSQL은 트랜잭션 격리 수준을 다양하게 조절할 수 있어. REPEATABLE READ는 한 트랜잭션 안에서 이미 읽은 데이터가 절대 안 바뀌는 게 중요한 상황에 딱 맞아.
GO TO FULL VERSION