격리 수준

여러 트랜잭션이 동시에 실행될 때 서로 얼마나 격리할지 정하는 수준입니다.


4단계 격리 수준

레벨설명
READ UNCOMMITTED커밋 안 된 데이터도 읽음
READ COMMITTED커밋된 데이터만 읽음
REPEATABLE READ같은 쿼리는 같은 결과 보장
SERIALIZABLE완전 격리

발생 가능한 문제 4가지

1. Dirty Read

커밋 안 된 데이터를 읽음

A: UPDATE age = 30 (커밋 전)
B: SELECT age → 30 읽음
A: ROLLBACK
B: 30을 믿고 로직 수행 → 문제!

2. Non-Repeatable Read

같은 row인데 값이 달라짐

A: SELECT age → 25
B: UPDATE age = 30, COMMIT
A: SELECT age → 30 (값이 바뀜!)

3. Phantom Read

없던 row가 생김

A: SELECT * WHERE age > 20 → 3건
B: INSERT (age=25), COMMIT
A: SELECT * WHERE age > 20 → 4건 (row 추가됨!)

4. Write Skew

각 트랜잭션은 규칙을 지켰지만, 결합하면 규칙이 깨짐

다른 이상 현상과 달리 단일 행이 아닌 여러 행에 걸친 제약 조건이 위반되는 현상입니다.

[규칙: 최소 1명의 의사가 당직이어야 함]
현재 상태: 의사A = 당직, 의사B = 당직 (2명 당직 중)

트랜잭션A (의사A): SELECT COUNT(*) WHERE 당직 = true → 2명
                  → "2명이니까 내가 빠져도 1명 남음" → UPDATE 의사A = 비당직

트랜잭션B (의사B): SELECT COUNT(*) WHERE 당직 = true → 2명
                  → "2명이니까 내가 빠져도 1명 남음" → UPDATE 의사B = 비당직

둘 다 COMMIT → 당직 의사 0명 → 규칙 위반! X
[Dirty Read / Non-Repeatable Read / Phantom Read와의 차이]

Dirty Read:          하나의 행에서 커밋 안 된 값을 읽음
Non-Repeatable Read: 하나의 행의 값이 달라짐
Phantom Read:        행의 수가 달라짐
Write Skew:          각각의 읽기와 쓰기는 정상이지만
                     두 트랜잭션의 쓰기가 결합되면 제약 조건 위반

→ 읽기 이상이 아닌 "쓰기 이상"
→ 개별 트랜잭션만 보면 문제가 없어서 감지가 어려움

Write Skew의 다른 예시들:

[예시 1: 회의실 중복 예약]
규칙: 같은 시간대에 같은 회의실 예약 불가

트랜잭션A: SELECT → 10시 예약 없음 → INSERT 10시 예약
트랜잭션B: SELECT → 10시 예약 없음 → INSERT 10시 예약
둘 다 COMMIT → 10시에 2건 예약 → 중복! X

[예시 2: 잔액 제약]
규칙: 계좌A + 계좌B 합계가 0 이상이어야 함
현재: 계좌A = 100, 계좌B = 100 (합계 200)

트랜잭션1: SELECT 합계 → 200 → UPDATE 계좌A = 계좌A - 200
트랜잭션2: SELECT 합계 → 200 → UPDATE 계좌B = 계좌B - 200
둘 다 COMMIT → 합계 = -200 → 규칙 위반! X

Write Skew 방지 방법:

1. SERIALIZABLE 격리 수준 사용
   → 트랜잭션을 순차 실행하므로 Write Skew 원천 차단

2. SELECT ... FOR UPDATE로 관련 행 잠금
   SELECT COUNT(*) FROM doctors WHERE 당직 = true FOR UPDATE;
   → 조회 대상 행에 배타 락 → 다른 트랜잭션 대기

3. 제약 조건을 DB 레벨에서 강제 (가능한 경우)
   → CHECK 제약, TRIGGER 등으로 규칙 위반 시 롤백

4. 낙관적 락 + 재시도
   → 버전 비교 후 충돌 감지 시 재시도
