PostgreSQL 18의 REPEATABLE READ에서는 다른 세션이 나중에 커밋한 변경을 두 번째 일반 SELECT가 바로 보지 않습니다. 첫 조회 등에서 잡은 스냅샷을 유지하기 때문입니다. 단, BEGIN을 실행한 시각과 첫 조회 시각을 같은 것으로 계산하면 답이 달라질 수 있습니다.
아래는 공식 문서의 규칙으로 직접 만든 두 세션 시간표입니다. 운영 DB의 실제 관측값이나 PostgreSQL 실행 결과라고 제시하는 자료는 아닙니다.
기준이 되는 시점부터 표시합니다
스냅샷은 이 트랜잭션의 읽기가 어떤 데이터 상태를 기준으로 삼는지 정합니다. 같은 시각에 여러 세션이 동작해도 모든 세션이 항상 같은 값을 보는 것은 아닙니다. 저장된 최신 값과 특정 트랜잭션에서 보이는 값을 구별해야 합니다. ‘B가 커밋했으니 A도 즉시 읽는다’는 추측은 격리수준을 생략한 답입니다.
PostgreSQL 18 문서는 REPEATABLE READ에서 첫 조회 또는 데이터 변경 문장이 실행되기 전까지 커밋된 행을 기준으로 설명합니다. 아래 A는 BEGIN 다음에 곧바로 SELECT를 실행하므로 그 첫 SELECT가 기준점입니다. 격리수준은 처음부터 지정하고, 중간에 다른 조회나 변경을 실행하지 않았다고 가정합니다.
A가 먼저 읽은 뒤 B가 재고를 바꿉니다
| 순서 | 세션 A | 세션 B | A가 읽은 값 |
|---|---|---|---|
| 1 | BEGIN ISOLATION LEVEL REPEATABLE READ | 대기 | 아직 조회하지 않음 |
| 2 | id 1 재고 SELECT | 대기 | 100 |
| 3 | 대기 | 재고를 80으로 UPDATE | 조회 없음 |
| 4 | 대기 | COMMIT | 조회 없음 |
| 5 | 같은 재고 SELECT | 대기 | 100 |
| 6 | COMMIT | 대기 | 트랜잭션 종료 |
| 7 | 새 트랜잭션에서 같은 재고 SELECT | 추가 변경 없음 | 80 |
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT qty FROM inventory WHERE id = 1;
-- 여기에서 B가 80으로 갱신하고 커밋한 뒤 A를 계속합니다.
SELECT qty FROM inventory WHERE id = 1;
COMMIT;두 번째 조회가 100이라고 해서 B의 UPDATE가 실패한 것은 아닙니다. B는 80을 커밋했지만 A는 첫 조회의 기준을 유지합니다. A를 끝내고 새 트랜잭션으로 읽으면, 추가 변경이 없다는 조건에서 80을 봅니다. 한 세션의 화면만 보고 다른 세션의 커밋 여부를 판정하면 안 되는 이유입니다.
두 열을 따로 그려 보면 혼동이 줄어듭니다. 하나는 각 시점에 커밋된 최신 값이고 다른 하나는 A의 조회 결과입니다. 순서 4 이후 최신 값 열에는 80을 적지만 A의 결과 열에는 100을 유지합니다. 이 차이를 표시해야 같은 자료를 두 번 읽는다는 말과 언제나 최신값을 읽는다는 말을 구분할 수 있습니다.
READ COMMITTED로 같은 시간표를 읽습니다
A의 격리수준만 READ COMMITTED로 바꾸면 일반 SELECT는 문장 시작 시점을 기준으로 읽습니다. 첫 SELECT가 100을 본 뒤 B가 80을 커밋하고 두 번째 SELECT가 시작한다면, 두 번째 값은 80입니다. 동일한 입력과 커밋 순서에서 무엇이 달라지는지 비교해야 격리수준 이름을 결과와 연결할 수 있습니다.
| A의 격리수준 | 첫 조회 | B 커밋 후 둘째 조회 |
|---|---|---|
| READ COMMITTED | 100 | 80 |
| REPEATABLE READ | 100 | 100 |
여기서는 두 SELECT 사이에 B의 커밋이 완료되었다고 명시했습니다. B가 수정만 하고 아직 커밋하지 않았다면 위 시간표와 같은 조건이 아닙니다. 문제에서 UPDATE와 COMMIT을 따로 표시하고, 어느 문장이 시작하기 전에 커밋이 끝났는지 확인하세요. 비슷한 시간표라도 순서 하나가 바뀌면 답을 다시 계산해야 합니다.
BEGIN만 먼저 실행한 변형을 풉니다
이번에는 A가 BEGIN만 실행하고 기다립니다. 그동안 B가 재고를 80으로 바꾸어 커밋한 뒤, A가 처음으로 SELECT를 실행한다고 바꿔 보세요. 앞서 실행한 조회나 데이터 변경이 없는 이 조건에서 A의 첫 조회는 80입니다. BEGIN이 먼저 있었다는 이유만으로 처음 값 100이 영구히 고정됐다고 답하면 틀립니다.
이후 B가 다시 70으로 바꾸어 커밋하더라도, 같은 REPEATABLE READ 트랜잭션의 A는 두 번째 일반 SELECT에서 80을 유지합니다. 100에서 80으로 바뀌었는지보다 A의 첫 조회가 어느 커밋 뒤에 있었는지가 결정적입니다. 복습할 때 BEGIN과 첫 SELECT 사이를 늘려 그려 보면 두 사건이 같은 것이 아니라는 점이 선명해집니다.
모든 읽기와 쓰기가 같은 결과라는 뜻은 아닙니다
A 자신이 같은 트랜잭션 안에서 바꾼 값은 후속 조회에서 볼 수 있습니다. 따라서 ‘REPEATABLE READ에서는 어떤 경우에도 같은 숫자만 나온다’는 문장은 지나칩니다. 여기서 가르친 것은 다른 트랜잭션의 나중 커밋을 일반 조회가 바로 반영하지 않는다는 조건입니다. 자신의 변경과 다른 세션의 변경을 한 줄로 묶지 마세요.
또 이 시간표를 UPDATE나 SELECT FOR UPDATE까지 그대로 확대하면 안 됩니다. PostgreSQL에서는 스냅샷 이후 다른 트랜잭션이 변경한 행을 갱신하려다 동시 갱신 오류로 트랜잭션이 실패할 수 있습니다. 이 경우 한 문장만 다시 보내는 문제가 아니라 전체 트랜잭션의 재시도를 검토해야 합니다. 격리수준 이름이 같다는 이유로 다른 DB 제품에도 같은 실패 규칙을 적용하지 않습니다.
시간표를 가리고 기준값을 찾습니다
- DB 제품과 버전, A의 격리수준을 먼저 적습니다.
- 각 세션의 BEGIN·첫 조회·UPDATE·COMMIT을 별도 사건으로 그립니다.
- A가 기준으로 삼는 첫 조회 시점을 표시합니다.
- 그 시점 전에 커밋된 값과 그 뒤에 커밋된 값을 나눕니다.
- A 자신의 변경이 있는지 확인한 뒤 후속 조회값을 적습니다.
- A를 종료하고 새 트랜잭션을 시작하는 변형까지 비교합니다.
정답을 가리고 확인해 보세요
이 글을 위해 만든 학습 예시이며 공식 기출문제나 판매 카드의 인용이 아닙니다.
초기 재고는 100입니다. PostgreSQL 18에서 A가 REPEATABLE READ로 BEGIN만 실행한 뒤, B가 80을 커밋합니다. 그다음 A의 첫 SELECT가 실행되고, 이어 B가 70을 커밋한 뒤 A가 같은 트랜잭션에서 다시 SELECT합니다. A는 다른 조회·변경을 하지 않았습니다. 두 조회값은 무엇인가요?
정답과 해설 확인
첫 조회와 두 번째 조회 모두 80입니다.이 조건에서 첫 SELECT 전에 80이 커밋되어 읽기 기준에 들어갑니다. 이후 다른 세션이 커밋한 70은 이미 잡힌 A의 스냅샷을 바꾸지 않습니다. BEGIN 시점의 100을 기준으로 삼으면 안 됩니다.