콘텐츠로 이동

트랜잭션 격리 수준 (Transaction Isolation Level)


격리 수준이 왜 필요한가

동시성 문제

여러 트랜잭션이 동시에 같은 데이터에 접근할 때 문제가 발생한다.

트랜잭션 A: 계좌 잔액 조회 중...
트랜잭션 B: 계좌 잔액 변경 중...
  → A가 변경 도중의 값을 읽으면 잘못된 결과!

격리 수준이 높으면 → 정확성 UP, 동시성 DOWN (성능 저하) 격리 수준이 낮으면 → 동시성 UP, 정확성 DOWN (이상 현상 발생 가능)

이 트레이드오프를 조절하기 위해 4단계의 격리 수준을 표준으로 정의했다.


격리 수준별 발생하는 이상 현상

Dirty Read (더티 리드)

커밋되지 않은 데이터를 읽는 현상

트랜잭션 A                     트랜잭션 B
                               UPDATE money = 2000 (아직 커밋 X)
SELECT money = 2000 (읽음!)
                               ROLLBACK  money는 다시 1000
 A는 존재하지 않는 (2000) 읽었다!

Non-Repeatable Read (반복 불가능한 읽기)

같은 트랜잭션 내에서 같은 데이터를 두 번 읽었는데 값이 달라지는 현상

트랜잭션 A                     트랜잭션 B
SELECT money = 1000
                               UPDATE money = 2000 + COMMIT
SELECT money = 2000  달라짐!
 같은 트랜잭션에서 같은 행을 읽었는데 값이 다르다!

Phantom Read (팬텀 리드)

같은 트랜잭션 내에서 같은 조건으로 조회했는데 결과 건수가 달라지는 현상

트랜잭션 A                     트랜잭션 B
SELECT COUNT(*) = 5 (WHERE age > 20)
                               INSERT new row (age=25) + COMMIT
SELECT COUNT(*) = 6  달라짐!
 없던 행이 갑자기 나타났다(Phantom)!

4가지 격리 수준

READ UNCOMMITTED (격리 수준 최하)

커밋되지 않은 변경 사항도 읽을 수 있다.

트랜잭션 A: 잔액 1000원 → 2000원으로 변경 (아직 커밋 X)
트랜잭션 B: 잔액 조회 → 2000원 (커밋 전 데이터 읽음)
트랜잭션 A: ROLLBACK → 잔액 다시 1000원
트랜잭션 B: 2000원이라는 잘못된 데이터 기반으로 처리...
  • 발생 문제: Dirty Read, Non-Repeatable Read, Phantom Read
  • 실무: 거의 사용하지 않음 (데이터 정합성이 너무 낮음)

READ COMMITTED (격리 수준 2)

커밋된 데이터만 읽는다. Oracle 기본값.

트랜잭션 A: 잔액 1000원 → 2000원으로 변경 (아직 커밋 X)
트랜잭션 B: 잔액 조회 → 1000원 (커밋 전이므로 undo log에서 읽음)
트랜잭션 A: COMMIT
트랜잭션 B: 잔액 다시 조회 → 2000원 (이제 커밋됐으므로)
→ 같은 트랜잭션에서 값이 달라짐!
  • 발생 문제: Non-Repeatable Read, Phantom Read
  • 실무: 가장 많이 사용 (Oracle 기본, AWS RDS 기본)

REPEATABLE READ (격리 수준 3)

트랜잭션 시작 전에 커밋된 데이터만 읽으며, 트랜잭션 내에서 같은 행은 항상 같은 값. MySQL InnoDB 기본값.

트랜잭션 B (트랜잭션 ID: 10) 시작
트랜잭션 A: 잔액 1000원 → 2000원으로 변경 + COMMIT (트랜잭션 ID: 11)
트랜잭션 B: 잔액 조회 → 1000원 (트랜잭션 ID 10 이전 데이터, undo log에서 읽음)
트랜잭션 B: 잔액 다시 조회 → 1000원 (여전히 동일!)
→ Non-Repeatable Read 없음!
[MVCC (Multi-Version Concurrency Control)]
변경 시 이전 버전을 undo log에 보관
읽기 시 트랜잭션 시작 시점의 undo log 버전 읽음
→ 읽기와 쓰기가 서로 블로킹하지 않음!
  • 발생 문제: Phantom Read (SELECT ... FOR UPDATE 등 잠금 읽기 시)
  • 실무: MySQL 기본값, 자주 사용

