[SQL] MySQL 격리 수준

2025. 12. 26. 16:20·SQL

✅ 트랜잭션 격리 수준이란?

트랜잭션의 격리 수준(isolation level)이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것입니다.

격리 수준은 크게 `READ UNCOMMITTED`, `READ COMMITTED`, `REPEATABLE READ`, `SERIALIZABLE`로 4가지로 나뉘며, 순서대로 뒤로 갈수록 각 트랜잭션 간의 데이터 격리(고립) 정도가 높아지고 동시 처리 성능이 떨어진다고 볼 수 있습니다.

 

✅ READ UNCOMMITTED

`READ UNCOMMITTED` 격리 수준에서는 다음 시퀀스 다이어그램과 같이 각 트랜잭션에서의 변경 내용이 `commit`이나 `rollback` 여부에 상관없이 다른 트랜잭션이 보입니다. 

READ UNCOMMITTED

위와 같이 사용자 B는 사용자 A가 `INSERT`한 정보를 커밋되지 않은 상태에서도 조회할 수 있습니다. 문제는 사용자 A가 처리 도중 문제가 발생해 `INSERT`된 내용을 롤백하더라도 여전히 사용자 B는 조회한 결과(Lee)가 정상적인 정보라고 생각하고 계속 처리할 것이라는 점입니다.

 

이렇게 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상을 더티 리드(Dirty read)라 하고, 더티 리드가 허용되는 격리 수준이 `READ UNCOMMITTED`인 것입니다. 더티 리드 현상은 데이터가 나타났다가 사라졌다 하는 현상을 초래하므로 `READ UNCOMMITTED`는 정합성에 문제가 많은 격리 수준입니다.

 

✅ READ COMMITTED

`READ COMMITTED` 격리 수준에서는 다음 시퀀스 다이어그램과 같이 어떤 트랜잭션에서 데이터를 변경했더라도 `commit`된 데이터만 다른 트랜잭션에서 조회할 수 있습니다.

READ COMMITTED

사용자 B는 `select` 쿼리 결과를 테이블이 아니라 언두 영역에 백업된 레코드에서 가져옵니다. `READ COMMITTED` 격리 수준에서는 어떤 트랜잭션에서 변경한 내용이 커밋되기 전까지는 다른 트랜잭션에서 그러한 변경 내역을 조회할 수 없기 때문입니다. 사용자 A가 변경된 내용을 커밋하면 그때부터는 다른 트랜잭션에서도 백업된 언두 레코드가 아니라 새롭게 변경된 테이블에서 값을 읽게 됩니다.

 

👆 `READ COMMITTED` 격리 수준에서는 `NON-REPEATABLE READ`라는 부정합 문제가 발생할 수 있습니다. 다음은 그 예시입니다.

NON-REPEATABLE READ

사용자 B는 똑같은 두 번의 똑같은 `SELECT` 쿼리를 실행했을 때 결과가 각각 다릅니다. 정상적인 상황처럼 보이지만, 하나의 트랜잭션 내에서 똑같은 `SELECT` 쿼리를 실행했을 때는 항상 같은 결과를 가져와야 한다는 `REPEATABLE READ` 정합성에 어긋나게 됩니다.

 

이러한 부정합 현상은 일반적인 웹 서비스에서는 크게 문제 되지 않을 수 있지만 하나의 트랜잭션에서 동일 데이터를 여러 번 읽고 변경하는 작업이 금전적인 처리와 연결되면 문제가 될 수 있습니다.

예를 들어 트랜잭션 A에서 입금과 출금 처리가 계속 진행될 때 트랜잭션 B에서 오늘 입금된 금액의 총합을 조회한다고 가정했을 때, `REPEATABLE READ`가 보장되지 않기 때문에 트랜잭션 B의 총합을 계산하는 `SELECT` 쿼리는 실행될 때마다 다른 결과를 가져오게 될 것입니다.

 