// Spring에서 Write Skew 방지 — FOR UPDATE 사용
@Transactional
public void cancelDuty(Long doctorId) {
    // 당직 의사 목록을 FOR UPDATE로 잠금
    int onDutyCount = doctorRepository.countOnDutyForUpdate();

    if (onDutyCount <= 1) {
        throw new IllegalStateException("최소 1명의 당직 의사가 필요합니다");
    }

    Doctor doctor = doctorRepository.findById(doctorId).orElseThrow();
    doctor.cancelDuty();
}

격리 수준별 문제 발생

레벨Dirty ReadNon-Repeatable ReadPhantom ReadWrite Skew
READ UNCOMMITTEDOOOO
READ COMMITTEDXOOO
REPEATABLE READXXOO
SERIALIZABLEXXXX

아래로 갈수록 안전하지만 느려집니다.

참고: Write Skew는 SERIALIZABLE에서만 완전히 방지됩니다. REPEATABLE READ 이하에서는 SELECT ... FOR UPDATE로 명시적으로 방지해야 합니다.


각 레벨 상세

READ UNCOMMITTED

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

READ COMMITTED

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

REPEATABLE READ

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SERIALIZABLE

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

격리 수준 vs 성능

격리 수준 높음 → 안전 ↑ 동시성 ↓ 성능 ↓
격리 수준 낮음 → 안전 ↓ 동시성 ↑ 성능 ↑

기본값

DB기본 격리 수준
MySQLREPEATABLE READ
PostgreSQLREAD COMMITTED
OracleREAD COMMITTED

각 격리 레벨 동작 방식

READ UNCOMMITTED

읽기: 락 X, 스냅샷 X → 메모리에 있는 값 그대로 읽음
쓰기: 배타 락 + 최신 데이터
트랜잭션 1                     트랜잭션 2
─────────                     ─────────
UPDATE 나이 = 30 (커밋 전)
                              SELECT 나이 → 30 (커밋 안 된 값!)
ROLLBACK
                              30을 믿고 로직 수행 → 문제!

READ COMMITTED

읽기: 쿼리 시점 스냅샷 (쿼리마다 새로운 스냅샷)
쓰기: 배타 락 + 최신 데이터
트랜잭션 1                     트랜잭션 2
─────────                     ─────────
SELECT 나이 → 25 (현재 스냅샷)
                              UPDATE 나이 = 30
                              COMMIT
SELECT 나이 → 30 (새 스냅샷 → 값이 바뀜!)

REPEATABLE READ

읽기: 트랜잭션 시작 시점 스냅샷 (끝까지 유지)
쓰기: 배타 락 + 최신 데이터
트랜잭션 1 (10시 시작)          트랜잭션 2
─────────                     ─────────
SELECT 나이 → 25 (10시 스냅샷)
                              UPDATE 나이 = 30
                              COMMIT
SELECT 나이 → 25 (여전히 10시 스냅샷)

SERIALIZABLE

읽기: 트랜잭션 시작 시점 스냅샷 + 공유 락
쓰기: 배타 락 + 최신 데이터
트랜잭션 1                     트랜잭션 2
─────────                     ─────────
SELECT * WHERE 나이 >= 25
(스냅샷 + 공유 락)
                              INSERT (나이=28)
                              → 락 대기... (블로킹!)
COMMIT (락 해제)
                              (이제 INSERT 가능)

락 정리

락 종류다른 읽기다른 쓰기
공유 락 (읽기)가능대기
배타 락 (쓰기)대기대기

격리 수준별 동작 요약

격리 수준읽기 방식쓰기 방식
READ UNCOMMITTED현재 메모리 값배타 락 + 최신 데이터
READ COMMITTED쿼리 시점 스냅샷배타 락 + 최신 데이터
REPEATABLE READ트랜잭션 시작 스냅샷배타 락 + 최신 데이터
SERIALIZABLE트랜잭션 시작 스냅샷 + 공유 락배타 락 + 최신 데이터

격리 수준 = MVCC + LBCC의 조합 비율

READ UNCOMMITTED ──→ READ COMMITTED ──→ REPEATABLE READ ──→ SERIALIZABLE