SERIALIZABLE (격리 수준 최상)

모든 트랜잭션을 순차적으로 실행하는 것처럼 처리. 읽기 시 공유 잠금 획득.

트랜잭션 A: SELECT * FROM order WHERE ... (공유 잠금 획득)
트랜잭션 B: INSERT INTO order ...  대기! (A가 공유 잠금 보유 )
트랜잭션 A: COMMIT  잠금 해제
트랜잭션 B: 이제 INSERT 가능
  • 발생 문제: 없음 (모든 이상 현상 방지)
  • 실무: 거의 사용하지 않음 (성능이 너무 낮음, 데드락 위험)

요약 표

격리 수준 Dirty Read Non-Repeatable Read Phantom Read 설명
READ UNCOMMITTED 발생 발생 발생 미완료 변경도 읽음
READ COMMITTED 없음 발생 발생 커밋된 데이터만 읽음
REPEATABLE READ 없음 없음 발생(일부) 트랜잭션 동안 같은 행은 고정
SERIALIZABLE 없음 없음 없음 완전 직렬화, 가장 안전

이상 현상 상세 설명

Dirty Read

아직 확정되지 않은 값을 읽어버리는 것.

트랜잭션 A                트랜잭션 B
BEGIN
UPDATE account SET money=2000 WHERE id=1
                         BEGIN
                         SELECT money FROM account WHERE id=1
                         -- 결과: 2000 (커밋 전!)
ROLLBACK
-- money는 다시 1000
                         -- B가 읽은 2000은 "더티 데이터"

Non-Repeatable Read

같은 데이터를 두 번 읽었는데 값이 달라지는 것.

트랜잭션 A                트랜잭션 B
BEGIN
SELECT money = 1000
                         BEGIN
                         UPDATE money=2000
                         COMMIT
SELECT money = 2000  달라짐!
-- 같은 트랜잭션인데 다른 값

문제 시나리오: 재고가 10개인지 확인 후 주문 → 주문 처리 사이에 재고가 0개로 변경됨.

Phantom Read

없던 데이터가 갑자기 나타나거나(또는 사라지는) 현상.

트랜잭션 A                트랜잭션 B
BEGIN
SELECT COUNT(*) = 5 WHERE age > 20
                         BEGIN
                         INSERT INTO members (age=25)
                         COMMIT
SELECT COUNT(*) = 6  행이 추가됨!
-- 없던 행이 나타남 (팬텀)

MySQL REPEATABLE READ에서의 특이점: - 일반 SELECT: MVCC로 팬텀 리드 방지 - SELECT ... FOR UPDATE (잠금 읽기): 팬텀 리드 발생 가능


DB별 기본 격리 수준

데이터베이스 기본 격리 수준
MySQL (InnoDB) REPEATABLE READ
Oracle READ COMMITTED
PostgreSQL READ COMMITTED
SQL Server READ COMMITTED
H2 READ COMMITTED

실무 선택 기준

상황 권장 격리 수준 이유
일반 웹 서비스 READ COMMITTED 성능과 정합성의 균형
금융 / 정산 REPEATABLE READ 반복 읽기 정합성 필요
배치 집계 작업 READ COMMITTED 대용량 데이터, 성능 중요
타임슬롯/예약 시스템 REPEATABLE READ + 잠금 동시 예약 방지
실시간 재고 관리 SELECT FOR UPDATE 정확한 재고 차감 필요

스프링에서 격리 수준 지정

// 기본 (DB 기본값 사용)
@Transactional
public void defaultIsolation() { ... }

// READ COMMITTED 명시
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedExample() { ... }

// REPEATABLE READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadExample() { ... }

// 금융 정산 등 중요한 작업
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalWork() { ... }

주의사항

격리 수준이 높을수록:
  - 잠금(Lock) 유지 시간이 길어짐
  - 동시 처리 처리량(Throughput) 감소
  - 데드락(Deadlock) 발생 위험 증가

실무 결론:
  READ COMMITTED + 필요한 곳에만 REPEATABLE READ 적용
  SERIALIZABLE은 절대적으로 필요한 경우 외에는 사용하지 않음