여기서 중요한 것은 사용 중인 트랜잭션의 격리 수준에 의해 실행되는 SQL 문장이 어떤 결과를 가져오게 되는지를 정확히 예측할 수 있어야 한다는 것입니다. 그리고 이를 위해서는 각 트랜잭션의 격리 수준이 어떻게 작동하는지 아는 것이 중요합니다.

💡 트랜잭션 내에서 실행되는 `SELECT` 문장과 트랜잭션 없이 실행되는 `SELECT` 문장에는 차이가 있습니다.
`READ COMMITTED` 격리 수준에서는 트랜잭션 내, 외부에서 실행되는 `SELECT` 문장의 차이가 거의 없습니다. 하지만 다음에 설명할 `REPEATABLE READ` 격리 수준에서는 기본적으로 `SELECT` 문장도 트랜잭션 범위 내에서만 작동합니다. 
즉, `START TRANSACTION`(or BEGIN) 명령으로 트랜잭션을 시작한 상태에서 동일한 쿼리를 반복해서 실행하면 항상 동일한 결과를 보게 됩니다.(다른 트랜잭션에서 그 데이터를 변경 후 커밋한다고 해도)
별로 중요하지 않아 보이지만 이런 문제로 데이터 정합성이 깨지고 애플리케이션에 버그가 발생하면 찾아내기가 쉽지 않을 수 있습니다.

 

✅ REPEATABLE READ

`REPEATABLE READ` 격리 수준은 MySQL의 InnoDB 스토리지 엔진에서 기본으로 사용되는 격리 수준입니다. InnoDB 스토리지 엔진은 트랜잭션이 롤백될 가능성에 대비해 변경되기 전 레코드를 언두(Undo) 공간에 백업해 두고 실제 레코드 값을 변경합니다. 이러한 변경 방식을 MVCC라고 하며, `REPEATABLE READ`는 MVCC를 위해 언두 영역에 백업된 이전 데이터를 이용해 동일 트랜잭션 내에서는 동일한 결과를 보여줄 수 있도록 보장합니다.

💡 사실 `READ COMMITTED`도 MVCC를 이용해 커밋되기 전의 데이터를 보여줍니다. `REPEATABLE READ`와 차이는 언두 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 이전 버전까지 찾아 들어가냐 하느냐에 있습니다.

모든 InnoDB의 트랜잭션은 고유한 트랜잭션 번호(순차적으로 증가하는 값)를 가지며, 언두 영역에 백업된 모든 레코드에는 변경을 발생시킨 트랜잭션의 번호가 포함되어 있습니다. 그리고 언두 영역의 백업된 데이터는 InnoDB 스토리지 엔진이 불필요하다고 판단되는 시점에 주기적으로 삭제합니다.

 

`REPEATABLE READ` 격리 수준에서는 MVCC를 보장하기 위해 실행 중인 트랜잭션 가운데 가장 오래된 트랜잭션 번호보다 트랜잭션 번호가 앞선 언두 영역의 데이터는 삭제할 수가 없습니다. 그렇다고 가장 오래된 트랜잭션 번호 이전의 트랜잭션에 의해 변경된 모든 언두 데이터가 필요한 것은 아니며, 더 정확히는 특정 트랜잭션 번호의 구간 내에서 백업된 언두 데이터가 보존되어야 합니다.

 

`REPEATABLE READ` 격리 수준은 다음 시퀀스 다이어그램과 같이 작동합니다.

REPEATABLE READ

사용자 A가 변경 및 커밋을 했지만, 사용자 B는 사용자 A의 변경 전후 각각 한 번씩 `SELECT`했을 때 항상 같은 결과를 받습니다. 사용자 B가 `TX-ID=10`의 트랜잭션 번호를 부여받았고, 그때부터 사용자 B의 10번 트랜잭션 안에서 실행되는 모든 `SELECT` 쿼리는 트랜잭션 번호가 10보다 작은 트랜잭션 번호에서 변경한 것만 보게 되는 것입니다.

