격리 수준
동시성 문제를 어느 수준까지 방지할 것인지를 결정하는 것이 격리 수준(Isolation Level)입니다. SQL 표준(SQL:1992)은 네 가지 격리 수준을 정의합니다. 격리 수준은 트랜잭션의 ACID 속성 중 Isolation(격리성)의 정도를 조절하는 메커니즘입니다. 높은 격리 수준은 데이터 정합성을 보장하지만 동시성을 저하시키고, 낮은 격리 수준은 동시성을 높이지만 정합성 문제가 발생할 수 있습니다. 이 트레이드오프를 이해하고 상황에 맞는 격리 수준을 선택하는 것이 중요합니다.
동시성 문제 유형 복습
격리 수준을 이해하려면 먼저 동시성에서 발생할 수 있는 세 가지 핵심 문제를 숙지해야 합니다.
Dirty Read (오손 읽기)
커밋되지 않은 데이터를 읽는 현상입니다. 다른 트랜잭션이 변경 중인 값을 읽었는데, 그 트랜잭션이 ROLLBACK되면 읽은 데이터는 존재하지 않았던 것이 됩니다.
Non-Repeatable Read (반복 불가능 읽기)
같은 트랜잭션에서 같은 데이터를 두 번 읽었을 때 값이 달라지는 현상입니다. 두 번의 읽기 사이에 다른 트랜잭션이 해당 행을 UPDATE하고 COMMIT했기 때문입니다.
T1 T2
─── ───
READ 잔고 → 100만원
UPDATE 잔고 = 50만원
COMMIT
READ 잔고 → 50만원
↑ 같은 트랜잭션 내에서 값이 변했다!Phantom Read (팬텀 읽기)
같은 조건으로 여러 행을 조회했을 때, 두 번째 조회에서 이전에 없던 행이 새로 나타나거나 있던 행이 사라지는 현상입니다. 다른 트랜잭션이 INSERT나 DELETE를 수행하고 COMMIT했기 때문입니다.
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);실무에서는 거의 사용하지 않습니다. 데이터 정합성이 보장되지 않기 때문입니다. 대략적인 통계 조회 같은 극히 제한적인 상황에서만 사용됩니다. 예를 들어 실시간 대시보드에서 대략적인 주문 건수를 보여줄 때, 정확한 값보다 빠른 응답이 중요한 경우에 고려할 수 있습니다.
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는 읽기 시점마다 스냅샷을 갱신합니다.
Dirty Read는 방지되지만, 같은 트랜잭션 내에서 같은 쿼리의 결과가 달라질 수 있습니다 (Non-Repeatable Read). 대부분의 OLTP 시스템에서 READ COMMITTED로 충분합니다. 개별 SELECT 문의 결과가 항상 커밋된 데이터를 반영하므로 대부분의 비즈니스 요구사항을 만족합니다.
Oracle의 READ COMMITTED 구현
Oracle은 Undo Segment와 SCN(System Change Number)을 사용합니다. SELECT가 실행되는 시점의 SCN을 기준으로, 해당 SCN 이전에 커밋된 데이터만 읽습니다. 변경 전 데이터가 필요하면 Undo Segment에서 읽어옵니다.
REPEATABLE READ
트랜잭션 시작 시점의 데이터를 일관되게 읽습니다. MySQL InnoDB의 기본 격리 수준입니다.
-- MySQL (기본값)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;REPEATABLE READ 동작 원리
트랜잭션이 시작될 때 스냅샷이 생성되고, 트랜잭션이 끝날 때까지 이 스냅샷을 기반으로 읽기가 수행됩니다. 다른 트랜잭션이 데이터를 변경하고 커밋해도 스냅샷이 변하지 않으므로 항상 같은 값을 읽게 됩니다.
MySQL InnoDB에서의 Phantom Read 방지
SQL 표준에서 REPEATABLE READ는 Phantom Read를 허용합니다. 그러나 MySQL InnoDB에서는 REPEATABLE READ에서도 팬텀 읽기가 대부분 방지됩니다. 이는 InnoDB가 두 가지 메커니즘을 사용하기 때문입니다.
PostgreSQL의 REPEATABLE READ
PostgreSQL의 REPEATABLE READ는 SSI(Serializable Snapshot Isolation)의 일부 기능을 활용합니다. InnoDB처럼 스냅샷 기반으로 읽기를 수행하지만, 쓰기 충돌 시에는 직렬화 실패(serialization failure) 오류를 발생시킵니다.
SERIALIZABLE
가장 높은 격리 수준입니다. 트랜잭션을 마치 순차적으로 실행한 것처럼 보장합니다.
-- Oracle
ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE;
-- MySQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;DBMS별 SERIALIZABLE 구현 차이
모든 SELECT에 공유 락이 걸리거나 직렬화 검증이 수행되므로 동시성이 크게 떨어집니다. 금융 시스템처럼 데이터 정합성이 최우선인 경우에만 사용합니다.
MVCC와 격리 수준의 관계
현대 DBMS 대부분은 MVCC(Multi-Version Concurrency Control)를 사용하여 격리 수준을 구현합니다. MVCC에서는 데이터의 여러 버전을 유지하여, 읽기와 쓰기가 서로를 차단하지 않도록 합니다.
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 단위 |
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 | 커밋된 데이터만 기록 |
격리 수준과 성능
격리 수준이 높아질수록 락 경합이 증가하여 처리량이 감소합니다.
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단계 잠금을 다루겠습니다.