이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › DB › 트랜잭션과 잠금
격리 수준
다른 트랜잭션이 바꾸는 중인 데이터가 내게 어떻게 보이나
트랜잭션 A가 조회하는 사이에 트랜잭션 B가 같은 데이터를 바꿉니다. 격리 수준에 따라 A가 B의 변경을 보기도 하고 못 보기도 합니다.
실행으로 대조 · MySQL 8.4.11에서 세션 두 개로 열두 가지 조합을 실행해 대조
이 실습에 나오는 개념
- 트랜잭션 격리 수준
- 동시에 실행되는 트랜잭션들이 서로의 변경을 어디까지 볼 수 있게 할지 정하는 단계다. READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE의 네 단계가 있으며, 뒤로 갈수록 서로의 영향을 더 많이 차단하지만 동시에 처리할 수 있는 양은 줄어든다. MySQL(InnoDB)의 기본값은 REPEATABLE READ, PostgreSQL과 Oracle의 기본값은 READ COMMITTED다.
- Dirty Read
- 다른 트랜잭션이 아직 커밋하지 않은 변경 내용을 읽는 현상이다. 그 트랜잭션이 롤백되면 존재한 적 없는 데이터를 읽은 셈이 된다. READ COMMITTED 이상의 격리 수준에서는 발생하지 않는다.
- Non-repeatable Read
- 한 트랜잭션 안에서 같은 행을 두 번 읽었는데, 그 사이에 다른 트랜잭션이 값을 변경하고 커밋해서 두 번의 결과가 달라지는 현상이다. READ COMMITTED에서는 발생할 수 있고 REPEATABLE READ에서는 발생하지 않는다.
- Phantom Read
- 한 트랜잭션 안에서 같은 조건으로 두 번 조회했는데, 그 사이에 다른 트랜잭션이 행을 추가하거나 삭제해서 결과의 행 수가 달라지는 현상이다. 값이 바뀌는 것이 아니라 없던 행이 나타나거나 있던 행이 사라진다는 점에서 Non-repeatable Read와 구분된다.
- MVCC(다중 버전 동시성 제어)
- 데이터를 변경할 때 이전 버전을 보존해 두어, 읽는 트랜잭션이 자신의 스냅샷 시점에 맞는 버전을 볼 수 있게 하는 방식이다. 스냅샷 시점은 격리 수준에 따라 달라서, MySQL의 REPEATABLE READ는 트랜잭션의 첫 읽기 시점, READ COMMITTED는 문장을 실행할 때마다 새로 잡는다. 읽기 작업이 쓰기 작업을 기다리지 않고 쓰기 작업도 읽기 작업을 기다리지 않아 동시 처리 성능이 높다. MySQL InnoDB, PostgreSQL, Oracle이 모두 이 방식을 쓴다.
- Undo 로그와 Redo 로그
- Undo 로그는 변경 전의 데이터를 기록한 것으로, 트랜잭션을 롤백할 때 쓰이며, InnoDB에서는 다른 트랜잭션에 과거 버전을 보여 줄 때(MVCC)도 쓰인다. Redo 로그는 변경 내용을 기록한 것으로, 커밋된 변경이 데이터 파일에 완전히 쓰이기 전에 장애가 나더라도 재시작 시 이를 다시 적용해 복구하는 데 쓰인다.
- 공유 락과 배타 락
- 공유 락(S)은 읽기를 위한 잠금으로 여러 트랜잭션이 동시에 가질 수 있다. 배타 락(X)은 쓰기를 위한 잠금으로 한 트랜잭션만 가질 수 있으며, 배타 락이 걸린 데이터에는 다른 트랜잭션이 공유 락도 배타 락도 걸 수 없다.
이렇게 바꿔 보세요
- 처음 설정으로 — READ COMMITTED. 같은 조회를 두 번 했는데 값이 달라집니다.
- REPEATABLE READ로 올리면 — 두 번째 조회도 처음과 같은 값을 봅니다.
- 커밋 안 된 값을 읽는 경우 — READ UNCOMMITTED. 롤백되어 사라질 값을 읽습니다.
- 같은 상황, READ COMMITTED — 커밋 전의 값은 보이지 않습니다.
- 없던 행이 나타나는 경우 — READ COMMITTED. 두 번째 조회에서 건수가 늘어납니다.
- SERIALIZABLE — 이상 현상은 없지만 B가 A를 기다립니다.
처음 설정으로 실행하면
- A가 트랜잭션을 시작합니다.
- A의 첫 번째 조회입니다. 잔액은 1000입니다.
- B가 잔액을 500으로 바꿉니다. A의 조회는 락을 걸지 않았으므로 B는 기다리지 않습니다.
- B가 커밋합니다. 이제 DB의 확정된 잔액은 500입니다.
- A의 두 번째 조회입니다. READ COMMITTED는 조회할 때마다 그 시점에 커밋된 최신 데이터를 읽습니다. 같은 트랜잭션 안에서 같은 조회를 했는데 1000이 500으로 달라졌습니다. 이것이 Non-repeatable Read입니다.
- A가 커밋합니다.
실행 결과
- 격리 수준 READ COMMITTED: A의 첫 조회 1000, 두 번째 조회 500. 이상 현상: Non-repeatable Read.
- 한 트랜잭션 안에서 "조회 → 계산 → 다시 조회" 같은 흐름이 있으면 값이 중간에 바뀌어 계산이 어긋날 수 있습니다. REPEATABLE READ 이상에서 막힙니다.
코드
-- 세션 A
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 첫 번째 조회
-- 세션 B
START TRANSACTION;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;
-- 세션 A
SELECT balance FROM account WHERE id = 1; -- 두 번째 조회
COMMIT;
이 모형의 전제
MySQL InnoDB의 동작을 기준으로 했습니다. 격리 수준별로 허용되는 현상은 SQL 표준과 MySQL 공식 문서의 정의를 따랐습니다. REPEATABLE READ에서 Phantom Read가 보이지 않는 것은 락을 걸지 않는 일반 SELECT에 한한 이야기이며, SELECT ... FOR UPDATE나 UPDATE처럼 최신 데이터를 대상으로 하는 문장에서는 다른 결과가 나올 수 있습니다. SERIALIZABLE에서 조회가 공유 락을 거는 것은 InnoDB의 구현 방식이고, PostgreSQL 등 다른 DB는 방식이 다릅니다. 열두 가지 조합(장면 세 가지 × 격리 수준 네 가지) 모두를 실제 MySQL 8.4.11에서 세션 두 개로 실행해, A가 읽은 값과 락 대기가 화면과 일치하는 것을 확인했습니다.
앞 실습: 트랜잭션 기본 · 다음 실습: 갭 락과 넥스트 키 락