티스토리 뷰
개요
트랜잭션 격리 수준이란 여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.
격리 수준은 크게 4가지로 READ UNCOMMITED, READ COMMITED, REPEATABLE READ, SERIALIZABLE이 있다. 순서대로 뒤로 갈수록 각 트랜잭션의 데이터 격리(고립)정도 높아지며, 동시 처리 성능도 떨어지는 것이 일반적이다.
| 격리 수준\부정합 문제 | Dirty Read | Non-Repeatable Read | Phantom Read |
| READ UNCOMMITED | 발생 | 발생 | 발생 |
| READ COMMITED | 없음 | 발생 | 발생 |
| REPEATABLE READ | 없음 | 없음 | 발생(MySQL InnoDB 스토리지 엔진은 거의 발생하지 않음.) |
| SERIALIZABLE | 없음 | 없음 | 없음 |
해당 표는 각 격리 수준에서 발생할 수 있는 부정합 문제를 나타낸 것이다. 따라서 트랜잭션의 격리 수준을 선택할 때는 데이터 정합성과 성능(동시성 처리)간의 트레이드 오프를 고려하여 상황에 맞는 적절한 격리수준을 선택해야한다.
각 격리 수준과 발생할 수 있는 부정합 문제를 예시와 함께 알아보자. 특히, InnoDB의 기본 격리 수준인 REPEATABLE READ에서는 다른 상용 DBMS와 어떻게 다르게 동작하는지에 초점을 맞춰 분석해보겠다.
READ UNCOMMITTED
READ UNCOMMITTED 격리 수준에서는 트랜잭션의 변경 사항이 COMMIT이나 ROLLBACK 여부와 상관없이 다른 트랜잭션에서 보일 수 있다. 즉, 아직 확정되지 않은(커밋되지 않은) 데이터도 다른 트랜잭션에서 읽을 수 있다는 의미다.

예를 들어, 사용자 A가 새로운 사원을 INSERT하고 아직 COMMIT을 하지 않은 상태라고 가정하자. 하지만 사용자 B는 이미 이 새로운 사원의 데이터를 조회할 수 있다.
문제는 사용자 A가 트랜잭션 도중 문제로 인해 ROLLBACK을 수행할 경우, 사용자 B가 조회했던 데이터는 실제로 존재하지 않게 된다. 그럼에도 불구하고 사용자 B는 이미 해당 데이터를 기반으로 로직을 수행했을 가능성이 있다.
이처럼 트랜잭션이 확정되지 않은 데이터를 다른 트랜잭션에서 읽을 수 있는 현상을 더티 리드(Dirty Read)라고 한다. 더티 리드는 데이터 정합성에 심각한 문제를 초래할 수 있으며, 개발자가 이를 신뢰할 수 없는 상태로 만들게 된다.
READ UNCOMMITTED는 RDBMS 표준에서 트랜잭션 격리 수준으로 인정되지 않을 정도로 데이터 정합성에 취약하다. 따라서, 일반적으로 READ COMMITTED 이상의 격리 수준을 사용할 것을 권장한다.
READ COMMITED
READ COMMITED 격리 수준은 오라클 DBMS에서 기본으로 사용되는 격리 수준이다.
이 격리 수준에서는 READ UNCOMMITTED에서 발생하던 더티 리드 현상이 발생하지 않는다. 그 이유는 트랜잭션에서 데이터 변경이 이루어졌더라도, 해당 변경이 커밋되지 않으면 다른 트랜잭션에서는 조회할 수 없기 때문이다. A 트랜잭션에서 데이터를 변경했더라도 커밋이 완료된 데이터만 B 트랜잭션에서 조회할 수 있다.

예를 들어, 사용자 A 트랜잭션이 emp_no 5001의 first_name을 'MinChan'으로 변경했다고 가정하자.
이때 변경된 데이터는 즉시 테이블에 반영되지만, 기존 데이터는 언두(UNDO) 영역에 백업된다. 그러나 트랜잭션이 아직 커밋되지 않은 상태이므로, 사용자 B 트랜잭션이 동일한 데이터를 조회하면 언두 로그에 저장된 이전 데이터를 참조하게 된다.
이로인해 커밋되지 않은 데이터를 읽을 수 없으므로 더티 리드 문제가 발생하지 않는다.
그러나 READ COMMITTED에서도 특정 부정합 문제가 발생할 수 있다.
하나의 트랜잭션 내에서 동일한 조회(SELECT)를 수행했을 때, 항상 같은 결과가 보장되지 않는 NON-REPEATABLE READ(비반복 읽기) 부정합이 발생할 수 있다.

