지난 글에 이어서 이번 글에서는 Transaction Isolation Level(트랜잭션 격리 수준)에 대해 알아보겠습니다.
트랜잭션 격리 수준은 동시에 여러 트랜잭션이 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것입니다.
그렇다면, 트랜잭션 격리 수준 개념은 왜 등장했을까요??
전 글에서 다룬 트랜잭션의 ACID(원자성, 일관성, 고립성, 지속성)특성을 모두 다 지키려고 노력하다보면 동시성에 대한 성능이 저하됩니다. 따라서, 트랜잭션 격리 수준을 나누어 ACID 특성을 너무 타이트하게 지키지 않게끔 만들어줍니다.
많은 데이터베이스 어플리케이션들은 높은 트랜잭션 격리 수준을 사용하지 않으려고 합니다.
왜냐하면,
Deadlock
이 발생할 확률이 높아집니다.추가로, DBMS는 MVCC(Multiversion Concurrency Control)을 구현하거나 Lock을 사용(lock-based concurrency control)해야 합니다.
동시성에 대한 성능이 떨어지는 이유는 트랜잭션이 ACID 특성을 모두 지키고 있다면 각각의 원자성과 고립성을 지켜야하므로 한 트랜잭션이 데이터 X를 점유하고 있다면 그 외의 모든 트랜잭션은 전부 대기를 해야하기 때문입니다.
트랜잭션 격리 수준은 크게 4가지입니다.
Read Uncommitted
는 commit
이나 rollback
에 상관없이 트랜잭션의 데이터 변경 내용을 다른 트랜잭션이 읽는 것을 허용하는 트랜잭션 격리 수준입니다.
따라서, Dirty Read
가 발생합니다.
Dirty Read
는 트랜잭션의 작업이 완료되지 않았는데도 다른 트랜잭션에서 해당 데이터를 읽는 현상을 말합니다.
Read Uncommitted
는 데이터 정합성에 문제가 생기는 격리 수준입니다.
따라서, 데이터 정합성이 중요하다면 해당 격리 수준은 사용하지 말아야 합니다.
아래 예시처럼 Transaction A
에서의 데이터 변경이 commit되지 않았는데 Transaction B
에서 변경된 데이터를 조회해서 사용 가능합니다.
Read Committed
는 트랜잭션이 commit
, rollback
으로 완료되면 다른 트랜잭션에서 조회가 가능한 트랜잭션 격리 수준입니다.
Dirty Read
가 발생하지 않으며, Undo
영역의 백업된 레코드에서 값을 가져옵니다.
많은 RDB 서비스에서 가장 많이 사용하는 트랜잭션 격리 수준입니다.
하지만, Non-Repeatable Read
가 발생합니다.
Non-Repeatable Read
는 한 트랜잭션 안에서 똑같은 SELECT 쿼리(read
)를 실행했을 때 항상 같은 결과를 가져오지 않는 현상입니다. Lock을 통해 동시성을 제어하는 방식(lock-based concurrency control)에서는 SELECT 쿼리 실행 시에 read lock
을 획득하지 않거나, 획득한 lock이 SELECT 쿼리를 실행하자마자 해제될 때 발생합니다.
Non-Repeatable Read
를 예방하는 방법으로는 두 가지 방법이 존재합니다.
1. Transaction A가 commit
이나 rollback
이 될 때까지 Transaction B의 실행을 지연하는 것입니다.
2. Multiversion Concurrency Control(MVCC)을 사용합니다.
Non-Repeatable Read
의 데이터 부정합 문제는 일반적인 서비스에서는 큰 문제가 되지 않지만, 금전을 다루는 서비스에서 특히 큰 문제가 발생할 수 있습니다.
👉 Undo
영역은 트랜잭션의 격리 수준에 사용될 뿐 아니라 트랜잭션 rollback
이 발생했을 때 복구에도 사용됩니다.
아래 예시처럼 동일한 SELECT 쿼리에 다른 결과를 가져오는 경우가 발생합니다.
Repeatable Read
는 트랜잭션 범위 내에서 조회한 내용이 항상 동일함을 보장하는 트랜잭션 격리 수준입니다. 또한, MySQL의 InnoDB에서 기본으로 사용하는 트랜잭션 격리 수준입니다. 트랜잭션은 고유한 번호를 가지며 Undo
영역에 백업된 모든 레코드에는 변경을 발생시킨 트랜잭션의 번호가 포함되어 있습니다. 하나의 트랜잭션 안에서 일어나는 모든 SELECT 쿼리는 자신의 트랜잭션 번호보다 작은 트랜잭션 번호에서 변경한 사항들만 볼 수 있습니다. 이러한 방식을 MVCC라고 합니다.
MVCC(Multiversion Concurrency Control)는 다중 버젼 동시성 제어의 약자로 DBMS에서 서로 다른 세션이 동일한 데이터에 접근했을 때 각 세션마다 스냅샷 이미지를 보장해주는 매커니즘입니다.
RDBMS에서 동시성을 높이기 위해 등장한 기술입니다.
MVCC의 가장 큰 목적은 Lock을 사용하지 않는 일관된 읽기를 제공하기 위함입니다.
또한, 데이터의 여러 버전을 유지함으로써, 다수의 트랜잭션이 동시에 데이터에 접근할 수 있어 동시성을 크게 향상시켜줍니다.
하지만, 추가적인 저장공간이 필요하며 오래된 데이터 버전을 정리하는 과정이 필요합니다.
Undo
영역에는 하나의 레코드에 대한 여러 개의 백업이 존재할 수 있습니다. DBMS는 백업된 데이터가 불필요하다고 판단되는 시점에 주기적으로 삭제합니다.
하지만 이런 Repeatable Read
에서도 Phantom Read
라는 데이터 부정합 문제가 발생할 수 있습니다.
Phantom Read
는 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상입니다. Non-Repeatable Read
의 한 가지 경우이며 Transaction A가 범위를 조회하는 SELECT...WHERE 쿼리를 반복적으로 사용할 때, 그 쿼리들 사이에서 Transaction B가 해당 WHERE절을 만족하는 새로운 행을 생성했을 때 발생합니다.
아래 예시의 SELECT... FOR UPDATE 쿼리는 해당 레코드에 write lock
을 거는데, Undo
영역에는 Lock을 걸 수 없기 때문에 현재 레코드의 값을 가져오게 됩니다.
여담으로, InnoDB는 갭 락과 넥스트키 락 덕분에 Repeatable Read
에서도 Phantom Read
를 예방할 수 있습니다.
Serializable
는 모든 작업을 하나의 트랜잭션에서 처리하는 것과 같이 동작하는 가장 높은 트랜잭션 격리 수준입니다.
한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없습니다.
따라서, Phantom Read
문제가 발생하지 않습니다. 하지만, Deadlock
에 걸릴 가능성이 존재하고 동시성 처리 성능이 저하되기 때문에 신중하게 사용해야 합니다.
레벨 | Dirty Read | Non-Repeatable Read | Phantom Read |
---|---|---|---|
Read Uncommitted | 가능 | 가능 | 가능 |
Read Committed | 불가능 | 가능 | 가능 |
Repeatable Read | 불가능 | 불가능 | 가능 |
Serializable | 불가능 | 불가능 | 불가능 |
Transaction Isolation Level(트랜잭션 격리 수준)에 대해 공부하며 기록한 글입니다. 잘못된 부분이나 오타가 있다면 댓글로 알려주시면 감사하겠습니다. 긴 글 읽어주셔서 감사합니다 😊