MVCC 의존도:  없음         높음              높음               중간
LBCC 의존도:  없음         낮음              중간               높음

                    MVCC ████████████████████████████░░░░░░░░
                    LBCC ░░░░░░░░░░░░████████████████████████

                    ← MVCC 비중 높음        LBCC 비중 높음 →
                    ← 동시성 높음           정합성 높음 →

격리 수준별 MVCC/LBCC 사용 내역

READ UNCOMMITTED

MVCC: 사용 안 함
LBCC: 사용 안 함

일반 SELECT: 현재 데이터를 그대로 읽음 (버전 관리 없음)
쓰기:        배타 락 (최소한의 보호)

→ 거의 아무런 보호 장치 없음
→ 실무에서 사용하지 않음

READ COMMITTED

MVCC: ████████ (주력)
LBCC: ██ (보조)

일반 SELECT:           MVCC (매 SELECT마다 새 스냅샷)  → 락 없음
SELECT ... FOR UPDATE: LBCC (레코드 락)
UPDATE / DELETE:       LBCC (레코드 락)
INSERT:                LBCC (레코드 락)

→ 읽기는 MVCC가 전담 → 동시성 높음
→ 쓰기만 LBCC 사용 → 최소한의 락

REPEATABLE READ (InnoDB)

MVCC: ██████ (주력)
LBCC: ████ (강화)

일반 SELECT:           MVCC (트랜잭션 시작 시 스냅샷 고정) → 락 없음
SELECT ... FOR UPDATE: LBCC (레코드 락 + 갭 락)
UPDATE / DELETE:       LBCC (레코드 락 + 갭 락)
INSERT:                LBCC (레코드 락)

→ 읽기는 여전히 MVCC → 동시성 유지
→ 쓰기에 갭 락 추가 → LBCC 범위 확대
→ MVCC 스냅샷 유지 기간도 늘어남

SERIALIZABLE

MVCC: ██ (보조)
LBCC: ████████ (주력)

일반 SELECT:           LBCC (자동으로 공유 락 + 갭 락) ← 핵심 차이
SELECT ... FOR UPDATE: LBCC (배타 락 + 갭 락)
UPDATE / DELETE:       LBCC (배타 락 + 갭 락)
INSERT:                LBCC (배타 락)

→ 읽기도 LBCC 사용 → 읽기-쓰기 상호 차단
→ MVCC는 버전 관리 용도로만 보조적 사용
→ 사실상 순차 실행
→ Write Skew까지 완전 방지

한눈에 비교

격리 수준일반 SELECT잠금 읽기 (FOR UPDATE)쓰기 (UPDATE/DELETE)
READ UNCOMMITTED보호 없음레코드 락레코드 락
READ COMMITTEDMVCC (매번 새 스냅샷)레코드 락레코드 락
REPEATABLE READMVCC (스냅샷 고정)레코드 락 + 갭 락레코드 락 + 갭 락
SERIALIZABLE공유 락 + 갭 락 (LBCC)배타 락 + 갭 락배타 락 + 갭 락
격리 수준이 올라갈수록:

MVCC 측면: 스냅샷 유지 기간이 길어짐
           (매번 새로 → 고정 → 고정)

LBCC 측면: 락의 범위와 종류가 추가됨
           (레코드 락 → + 갭 락 → + 일반 SELECT에도 공유 락)

비유로 이해하기

[도서관 비유]

READ UNCOMMITTED:
→ 아무나 자유롭게 열람/수정 가능
→ 누군가 수정 중인 책도 볼 수 있음
→ MVCC: X  LBCC: X

READ COMMITTED:
→ 수정이 완료된 책만 열람 가능 (MVCC — 완성된 버전만 제공)
→ 수정 중인 책은 이전 버전을 보여줌
→ MVCC: O  LBCC: 쓰기만

REPEATABLE READ:
→ 입장 시점의 책 상태를 계속 봄 (MVCC — 스냅샷 고정)
→ 수정 시 주변 선반까지 잠금 (LBCC — 갭 락)
→ MVCC: O  LBCC: 쓰기 + 갭

SERIALIZABLE:
→ 열람할 때도 해당 선반 잠금 (LBCC — 읽기에도 락)
→ 한 사람이 보고 있으면 다른 사람은 수정 불가
→ MVCC: 보조  LBCC: 전면 적용

면접 포인트

CSLOG에서 보기