예를 들어, 사용자 B 트랜잭션에서 first_name이 'MinChan'인 사용자를 검색했을 때, 해당하는 데이터가 없었다. 그런데 사용자 A 트랜잭션이 emp_no 5001 레코드의 first_name을 'MinChan'으로 변경하고 커밋하였다. 이후 동일한 사용자 B 트랜잭션에서 다시 동일한 조회를 수행했을 때, 이번에는 'MinChan'이 포함된 결과가 반환된다.
이처럼 같은 트랜잭션 내에서 동일한 조회를 수행했음에도 불구하고, 결과가 달라지는 현상을 NON-REPEATABLE READ 부정합이라고 한다.
다른 트랜잭션에서 입금과 출금이 계속될 때, 하나의 트랜잭션에서는 오늘 입금된 총 금액을 여러 번 조회하여 처리해야 하는 비지니스 로직이 있다고 가정해보자.
READ COMMITED 격리 수준에서는 REPEATABLE READ가 보장되지 않기 때문에 입금된 총 금액을 조회하는 트랜잭션 내에서는 계속 값이 바뀌게 되어 예측하지 못한 결과를 반환한다. 이는 데이터 정합성이 깨지고, 버그가 발생할 수도 있을 것이다.
따라서 이를 방지하기 위해서는 더 엄격한 격리 수준인 REPEATABLE READ 격리 수준을 사용해야한다.
REPEATABLE READ
MySQL의 InnoDB 스토리지 엔진에서 기본으로 사용되는 격리 수준이다.
이 격리 수준에서는 NON-REPEATABLE READ 부정합 문제가 발생하지 않는다. InnoDB는 트랜잭션이 ROLLBACK(롤백) 될 가능성에 대비해, 변경 전 레코드를 UNDO(언두) 공간에 백업한 후 실제 데이터를 변경한다. 이러한 변경 방식은 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)라 하며, 잠금을 사용하지 않고도 일관된 읽기(Consistent Read)를 제공하는 것이 목적이다.
MVCC는 하나의 레코드에 대해 여러 버전을 관리하며, 동일한 트랜잭션 내에서 일관된 데이터를 조회할 수 있도록 언두 영역의 백업 데이터를 참조한다. 또한, 레코드가 변경될 때, 이를 수행한 트랜잭션 번호가 포함되어 저장되며, 이 언두 영역의 백업 데이터는 InnoDB가 불필요하다고 판단하는 시점에 자동으로 삭제된다.
아래 예시를 통해 REPEATABLE READ 격리 수준에서 일관된 읽기(REPEATABLE READ)가 어떻게 보장되는지 살펴보자.

사용자 B 트랜잭션(TRX-ID: 10)에서 first_name이 'JungBin'인 레코드를 조회해 결과 1건을 조회하였다. 그리고 이때 사용자 A 트랜잭션(TRX-ID: 15)이 emp_no 5001번의 레코드의 first_name을 'MinChan'으로 업데이트 하였다. 이때 기존 값을 언두 영역에 백업하고, 실제 테이블에 변경된 값을 트랜잭션 번호와 함께 반영한다.
사용자 B 트랜잭션(TRX-ID: 10)에서는 동일한 결과를 다시 조회할 때, 자신의 트랜잭션 번호보다 작은 트랜잭션 번호에서 변경된 것만 보게된다. 따라서 언두 영역의 트랜잭션 아이디 6번 레코드를 동일하게 가져올 수 있게 된다.
이로써 NON-REPEATABLE READ의 부정합 문제를 방지할 수 있게 되었다. 그러나 또 하나의 부정합 문제가 발생할 수 있는데 팬텀 리드(Phantom Read)부정합 문제이다. 팬텀 리드는 동일한 트랜잭션 내에서 같은 조건으로 조회했을 때, 처음에는 없던 데이터가 나중에 추가되어 조회되는 현상을 말한다.
팬텀 리드 발생 예시를 살펴보자.