💡 언두 영역에 백업된 데이터는 하나의 레코드에 대해 얼마든지 존재할 수 있습니다. 한 사용자가 트랜잭션 시작 후 장시간 트랜잭션을 종료하지 않으면 언두 영역이 백업된 데이터로 무한정 커질 수도 있습니다. 이렇게 언두에 백업된 레코드가 많아지면 MySQL 서버의 처리 성능이 떨어질 수 있습니다.

 

`REPEATABLE READ` 격리 수준에서도 부정합이 발생할 수 있습니다. 다음은 그 예시로, 사용자 A가 `INSERT`를 실행하는 도중에 사용자 B가 `SELECT ... FOR UPDATE` 쿼리로 테이블을 조회했을 때 어떻게 되는지 보여줍니다.

PHANTOM READ

사용자 B는 `REPEATABLE READ`에서와 같이 두 번의 `SELECT` 쿼리 결과는 똑같아야 할 것입니다. 하지만 쿼리 결과는 서로 다른데, 이렇게 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다 안 보였다 하는 현상을 `PHANTOM READ`라고 합니다.

`SELECT ... FOR UPDATE` 또는 `SELECT ... LOCK IN SHARE MODE`로 조회되는 레코드는 언두 영역의 변경 전 데이터를 가져오는 것이 아니라 현재 레코드의 값을 가져오게 되는 것입니다.

 

✅ SERIALIZABLE

`SERIALIZABLE` 격리 수준은 가장 단순하면서 동시에 가장 엄격한 격리 수준입니다. 그만큼 동시 처리 성능도 다른 트랜잭션 격리 수준보다 떨어집니다. 

InnoDB 테이블에서 기본적으로 순수한 `SELECT` 작업은 아무런 레코드 잠금도 설정하지 않고 실행되는데 반해, `SERIALIZABLE` 격리 수준에서는 읽기 작업도 공유 잠금(읽기 잠금)을 획득해야만 하며, 동시에 다른 트랜잭션은 그러한 레코드를 변경하지 못하게 됩니다. 즉, 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없습니다.

 

`SERIALIZABLE` 격리 수준에서는 일반적인 DBMS에서 일어나는 `PHANTOM READ` 문제가 발생하지 않습니다. 하지만 InnoDB 스토리지 엔진에서는 갭 락과 넥스키 락 덕분에 `REPEATABLE READ` 격리 수준에서도 이미 `PHANTOM READ`가 발생하지 않기 때문에 굳이 `SERIALIZABLE`을 사용할 필요가 없을 수 있습니다.

 

✔️ 정리

  DIRTY READ NON-REPEATABLE READ SERIALIZABLE
READ UNCOMMITTED ✅ 발생 ✅ 발생 ✅ 발생
READ COMMITTED ❌ 없음 ✅ 발생 ✅ 발생
REPEATABLE READ ❌ 없음 ❌ 없음 ✅ 발생 (InnoDB는 ❌)
SERIALIZABE ❌ 없음 ❌ 없음 ❌ 없음

 

🔖 참고

Real MySQL 8.0 1권

'SQL' 카테고리의 다른 글

[SQL] 실행 계획 분석해 성능 개선하기(MariaDB)  (0) 2025.12.23
'SQL' 카테고리의 다른 글
  • [SQL] 실행 계획 분석해 성능 개선하기(MariaDB)
이런개발
이런개발
geun-00의 흔적 보관소
  • 이런개발
    내일이 기대되는 오늘
    이런개발
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • 백엔드 면접
      • SQL N
        • SUM, MAX, MIN
        • SELECT
        • GROUP BY
        • JOIN
      • Spring
      • JPA
      • 트러블슈팅
      • Infra
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    스프링
    오블완
    백엔드 면접
    토스 페이먼츠
    티스토리챌린지
    자바
    데브코스
    JPA
    raid
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
이런개발
[SQL] MySQL 격리 수준
상단으로

티스토리툴바