격리 수준
동시성 문제를 어느 수준까지 막을지 조절하는 설정이 격리 수준(Isolation Level)입니다. SQL 표준(SQL:1992)은 네 가지 격리 수준을 정의하지만, 실제 DBMS는 MVCC, 잠금, 갭 락, 직렬화 검증을 서로 다르게 조합합니다. 따라서 격리 수준은 “정합성과 동시성의 스위치”라기보다, 특정 이상 현상을 얼마나 허용할지 정하는 실무 선택지로 이해하는 것이 좋습니다.
동시성 문제 유형 복습
격리 수준을 이해하려면 먼저 SQL 표준이 다루는 세 가지 읽기 이상 현상과, 실무에서 자주 함께 다루는 갱신 분실을 구분해야 합니다.
Dirty Read (오손 읽기)
커밋되지 않은 데이터를 읽는 현상입니다. 다른 트랜잭션이 변경 중인 값을 읽었는데, 그 트랜잭션이 ROLLBACK되면 읽은 데이터는 존재하지 않았던 것이 됩니다.
Non-Repeatable Read (반복 불가능 읽기)
같은 트랜잭션에서 같은 데이터를 두 번 읽었을 때 값이 달라지는 현상입니다. 두 번의 읽기 사이에 다른 트랜잭션이 해당 행을 UPDATE하고 COMMIT했기 때문입니다.
T1 T2
─── ───
READ 잔고 → 100만원
UPDATE 잔고 = 50만원
COMMIT
READ 잔고 → 50만원
↑ 같은 트랜잭션 내에서 값이 변했다!Phantom Read (팬텀 읽기)
같은 조건으로 여러 행을 조회했을 때, 두 번째 조회에서 이전에 없던 행이 새로 나타나거나 있던 행이 사라지는 현상입니다. 다른 트랜잭션이 INSERT, DELETE, 또는 조건을 만족하도록 바꾸는 UPDATE를 수행하고 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 | 발생 가능 | 발생 가능 | 발생 가능 | PostgreSQL은 사실상 RC로 동작 |
| READ COMMITTED | 방지 | 발생 가능 | 발생 가능 | 문장 단위 일관성이 일반적 |
| REPEATABLE READ | 방지 | 방지 | DBMS별 차이 | InnoDB, PostgreSQL 구현 차이 |
| SERIALIZABLE | 방지 | 방지 | 방지 | 성공 커밋 기준, 재시도 가능 |
위 표는 표준을 이해하기 위한 큰 방향입니다. 실제 허용 여부와 성능 비용은 DBMS, 인덱스, 쿼리 형태, 읽기 방식(일반 SELECT인지 locking read인지)에 따라 달라집니다.
또한 갱신 분실은 위 세 가지 읽기 이상 현상과 별도로 봐야 합니다. 같은 READ COMMITTED라도 단일 원자적 UPDATE를 쓰는지, 애플리케이션이 읽은 낡은 값을 절대값으로 저장하는지, 버전 조건을 확인하는지에 따라 결과가 달라질 수 있습니다.
READ UNCOMMITTED
가장 낮은 격리 수준입니다. MySQL이나 SQL Server에서는 커밋되지 않은 데이터까지 읽을 수 있어 Dirty Read가 가능합니다. PostgreSQL은 문법상 READ UNCOMMITTED를 받지만 내부적으로 READ COMMITTED처럼 동작하고, Oracle은 READ UNCOMMITTED를 제공하지 않습니다.
-- MySQL
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 다음 트랜잭션부터 적용하거나, BEGIN 전에 실행하는 방식으로 사용
-- SQL Server
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 또는 쿼리 힌트로 사용
SELECT * FROM 주문 WITH (NOLOCK);실무에서는 매우 제한적으로만 사용합니다. NOLOCK류 조회는 Dirty Read뿐 아니라 중복 읽기, 누락, 이동 중인 행 관찰 같은 부작용도 생길 수 있습니다. 대략적인 통계라도 값이 틀려도 되는지, 재시도와 캐시로 해결할 수 없는지 먼저 확인해야 합니다.
READ COMMITTED
커밋된 데이터만 읽을 수 있습니다. Oracle, PostgreSQL, SQL Server의 일반 기본 격리 수준입니다. 단, SQL Server는 READ_COMMITTED_SNAPSHOT 설정에 따라 잠금 기반 읽기와 row versioning 기반 읽기가 달라지고, Azure SQL 계열은 기본 설정이 다를 수 있습니다.
-- Oracle (기본값, 명시적 설정 시)
ALTER SESSION SET ISOLATION_LEVEL = READ COMMITTED;
-- MySQL
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- PostgreSQL (기본값)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;READ COMMITTED 동작 원리
각 문장이 시작할 때 커밋되어 있던 데이터를 읽는 방식이 일반적입니다. MVCC DBMS에서는 문장 단위 스냅샷을 사용하고, SQL Server처럼 설정에 따라 공유 락으로 읽기를 보호하는 제품도 있습니다.
Dirty Read는 방지되지만, 같은 트랜잭션 내에서 같은 쿼리의 결과가 달라질 수 있습니다(Non-Repeatable Read, Phantom Read). 많은 OLTP 요청은 짧은 트랜잭션과 원자적 UPDATE를 함께 쓰면 READ COMMITTED로 충분하지만, 같은 트랜잭션 안에서 일관된 스냅샷이 필요한 보고서나 검증 흐름은 더 높은 수준이나 명시적 잠금을 검토합니다.
격리 수준 설정은 DBMS마다 적용 범위가 다릅니다. 세션 전체에 적용되는 설정인지, 다음 트랜잭션에만 적용되는 설정인지, BEGIN 이후 첫 문장 전에만 허용되는 설정인지 확인해야 합니다.
Oracle의 READ COMMITTED 구현
Oracle은 Undo Segment와 SCN(System Change Number)을 사용합니다. SELECT가 실행되는 시점의 SCN을 기준으로, 해당 SCN 이전에 커밋된 데이터만 읽습니다. 변경 전 데이터가 필요하면 Undo Segment에서 읽어옵니다.
REPEATABLE READ
같은 트랜잭션 안의 일반 읽기가 일관된 스냅샷을 보도록 하는 수준입니다. MySQL InnoDB의 기본 격리 수준이며, PostgreSQL도 같은 이름을 지원하지만 구현과 충돌 처리 방식은 다릅니다.
-- MySQL (기본값)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;REPEATABLE READ 동작 원리
일반적으로 첫 읽기 또는 트랜잭션의 기준 시점에 스냅샷이 정해지고, 트랜잭션이 끝날 때까지 일반 SELECT는 그 스냅샷을 기준으로 읽습니다. 다른 트랜잭션이 데이터를 변경하고 커밋해도 기존 스냅샷은 변하지 않습니다. 다만 UPDATE, DELETE, SELECT FOR UPDATE 같은 locking read는 현재 행과 잠금 규칙의 영향을 받습니다.
이 때문에 같은 트랜잭션 안에서도 "일반 SELECT가 보는 스냅샷"과 "UPDATE/locking read가 다루는 현재 행"을 섞어 쓰면 처음 예상한 것과 다른 대기, 충돌, 재조회가 발생할 수 있습니다.
MySQL InnoDB에서의 Phantom Read 방지
SQL 표준 표에서는 REPEATABLE READ가 Phantom Read를 허용하는 것으로 설명됩니다. 그러나 MySQL InnoDB의 일반 consistent read는 같은 ReadView를 보므로 같은 조건 조회 결과가 유지되고, locking read나 UPDATE/DELETE는 인덱스 범위에 next-key lock/gap lock을 사용해 새 행 유입을 막을 수 있습니다. 대신 잠금 범위는 인덱스와 조건 형태에 크게 의존합니다.
PostgreSQL의 REPEATABLE READ
PostgreSQL의 REPEATABLE READ는 스냅샷 격리(Snapshot Isolation)에 가깝게 동작합니다. 트랜잭션 기준 스냅샷으로 Dirty Read, Non-Repeatable Read, Phantom Read를 막지만, 모든 직렬화 이상을 막는 것은 아니므로 write skew 같은 패턴은 SERIALIZABLE에서 다룹니다. 이미 바뀐 행을 갱신하려 하면 동시 갱신 오류가 날 수 있어 재시도 처리가 필요합니다.
SERIALIZABLE
가장 높은 격리 수준입니다. 성공적으로 커밋된 트랜잭션들의 결과가 어떤 직렬 순서로 실행된 것과 같도록 만드는 것을 목표로 합니다.
-- Oracle
ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE;
-- MySQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;DBMS별 SERIALIZABLE 구현 차이
구현 방식은 DBMS마다 다릅니다. MySQL InnoDB는 잠금 읽기를 강화하고, PostgreSQL은 SSI로 위험한 의존성을 감지해 한쪽을 실패시킬 수 있으며, Oracle은 트랜잭션 수준 읽기 일관성과 충돌 감지를 사용합니다. 동시성이 낮아질 수 있지만, 중요한 불변식이 여러 행이나 조건 범위에 걸릴 때는 SERIALIZABLE 또는 동등한 잠금/제약 설계를 검토합니다. 어떤 방식이든 직렬화 실패, 데드락, 충돌 오류는 정상적인 제어 흐름으로 보고 재시도 정책을 준비해야 합니다.
MVCC와 격리 수준의 관계
많은 현대 DBMS는 MVCC(Multi-Version Concurrency Control) 또는 row versioning을 사용하여 읽기 일관성을 구현합니다. MVCC에서는 데이터의 여러 버전을 유지하여 일반 읽기와 쓰기가 서로를 덜 차단하도록 하지만, 쓰기 충돌과 직렬화 이상은 별도 잠금이나 충돌 감지가 필요합니다.
DBMS별 MVCC 구현
| 항목 | Oracle | MySQL (InnoDB) | PostgreSQL | SQL Server |
|---|---|---|---|---|
| 구현 위치 | Undo Segment | Undo Log + ReadView | Heap Tuple (다중 버전) | tempdb version store |
| 버전 식별 | SCN (System Change Number) | Transaction ID + ReadView | xmin/xmax (트랜잭션 ID) | transaction sequence/version pointer |
| 이전 버전 보관 | Undo Tablespace | Undo Log | 기본 테이블 내 | tempdb |
| 정리 작업 | Undo Retention 정책 | Purge Thread | VACUUM | version cleanup |
| 읽기 일관성 | Statement 단위(RC), Tx 단위(S) | Statement(RC), Tx 단위(RR) | Statement(RC), Tx 단위 | RCSI는 문장, SNAPSHOT은 Tx 단위 |
Oracle vs MySQL 기본 격리 수준
| 항목 | Oracle | MySQL (InnoDB) |
|---|---|---|
| 기본 격리 수준 | READ COMMITTED | REPEATABLE READ |
| 지원 격리 수준 | READ COMMITTED, SERIALIZABLE, READ ONLY 트랜잭션 | 4가지 모두 |
| MVCC 구현 | Undo Segment + SCN | Undo Log + ReadView |
| 팬텀 처리 | SERIALIZABLE/READ ONLY에서 Tx 일관성 | RR consistent read, next-key/gap lock |
| 읽기 일관성 | Statement 수준(RC) | Transaction 수준(RR 일반 읽기) |
Oracle은 READ UNCOMMITTED와 REPEATABLE READ 이름을 제공하지 않습니다. 대신 READ COMMITTED의 문장 단위 일관성, SERIALIZABLE 또는 READ ONLY 트랜잭션의 트랜잭션 단위 일관성을 제공합니다. 특정 행을 수정 전제로 보호해야 할 때는 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가 필요한 경우
-- 트랜잭션 내 조회 기준을 유지하면서 주문을 생성
-- 실제 차감은 조건부 UPDATE로 충돌과 잔액 부족을 확인
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT stock FROM products WHERE id = 100; -- stock = 5
-- 비즈니스 로직: 재고가 충분한지 확인
-- ... 여러 쿼리 수행 ...
SELECT stock FROM products WHERE id = 100; -- 일반 SELECT는 같은 스냅샷 기준
UPDATE products
SET stock = stock - 1
WHERE id = 100 AND stock >= 1;
-- 영향받은 행이 0이면 재고 부족 또는 동시 차감 충돌
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';
-- 실제 서비스에서는 balance >= 30 조건, 제약, 영향 행 수 확인을 함께 둔다
-- 계좌 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 + 원자적 UPDATE | 짧은 요청, 높은 동시성 |
| 재고/좌석 예약 | 조건부 UPDATE, 잠금, unique 제약 | 중복 예약과 음수 재고 방지 |
| 금융 거래 | SERIALIZABLE, 비관적 락, 제약/재시도 | 여러 행 불변식과 실패 비용 고려 |
| 대량 데이터 조회 | READ COMMITTED 또는 스냅샷 조회 | 일관된 리포트인지 최신값인지 선택 |
| 배치 작업 | chunk 단위 + 재시도 정책 | 실패 범위와 락 보유 시간 통제 |
| 감사/로깅 | READ COMMITTED, Outbox | 커밋된 사실과 발행 원자성 기록 |
격리 수준과 성능
격리 수준이 높아질수록 대기, 충돌 실패, 재시도, 버전 유지 비용이 증가할 수 있습니다.
MVCC를 사용하는 DBMS에서는 일반 읽기만 보면 READ COMMITTED와 REPEATABLE READ의 차이가 작게 보일 수 있습니다. 하지만 긴 트랜잭션은 오래된 버전 정리를 지연시키고, locking read, UPDATE/DELETE, gap lock, SERIALIZABLE 검증은 성능과 실패율에 영향을 줍니다.
정리
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read | 실무 메모 |
|---|---|---|---|---|
| READ UNCOMMITTED | 가능 | 가능 | 가능 | PostgreSQL은 사실상 RC |
| READ COMMITTED | 방지 | 가능 | 가능 | 기본값인 DBMS가 많음 |
| REPEATABLE READ | 방지 | 방지 | DBMS별 차이 | MySQL/PG 구현 차이 확인 |
| SERIALIZABLE | 방지 | 방지 | 방지 | 성공 커밋 기준, 재시도 전제 |
격리 수준의 핵심은 트레이드오프입니다. 강한 격리를 원하면 대기나 재시도 비용이 늘고, 높은 동시성을 원하면 일부 이상 현상을 애플리케이션 설계로 흡수해야 합니다. 실무에서는 DBMS 기본 격리 수준을 이해한 뒤, 정합성이 중요한 특정 경로에 조건부 UPDATE, 명시적 락, unique 제약, SERIALIZABLE, 재시도 정책을 조합하는 전략이 효과적입니다.
다음 절에서는 격리 수준을 구현하는 기술인 락과 2단계 잠금을 다루겠습니다.