본문 바로가기
Backend/Spring

[Spring] Transaction - 격리 수준(Isolation)

by 제이동 개발자 2025. 8. 17.
728x90

[Spring] Transaction - 격리 수준(Isolation)

 트랜잭션 격리수준(Isolation level)동시성일관성의 균형을 잡는 핵심 레버입니다. Spring의 @Transactional(isolation = …) 한 줄은 결국 JDBC Connection의 isolation을 조정해 DB 엔진(InnoDB, PostgreSQL 등)에 동작을 위임할 수 있습니다.

 

 

1. 이상현상

 DB 트랜잭션에서 자주 말하는 세 가지 이상현상이 있습니다. 이상현상을 알아야 Isolation을 알 수 있기 때문에 참고하시길 바랍니다.

Isolation Dirty Read Non-Repeatable Read Phantom Read
READ_UNCOMMITTED ✅ (이상현상 발생) ✅ (이상현상 발생) ✅ (이상현상 발생)
READ_COMMITTED ✅ (이상현상 발생) ✅ (이상현상 발생)
REPEATABLE_READ (DB별 상이)
SERIALIZABLE

 

 

1-1. Dirty Read (더티 리드)

1) 정의

  • 다른 트랜잭션이 커밋하지 않은 변경 내용을 읽는 현상.
  • JPA 관점에서 얘기하면 flush 상태를 말할 수 있습니다.

 

2) Dirty Read의 이상 현상

  • 상대 트랜잭션이 롤백하면, 내가 읽은 값은 존재하지 않았던 값이 됨.

예시 (T1=작성, T2=조회)

  • T1: UPDATE account SET balance = balance - 100 WHERE id=1; (아직 커밋 안 함)
  • T2: SELECT balance FROM account WHERE id=1; → 감소된 값을 봄
  • T1: ROLLBACK
  • 결과: T2가 본 값은 “유령 데이터”

 

3) 격리수준에 따른 이상 현상 발생 여부

  • 이상현상 발생 X : READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
  • 이상현상 발생 O : READ_UNCOMMITTED

 

1-2. Non-Repeatable Read (비반복 가능 읽기)

1) 정의

  • 같은 트랜잭션 안에서 같은 행을 두 번 읽었는데, 그 사이에 다른 트랜잭션이 커밋해 값이 달라지는 현상.

 

2) Non-Repeatable Read의 이상 현상

예시 (T1=두 번 읽기, T2=수정 후 커밋)

  • T1: SELECT balance FROM account WHERE id=1; → 1000
  • T2: UPDATE account SET balance=900 WHERE id=1; COMMIT;
  • T1: (같은 트랜잭션에서) SELECT balance ... → 900 (값이 달라짐)

 

3) 격리수준에 따른 이상 현상 발생 여부

  • 이상현상 발생 X : REPEATABLE_READ, SERIALIZABLE
  • 이상현상 발생 O : READ_COMMITTED, READ_UNCOMMITTED
❗️ JPA 사용시 : 같은 영속성 컨텍스트(1차 캐시)에서 같은 엔티티를 다시 조회하면 캐시가 반환되어 값이 안 바뀐 것처럼 보일 수 있습니다. EntityManager.clear()/refresh()를 해야 비반복 가능 읽기가 드러납니다. (격리수준과 별개 이슈)

 

1-3. Phantom Read (팬텀 리드)

1) 정의

  • 같은 트랜잭션 안에서 동일한 조건의 범위 조회를 두 번 했는데, 그 사이에 다른 트랜잭션의 INSERT/DELETE 커밋으로 행의 개수(혹은 매칭 집합)가 달라지는 현상.

 

2) Phantom Read (팬텀 리드)의 이상 현상

예시 (T1=범위조회, T2=신규 행 삽입 커밋)

  • T1: SELECT * FROM orders WHERE price > 100; → 10행
  • T2: INSERT INTO orders(price) VALUES (150); COMMIT;
  • T1: (같은 트랜잭션) SELECT * FROM orders WHERE price > 100; → 11행 (새 행이 “팬텀”처럼 나타남)

 

3) 격리수준에 따른 이상 현상 발생 여부

  • 이상현상 발생 X : SERIALIZABLE
  • 이상현상 발생 O : DB 별 상이

 

 

 

