격리 수준
동시성 문제를 어느 수준까지 방지할 것인지를 결정하는 것이 격리 수준(Isolation Level)입니다. SQL 표준(SQL:1992)은 네 가지 격리 수준을 정의합니다. 격리 수준은 트랜잭션의 ACID 속성 중 Isolation(격리성)의 정도를 조절하는 메커니즘입니다. 높은 격리 수준은 데이터 정합성을 보장하지만 동시성을 저하시키고, 낮은 격리 수준은 동시성을 높이지만 정합성 문제가 발생할 수 있습니다. 이 트레이드오프를 이해하고 상황에 맞는 격리 수준을 선택하는 것이 중요합니다.
동시성 문제 유형 복습
격리 수준을 이해하려면 먼저 동시성에서 발생할 수 있는 세 가지 핵심 문제를 숙지해야 합니다.
Dirty Read (오손 읽기)
커밋되지 않은 데이터를 읽는 현상입니다. 다른 트랜잭션이 변경 중인 값을 읽었는데, 그 트랜잭션이 ROLLBACK되면 읽은 데이터는 존재하지 않았던 것이 됩니다.
T1 T2
─── ───
UPDATE 잔고 = 50만원 (100→50)
READ 잔고 → 50만원 (Dirty Read!)
ROLLBACK (잔고 = 100만원으로 복원)
50만원 기반으로 비즈니스 로직 수행
→ 잘못된 데이터 기반의 결과!Non-Repeatable Read (반복 불가능 읽기)
같은 트랜잭션에서 같은 데이터를 두 번 읽었을 때 값이 달라지는 현상입니다. 두 번의 읽기 사이에 다른 트랜잭션이 해당 행을 UPDATE하고 COMMIT했기 때문입니다.
T1 T2
─── ───
READ 잔고 → 100만원
UPDATE 잔고 = 50만원
COMMIT
READ 잔고 → 50만원
↑ 같은 트랜잭션 내에서 값이 변했다!Phantom Read (팬텀 읽기)
같은 조건으로 여러 행을 조회했을 때, 두 번째 조회에서 이전에 없던 행이 새로 나타나거나 있던 행이 사라지는 현상입니다. 다른 트랜잭션이 INSERT나 DELETE를 수행하고 COMMIT했기 때문입니다.
T1 T2
─── ───
SELECT COUNT(*) FROM 주문
WHERE 날짜 = '2024-01-01'
→ 5건
INSERT INTO 주문 (날짜 = '2024-01-01')
COMMIT
SELECT COUNT(*) FROM 주문
WHERE 날짜 = '2024-01-01'
→ 6건 ← 팬텀 행 출현!Non-Repeatable Read와 Phantom Read의 핵심 차이는, Non-Repeatable Read는 기존 행의 값 변경에 의한 현상이고, Phantom Read는 행의 추가/삭제에 의한 현상이라는 점입니다.
네 가지 격리 수준
격리 수준이 높을수록 안전하지만 동시성이 떨어집니다 (느려집니다).
동시성 높음 ◀──────────────────────────────────▶ 안전성 높음
성능 좋음 성능 낮음
READ READ REPEATABLE SERIALIZABLE
UNCOMMITTED COMMITTED READ| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | 발생 | 발생 | 발생 |
| READ COMMITTED | 방지 | 발생 | 발생 |
| REPEATABLE READ | 방지 | 방지 | 발생 |
| SERIALIZABLE | 방지 | 방지 | 방지 |
위 표에서 아래로 갈수록 격리 수준이 높아지며, 더 많은 동시성 문제를 방지합니다 대신 락 경합이 증가하여 처리량(Throughput)이 감소합니다.
READ UNCOMMITTED
가장 낮은 격리 수준입니다. 커밋되지 않은 데이터도 읽을 수 있습니다.
-- MySQL
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- SQL Server
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 또는 쿼리 힌트로 사용
SELECT * FROM 주문 WITH (NOLOCK);T1 T2
─── ───
BEGIN
UPDATE 상품 SET 가격 = 0
WHERE id = 1;
(원래 가격: 10000)
BEGIN
SET ISOLATION LEVEL READ UNCOMMITTED
SELECT 가격 FROM 상품 WHERE id = 1
→ 0 ← 커밋 안 된 값을 읽음!
-- 이 값을 기반으로 주문 생성
INSERT INTO 주문 (상품id, 가격) VALUES (1, 0)
COMMIT
ROLLBACK
(가격: 10000으로 복원)
-- 주문은 가격 0으로 남아있음 → 데이터 불일치!실무에서는 거의 사용하지 않습니다. 데이터 정합성이 보장되지 않기 때문입니다. 대략적인 통계 조회 같은 극히 제한적인 상황에서만 사용됩니다. 예를 들어 실시간 대시보드에서 대략적인 주문 건수를 보여줄 때, 정확한 값보다 빠른 응답이 중요한 경우에 고려할 수 있습니다.
READ COMMITTED
커밋된 데이터만 읽을 수 있습니다. Oracle, PostgreSQL, SQL Server의 기본 격리 수준입니다.
-- Oracle (기본값, 명시적 설정 시)
ALTER SESSION SET ISOLATION_LEVEL = READ COMMITTED;
-- MySQL
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- PostgreSQL (기본값)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;READ COMMITTED 동작 원리
읽기 시점에 커밋 완료된 최신 데이터만 볼 수 있습니다. 내부적으로 DBMS는 읽기 시점마다 스냅샷을 갱신합니다.
T1 (READ COMMITTED) T2
─── ───
BEGIN
BEGIN
UPDATE 직원 SET 급여 = 400
WHERE id = 1 (원래: 300)
SELECT 급여 FROM 직원
WHERE id = 1
→ 300 (T2 미커밋이므로 커밋된 값을 읽음)
COMMIT
SELECT 급여 FROM 직원
WHERE id = 1
→ 400 (T2 커밋 완료, 새 값 읽음)
↑ Non-Repeatable Read 발생!
COMMITDirty Read는 방지되지만, 같은 트랜잭션 내에서 같은 쿼리의 결과가 달라질 수 있습니다 (Non-Repeatable Read). 대부분의 OLTP 시스템에서 READ COMMITTED로 충분합니다. 개별 SELECT 문의 결과가 항상 커밋된 데이터를 반영하므로 대부분의 비즈니스 요구사항을 만족합니다.
Oracle의 READ COMMITTED 구현
Oracle은 Undo Segment와 SCN(System Change Number)을 사용합니다. SELECT가 실행되는 시점의 SCN을 기준으로, 해당 SCN 이전에 커밋된 데이터만 읽습니다. 변경 전 데이터가 필요하면 Undo Segment에서 읽어옵니다.
쿼리 시작 SCN = 1000
데이터 블록 SCN = 1005 (최근 변경됨)
→ SCN 1005 > 1000이므로 이 블록은 변경됨을 인식
→ Undo Segment를 사용하여 SCN 1000 시점의 값을 복원
→ CR(Consistent Read) 블록 생성 후 반환
결과: 쿼리 시작 시점의 일관된 데이터를 반환REPEATABLE READ
트랜잭션 시작 시점의 데이터를 일관되게 읽습니다. MySQL InnoDB의 기본 격리 수준입니다.
-- MySQL (기본값)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;REPEATABLE READ 동작 원리
트랜잭션이 시작될 때 스냅샷이 생성되고, 트랜잭션이 끝날 때까지 이 스냅샷을 기반으로 읽기가 수행됩니다. 다른 트랜잭션이 데이터를 변경하고 커밋해도 스냅샷이 변하지 않으므로 항상 같은 값을 읽게 됩니다.
T1 (REPEATABLE READ) T2
─────────────────── ─────────────
BEGIN (스냅샷 생성 시점!)
SELECT 잔고 FROM 계좌
WHERE id = 1
→ 100만원
BEGIN
UPDATE 계좌 SET 잔고 = 50만원
WHERE id = 1
COMMIT
SELECT 잔고 FROM 계좌
WHERE id = 1
→ 100만원 ← 스냅샷 읽기! 변경 무시!
COMMIT
(트랜잭션 종료 후에야 50만원으로 보임)MySQL InnoDB에서의 Phantom Read 방지
SQL 표준에서 REPEATABLE READ는 Phantom Read를 허용합니다. 그러나 MySQL InnoDB에서는 REPEATABLE READ에서도 팬텀 읽기가 대부분 방지됩니다. 이는 InnoDB가 두 가지 메커니즘을 사용하기 때문입니다.
1. Consistent Read (SELECT)
→ MVCC 스냅샷을 사용하므로 팬텀 행이 보이지 않음
→ 트랜잭션 시작 시점 이후의 INSERT는 스냅샷에 없음
2. Locking Read (SELECT ... FOR UPDATE / FOR SHARE)
→ Gap Lock + Next-Key Lock을 사용하여 범위 잠금
→ 다른 트랜잭션의 INSERT를 물리적으로 차단
Gap Lock 예시:
SELECT * FROM 주문 WHERE 금액 BETWEEN 100 AND 200 FOR UPDATE;
→ (100, 200) 범위의 인덱스 갭에 락을 걸어
→ 이 범위에 새 행 INSERT 불가인덱스 값: 10, 20, 30
Next-Key Lock 범위
(-∞, 10] → 레코드 10 + 이전 갭
(10, 20] → 레코드 20 + 이전 갭
(20, 30] → 레코드 30 + 이전 갭
(30, +∞) → supremum + 이전 갭
SELECT * FROM t WHERE val = 20 FOR UPDATE;
→ (10, 20] 범위에 Next-Key Lock
→ (20, 30) 범위에 Gap Lock
→ val = 15, 25 등의 INSERT 차단PostgreSQL의 REPEATABLE READ
PostgreSQL의 REPEATABLE READ는 SSI(Serializable Snapshot Isolation)의 일부 기능을 활용합니다. InnoDB처럼 스냅샷 기반으로 읽기를 수행하지만, 쓰기 충돌 시에는 직렬화 실패(serialization failure) 오류를 발생시킵니다.
T1 (REPEATABLE READ) T2 (REPEATABLE READ)
─── ───
BEGIN BEGIN
SELECT 잔고 FROM 계좌 → 100
SELECT 잔고 FROM 계좌 → 100
UPDATE 계좌 SET 잔고 = 90
UPDATE 계좌 SET 잔고 = 80
COMMIT (성공)
COMMIT → ERROR: could not serialize access
→ T2는 ROLLBACK 후 재시도 필요SERIALIZABLE
가장 높은 격리 수준입니다. 트랜잭션을 마치 순차적으로 실행한 것처럼 보장합니다.
-- Oracle
ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE;
-- MySQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;DBMS별 SERIALIZABLE 구현 차이
MySQL InnoDB:
→ 모든 SELECT가 자동으로 SELECT ... FOR SHARE로 변환
→ 공유 락이 걸려 쓰기를 차단
→ 락 기반 직렬화 (Pessimistic)
PostgreSQL:
→ SSI(Serializable Snapshot Isolation) 사용
→ 락 없이 스냅샷 기반으로 동작
→ 커밋 시점에 직렬화 위반 여부 검사
→ 충돌 시 serialization failure 오류 → ROLLBACK 후 재시도
→ 낙관적(Optimistic) 접근
Oracle:
→ 내부적으로 SCN 기반 스냅샷 사용
→ 쓰기 충돌 시 ORA-08177: can't serialize access 발생
→ READ COMMITTED + SELECT ... FOR UPDATE 조합을 더 많이 사용모든 SELECT에 공유 락이 걸리거나 직렬화 검증이 수행되므로 동시성이 크게 떨어집니다. 금융 시스템처럼 데이터 정합성이 최우선인 경우에만 사용합니다.
MVCC와 격리 수준의 관계
현대 DBMS 대부분은 MVCC(Multi-Version Concurrency Control)를 사용하여 격리 수준을 구현합니다. MVCC에서는 데이터의 여러 버전을 유지하여, 읽기와 쓰기가 서로를 차단하지 않도록 합니다.
데이터 행: id=1, name='Alice', salary=300
T1이 UPDATE salary=400 실행
→ 현재 버전(salary=300)을 Undo 영역에 보존
→ 새 버전(salary=400)을 생성
T2가 SELECT *
→ T2의 격리 수준과 시작 시점에 따라 적절한 버전을 선택
→ READ COMMITTED: 가장 최근 커밋된 버전
→ REPEATABLE READ: 트랜잭션 시작 시점의 버전
핵심: 읽기가 쓰기를 차단하지 않음! (락 없이 읽기)DBMS별 MVCC 구현
| 항목 | Oracle | MySQL (InnoDB) | PostgreSQL |
|---|---|---|---|
| 구현 위치 | Undo Segment | Undo Log + Rollback Segment | Heap Tuple (다중 버전) |
| 버전 식별 | SCN (System Change Number) | Transaction ID + ReadView | xmin/xmax (트랜잭션 ID) |
| 이전 버전 보관 | Undo Tablespace | Undo Log | 기본 테이블 내 (HOT) |
| 가비지 컬렉션 | 자동 (Undo Retention) | Purge Thread | VACUUM 프로세스 |
| 읽기 일관성 | Statement 단위 (RC) / Tx 단위 | Tx 시작 시점 스냅샷 | Statement/Tx 단위 |
ReadView 생성 시 기록
1. creator_trx_id: 이 ReadView를 만든 트랜잭션 ID
2. m_ids: 현재 활성 트랜잭션 ID 목록
3. m_low_limit_id: 다음에 할당될 트랜잭션 ID (상한)
4. m_up_limit_id: 활성 트랜잭션 중 가장 작은 ID (하한)
가시성 판단
행의 trx_id < m_up_limit_id → 커밋된 버전, 보임
행의 trx_id >= m_low_limit_id → 미래 트랜잭션, 안 보임
행의 trx_id in m_ids → 활성 트랜잭션, 안 보임
그 외 → 커밋된 버전, 보임
READ COMMITTED: 매 SELECT마다 새 ReadView 생성
REPEATABLE READ: 첫 SELECT 시 한 번만 ReadView 생성 → 재사용Oracle vs MySQL 기본 격리 수준
| 항목 | Oracle | MySQL (InnoDB) |
|---|---|---|
| 기본 격리 수준 | READ COMMITTED | REPEATABLE READ |
| 지원 격리 수준 | READ COMMITTED, SERIALIZABLE | 4가지 모두 |
| MVCC 구현 | Undo Segment + SCN | Undo Log + ReadView |
| 팬텀 읽기 방지 | SERIALIZABLE에서만 | REPEATABLE READ에서 대부분 방지 |
| 읽기 일관성 보장 | Statement 수준 (RC에서) | Transaction 수준 (RR에서) |
Oracle은 READ UNCOMMITTED와 REPEATABLE READ를 지원하지 않습니다. 대신 MVCC 기반의 읽기 일관성으로 실질적으로 높은 수준의 보장을 제공합니다. Oracle에서 REPEATABLE READ가 필요하면 SELECT ... FOR UPDATE를 사용하여 비관적 락으로 구현합니다.
격리 수준별 실무 예시
READ COMMITTED가 적합한 경우
-- 상품 목록 조회: 최신 커밋된 데이터면 충분
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT p.name, p.price, p.stock
FROM products p
WHERE p.category = '전자제품'
ORDER BY p.created_at DESC;
-- 다른 트랜잭션이 가격을 변경하고 커밋하면
-- 다음 조회에서 새 가격이 보이지만, 이것은 의도된 동작REPEATABLE READ가 필요한 경우
-- 트랜잭션 내에서 재고를 확인하고 주문을 생성
-- 재고 값이 중간에 바뀌면 안 됨
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT stock FROM products WHERE id = 100; -- stock = 5
-- 비즈니스 로직: 재고가 충분한지 확인
-- ... 여러 쿼리 수행 ...
SELECT stock FROM products WHERE id = 100; -- 여전히 5 (일관성 보장)
UPDATE products SET stock = stock - 1 WHERE id = 100;
INSERT INTO orders (product_id, quantity) VALUES (100, 1);
COMMIT;SERIALIZABLE이 필요한 경우
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 계좌 A에서 출금
SELECT balance FROM accounts WHERE id = 'A'; -- 100만원
UPDATE accounts SET balance = balance - 30 WHERE id = 'A';
-- 계좌 B에 입금
UPDATE accounts SET balance = balance + 30 WHERE id = 'B';
-- 동시에 다른 트랜잭션이 같은 계좌를 변경하면
-- 직렬화 실패로 한쪽이 롤백됨 → 데이터 무결성 보장
COMMIT;격리 수준 확인 쿼리
-- MySQL
SELECT @@transaction_isolation;
-- 또는
SELECT @@tx_isolation; -- MySQL 5.7 이하
-- PostgreSQL
SHOW transaction_isolation;
-- Oracle
SELECT s.sid, s.serial#,
CASE BITAND(t.flag, POWER(2, 28))
WHEN 0 THEN 'READ COMMITTED'
ELSE 'SERIALIZABLE'
END AS isolation_level
FROM v$session s, v$transaction t
WHERE s.taddr = t.addr;
-- SQL Server
DBCC USEROPTIONS;격리 수준 선택 기준
| 시스템 유형 | 권장 격리 수준 | 이유 |
|---|---|---|
| 일반 웹 서비스 | READ COMMITTED | 최신 데이터 조회, 높은 동시성 |
| 재고/좌석 예약 | REPEATABLE READ + 락 | 트랜잭션 내 일관성 필요 |
| 금융 거래 | SERIALIZABLE 또는 비관적 락 | 데이터 정합성 최우선 |
| 대량 데이터 조회 | READ COMMITTED (성능 우선) | 집계는 근사치로 충분 |
| 배치 작업 | READ COMMITTED | 실시간 정합성 불필요 |
| 감사/로깅 | READ COMMITTED | 커밋된 데이터만 기록 |
데이터 정합성이 생명보다 중요한가?
├─ YES → SERIALIZABLE (금융, 의료 등)
└─ NO → 트랜잭션 내에서 같은 데이터를 여러 번 읽는가?
├─ YES → REPEATABLE READ
└─ NO → READ COMMITTED (대부분의 경우)격리 수준과 성능
격리 수준이 높아질수록 락 경합이 증가하여 처리량이 감소합니다.
READ UNCOMMITTED
→ 락 없이 읽기 → 최고 처리량
→ 데이터 정합성 보장 불가
READ COMMITTED
→ 읽기 시 공유 락 즉시 해제 (또는 MVCC로 락 없이)
→ 높은 처리량, 적절한 정합성
REPEATABLE READ
→ 읽기 락 트랜잭션 종료까지 유지 (또는 MVCC 스냅샷)
→ InnoDB: Gap Lock 추가로 인한 INSERT 대기 발생 가능
→ 중간 처리량
SERIALIZABLE
→ 모든 읽기에 공유 락 또는 직렬화 검증
→ 가장 낮은 처리량
→ 재시도 로직 필수 (직렬화 실패 대응)MVCC를 사용하는 DBMS에서는 READ COMMITTED와 REPEATABLE READ 간의 성능 차이가 크지 않습니다. MVCC에서 읽기는 락을 잡지 않고 스냅샷을 통해 수행되기 때문입니다. 차이가 발생하는 주된 지점은 Locking Read(SELECT FOR UPDATE 등)와 쓰기 연산에서의 Gap Lock 경합입니다.
정리
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read | 주요 사용처 |
|---|---|---|---|---|
| READ UNCOMMITTED | O | O | O | 대략적 통계 |
| READ COMMITTED | X | O | O | 일반 OLTP |
| REPEATABLE READ | X | X | O (InnoDB는 대부분 X) | MySQL 기본 |
| SERIALIZABLE | X | X | X | 금융/감사 |
격리 수준의 핵심은 트레이드오프입니다. 완벽한 격리를 원한다면 성능을 포기해야 하고, 높은 동시성을 원한다면 일부 정합성 문제를 감수해야 합니다. 대부분의 실무 환경에서는 DBMS 기본 격리 수준(Oracle은 READ COMMITTED, MySQL은 REPEATABLE READ)을 유지하면서, 정합성이 중요한 특정 트랜잭션에만 격리 수준을 높이거나 명시적 락을 사용하는 전략이 효과적입니다.
다음 절에서는 격리 수준을 구현하는 기술인 락과 2단계 잠금을 다루겠습니다.