사용자 B 트랜잭션는 first_name LIKE 'J%'조건으로 1건의 레코드(JungBin)를 조회한다. 트랜잭션은 아직 진행중이다.
사용자 A 트랜잭션에서 emp_no 5001의 레코드의 first_name을 'MinChan'로 변경하고, 언두 영역에 백업한다. 그리고 first_name이 'JiMin'인 레코드를 새로 추가하고 커밋된다.
사용자 B 트랜잭션에서 다시 동일한 조회를 할 때, 새로 INSERT된 JiMin 레코드와 언두 영역의 JungBin 레코드를 결과로 반환하여 2개의 결과가 반환되는 팬텀리드가 발생하게 된다.
더 정확히 설명하자면 InnoDB의 MVCC는 기존 레코드의 변경(UPDATE, DELETE)에 대해서만 언두 로그를 활용하여 일관된 읽기를 제공하며, 새로운 레코드(INSERT)에 대해서는 MVCC가 적용되지 않는다. 새로운 레코드를 추가하는 것이므로 이전 데이터 버전이 존재하지 않기 때문이다. 이처럼 일관된 읽기에는 사용되지 않기 때문에 INSERT된 데이터도 조회되는 팬텀 리드가 발생 하는 것이다.
InnoDB 스토리지 엔진에서는 다른 상용 DBMS와 다른 특수한 기능으로 인해 팬텀 리드 부정합 문제를 REPEATABLE READ 트랜잭션 격리수준에서도 방지할 수 있게 설계되었다.
이는 갭 락(Gap Lock)과 넥스트 키 락(Next Key Lock)을 활용하여 새로운 레코드 삽입을 제어하는 방식을 사용한다.

사용자 B 트랜잭션에서 first_name이 J%인 레코드를 검색한다. 이때 범위 검색이므로 레코드와 레코드 사이에 새로운 INSERT가 되는 것을 방지하기 위해 레코드락+갭 락인 넥스트 키 락을 적용한다. 사용자 A 트랜잭션에서 새로운 레코드로 JiMin을 추가하려하지만 락으로 인해 대기가 발생한다. 때문에 사용자 B 트랜잭션에서 다시 조회하여도 동일한 결과로 조회할 수 있어 팬텀리드가 방지된다.
SERIALIZABLE
가장 엄격한 격리 수준이다. 그만큼 동시 처리 성능도 다른 트랜잭션 격리 수준보다 떨어진다. 이름 그대로 모든 트랜잭션을 순차적으로 처리한다.
InnoDB 테이블에서는 기본적으로 SELECT작업은 아무런 레코드 잠금없이 수행가능하다. 조회는 트랜잭션이 동시에 접근 가능하다는 것이다. 그러나 SERIALIZABLE 격리 수준에서는 공유 락(읽기 잠금)을 걸게된다.
동시에 어떠한 쓰기 작업도 수행할 수 없으니, 부정합 문제도 발생하지 않게된다. InnoDB 스토리지 엔진의 갭락 넥스트키 락 덕분에 PEPEATABLE READ 격리 수준에서 PHANTOM READ 문제까지 방지할 수 있으므로 극단적인 작업처리를 위한 것이 아니라면 사용할 일 없다.
참고 자료
- 백은빈, 이성욱, Real MySQL8.0(1), 2021.09.08 위키북스, 176p-183p
- RDS MySQL 과 Aurora MySQL 에서 Innodb purge 작업 최적화 하기
- [MySQL] 트랜잭션의 격리 수준(Isolation Level)에 대해 쉽고 완벽하게 이해하기, https://mangkyu.tistory.com/299
'Database' 카테고리의 다른 글
| InnoDB 스토리지 엔진 락에 대해(레코드 락, 갭 락, 넥스트 키 락, 자동증가 락) (0) | 2025.02.27 |
|---|---|
| 레디스가 원자성을 보장하는 방법 (0) | 2025.02.04 |