2. 격리수준

DB 별 기본 값은 아래와 같습니다.

  • MySQL/InnoDB: 기본 REPEATABLE_READ
  • PostgreSQL: 기본 READ_COMMITTED
  • Oracle: 기본 READ_COMMITTED

 

2-1. READ_UNCOMMITTED

  • 특징: 커밋 전 데이터까지 읽을 수 있음
  • 장점: 이론상 가장 빠름
  • 단점: 일관성 붕괴 위험이 큼 → 실무에서 거의 금지
  • 발생 가능한 이상현상
    1. Dirty Read
    2. Non-Repeatable Read
    3. Phantom Read
  • 사용: 사실상 사용 지양

 

2-2. READ_COMMITTED

 

  • 특징: 커밋된 데이터만 읽음(더티 리드 금지). 두 번 읽으면 값이 달라질 수 있음
  • 장점: 대부분의 OLTP 기본값(PG/Oracle/SQL Server 기본). 잠금 경합 비교적 낮음
  • 단점: Non-Repeatable/Phantom 가능
  • 발생 가능한 이상현상
    1. Non-Repeatable Read
    2. Phantom Read
  • 사용: 일반 조회/업무 로직의 기본값으로 적합

 

2-3. REPEATABLE_READ

 

  • 특징: 같은 로우를 동일 트랜잭션에서 반복 조회 시 값이 변하지 않도록 보장
  • 장점: 비반복 읽기 방지 → 조회 일관성 ↑
  • 단점: 범위잠금/갭락으로 동시성 저하/데드락 증가 가능
  • 발생 가능한 이상현상
    1. DB 별 상이
  • 사용: 참조 일관성이 중요한 조회 + 업데이트 혼재 상황. MySQL 기본

1) REPEATABLE_READ READ_COMMITTED 차이

 

  • T1 시작 → SELECT id=1 ⇒ 이름 “Alice” 조회
    → 이때 스냅샷 고정
  • T2 시작 → id=1을 “Bob”으로 UPDATE 후 커밋
  • T1에서 다시 SELECT id=1
    • REPEATABLE_READ: 여전히 “Alice” (스냅샷 유지)
    • READ_COMMITTED: 이번엔 “Bob” (문장마다 최신 커밋본)

 

 

2-4. SERIALIZABLE

 

  • 특징: 직렬 실행과 같은 효과(가장 강함)
  • 장점: Dirty/Non-Repeatable/Phantom 모두 차단
  • 단점: 처리량 급감, 충돌 시 재시도 증가, 동시성이 크게 떨어지고 락 경합/대기 시간이 증가
  • 발생 가능한 이상현상 없음
  • 사용: 금융 등 강 일관성이 최우선인 극히 일부 케이스에 짧은 구간

 

 

3. Default 격리수준 변경 케이스

 일반적으로 격리수준은 Default로 사용하는 것이 좋습니다. 하지만 실 서비스를 개발하다 보면 Default 격리수준을 사용하다보면 여러 이슈가 발생하게 되는데 이때 격리수준을 변경하여 해당 이슈들을 해결해야 합니다. 단, 격리수준을 변경하기 전에 변경하지 않고도 해결할 수 있다면 변경하지 않고 해결하는 것이 좋습니다.

 

  • 증상 파악
    • 데드락 증가(1213/40001), 대기/타임아웃, purge 지연, “신선도” 민원 등
  • 국소적 적용
    • 전역 기본값은 유지하고, 문제 메서드/흐름만 바꾼다.
    • @Transactional(isolation = …) 한 트랜잭션 범위에만 적용
  • 보조 전략 병행 - 우선 적용
    • 유니크 인덱스/정확한 인덱싱/쿼리 범위 축소
    • FOR UPDATE SKIP LOCKED(MySQL 8+)로 작업 픽업
    • 읽기 API는 readOnly = true로 JPA 오버헤드↓ → QueryDSL 성능
    • 재시도 정책(낙관적 락 충돌/데드락) 표준화

 

 

 아래 케이스들은 MySQL 기반으로 작성했습니다. 타 DB에서 동일한 케이스로 적용가능한지 여부는 확인해보지 않았으니 참고 부탁드리며 다양한 환경들이 존재하기 때문에 아래 정리한 케이스들이 '무조건 맞다'라고 할 수 없습니다. 따라서 사용 시 본인의 판단하에 적절한 방법을 선택하시길 바랍니다.

 

 

 

3-1. READ_COMMITTED 선택 케이스

1) Gap Lock / Next Key Lock으로 인한 데드락∙대기가 잦을 때

 

  • 징후: 범위 UPDATE/DELETE나 SELECT ... FOR UPDATE 중 교차 범위에서 데드락이 자주 발생.
  • 이유: REPEATABLE_READ는 팬텀 방지를 위해 갭락/넥스트키락을 많이 잡습니다.
  • 대응: 해당 트랜잭션만 READ_COMMITTED로 낮추면 InnoDB가 레코드락 위주로 잠가 충돌을 줄일 수 있습니다

 

2) 대용량 읽기/리포팅/배치에서 스냅샷 유지 비용을 줄이고 싶을 때

 

  • 징후: 긴 트랜잭션이 undo 기록을 오래 붙잡아 purge 지연/히스토리 폭증.
  • 이유: REPEATABLE_READ는 트랜잭션 동안 동일 스냅샷을 유지합니다.
  • 대응: 읽기 전용 흐름에 한해 READ_COMMITTED(문장 단위 스냅샷)로 전환하고, 트랜잭션을 짧게 유지.
💡 긴 트랜잭션이 undo 기록을 오래 붙잡아 purge 지연 /히스토리 폭증 설명
트랜잭션이 오래 열려 있으면 MySQL(InnoDB)이 예전에 찍어둔 “과거 스냅샷”을 보여주기 위해 과거 버전(undo 로그)을 계속  보관해야 하고, 그래서 청소(purge)를 못 해서 쓰레기(히스토리)가 쌓입니다.
→ 디스크/메모리/성능에 악영향.

REPEATABLE_READ는 트랜잭션 전체에 동일 스냅샷을 유지하기 때문에 오래 열려 있으면 purge가 지연됩니다.

 

 

 

3) CQRS/리드 레플리카에서 읽기 신선도가 더 중요한 경우

 

  • 징후: “방금 쓴 데이터가 왜 안 보이죠?” 같은 사용자 피드백.
  • 이유: REPEATABLE_READ에선 한 트랜잭션 안에서 반복 조회 시 같은 결과를 보장(=신선도↓).
  • 대응: UI용 조회 API만 READ_COMMITTED로 하여 신선도를 높임(여전히 커밋된 데이터만 읽음)

 

 

3-2. SERIALIZABLE 선택 케이스

1) 스냅샷 기반의 Write Skew를 확실히 막아야 할 때

 

  • 징후: “조건을 만족하는 행이 없을 때만 삽입/갱신”하는 규칙이 동시성에서 깨짐.
  • : 동일 자원 동시간대 이중 예약 금지, “활성 세션은 최대 N개” 등 범위 불변식.
  • 대응 우선순위
    1. 유니크 인덱스로 스키마에서 강제 (가능하면 최우선)
    2. 잠금 읽기(FOR UPDATE / PESSIMISTIC_WRITE)로 “없음” 판단 구간 잠금
    3. 그래도 불안하면 아주 짧은 구간만 SERIALIZABLE
  • 경전역/광범위하게 SERIALIZABLE을 쓰면 처리량 급감 + 재시도 폭증하므로 주의.
💡 Write Skew란?
두 트랜잭션(T1, T2)이 각자 “조건을 만족하는 행이 없다”고 판단해서 서로 다른 행을 추가/수정하고 둘 다 커밋 해버려, 전역 불변식(“동시에 한 명만 가능”, “중복 예약 금지” 등)이 깨지는 현상

 

3-3. READ_UNCOMMITTED을 선택 케이스

  • 특성: 더티 리드 허용(커밋 전 데이터까지 읽음) → 일관성 붕괴 위험 큼
  • 용도: 앱 코드에서는 거의 금지. 모니터링/샘플링 같은 허용 오차가 매우 큰 오프라인 분석에나 한시적 사용.

 

 

 

 

 

728x90