데이터베이스의 트랜잭션 격리 수준 4가지
0. 트랜잭션의 격리 수준이 필요한 이유
트랜잭션의 격리 수준이 4가지나 존재하는 이유는 같은 데이터에 동시에 접근했을 때, 데이터의 일관성과 정합성, 무결성을 보장하기 위해서다. 현재 진행하고 있는 프로젝트의 특성을 고려하지 않고, 무턱대로 격리 수준을 높이면, 불필요하게 데이터베이스의 성능이 낮아지고, 격리 수준을 너무 낮추면, 많은 동시성 문제들(Dirty Read, Non-repeatable Read, Phantom Read 등등)이 발생한다.
이처럼 성능, 그리고 데이터의 일관성 간의 균형을 조정할 수 있도록 해주는 것이 트랜잭션의 4가지 격리 수준이다. 각 애플리케이션의 요구 사항에 따라 격리 수준을 선택해서 이 균형을 맞추는 것이 중요하다.
1. SERIALIZABLE
가장 엄격한 격리 수준이다. 여러 트랜잭션이 동시에 실행되더라도, 순차적으로 실행되도록 보장한다. 동시성 문제가 거의 발생하지 않는다. 순수 SELECT에도 락을 걸기 때문에, Dirty Read, Non-repeatable Read, Phantom Read 등의 문제들이 발생하지 않는다.
하지만, SERIALIZABLE에서도 데드락이 발생 가능하다
데드락이란, 여러 트랜잭션이 각자 필요한 자원을 차지하고 있는 상태에서, 서로의 자원을 기다리면서, 무한정 대기하는 상태를 말한다.
SERIALIZABLE에서의 데드락 발생 시나리오
- 트랜잭션 A가 자원 X에 대한 락을 획득한 상태에서 자원 Y에 접근하려고 시도
- 동시에, 트랜잭션 B는 자원 Y에 대한 락을 획득하고 자원 X에 접근하려고 시도
- 서로의 자원을 기다리게 되면서 순환 대기 상태에 빠짐
해결 방안
- 일정 시간이 지나면 타임아웃이 되도록 하여, 강제로 트랜잭션을 롤백한다.
- DB 시스템은 주기적으로 트랜잭션 간의 의존 관계를 검사하여 데드락 사이클을 탐지한다.
- 락을 걸 때 순서를 정하여, 트랜잭션들이 자원에 접근하는 순서를 동일하게 강제한다.
- … 등등
2. REPEATABLE READ
데이터베이스의 2번째 격리 수준이다. 이름이 REPEATABLE READ인 이유는, 트랜잭션 내에서 동일한 데이터를 반복해서 읽을 때, 항상 동일한 결과를 반환하도록 보장하기 때문이다.
REPEATABLE READ의 특성을 다음과 같다.
- 데이터 조회의 일관성
- 한 트랜잭션이 데이터를 읽고 있는 동안, 다른 트랜잭션이 해당 데이터 수정 및 삭제 불가
- Phantom Read 방지 가능(MySQL)
- SERIALIZABLE보다 성능이 좋아지지만, Lost Update(갱신 손실), Deadlock(데드락), 읽기 일관성 문제 등이 발생 가능
REPEATABLE READ의 시나리오 총정리(A와 B는 트랜잭션을 의미함)
(초기 상태(sample 테이블)는 id = 10 에는 30이 담겨있고, id = 11 은 null이라고 가정)
- A가 순수 SELECT만을 했을 때 (범위 조회 X)
- A : SELECT WHERE id = 10 (순수 SELECT) -> 30
- B : UPDATE SET 40 WHERE id = 10 수정
- A : SELECT WHERE id = 10 (순수 SELECT) -> 30 (트랜잭션 내 일관성 보장)
- A가 SELECT FOR UPDATE를 했을 때 (범위 조회 X)
- A : SELECT WHERE id = 10 FOR UPDATE -> 30
- B : UPDATE SET 40 WHERE id = 10 수정 -> A가 commit할 때까지 대기 상태에 걸림
- A가 순수 SELECT만을 했을 때 (범위 조회 O)
- A : SELECT WHERE id >= 10 (순수 SELECT) -> 30
- B : INSERT INTO SAMPLE(11, 20) -> id = 11 에 삽입
- A : SELECT WHERE id >= 10 (순수 SELECT) -> 30 (id = 11 은 조회되지 않음)
- A가 SELECT FOR UPDATE을 했을 때 (범위 조회 O) : Phantom Read 발생!!
- A : SELECT WHERE id >= 10 FOR UPDATE -> 30
- B : INSERT INTO SAMPLE(11, 20) -> id = 11 에 삽입
- A : SELECT WHERE id >= 10 FOR UPDATE -> 30, 20 (id = 11 이 조회됨)
이유 : 잠금있는 읽기는 데이터 조회가 언두 로그(undo log)가 아닌 원본 테이블에서 수행되기 때문
- (MySQL) A가 SELECT FOR UPDATE을 했을 때 (범위 조회 O) : Phantom Read 발생 X
- A : SELECT WHERE id >= 10 FOR UPDATE -> 30
- B : INSERT INTO SAMPLE(11, 20) -> id = 11 에 삽입(A가 commit 하기 전까지 대기 상태)
- A : SELECT WHERE id >= 10 FOR UPDATE -> 30
- A가 commit 하면 B는 그제서야 INSERT 수행
일반적인 DBMS에서는 A가 SELECT FOR UPDATE로 데이터를 조회하고, B가 새로운 요소를 삽입한 뒤, A가 읽으면 그 새로운 요소까지 조회가 된다. (Phantom Read) 하지만, MySQL에서는 A가 SELECT FOR UPDATE로 데이터를 조회한 경우, id >= 10 인 요소들에는 갭 락(Gap Lock)을 건다. 따라서 B가 INSERT를 수행하려고 하면 A가 commit할 때까지 대기 상태가 된다. Phantom Read를 방지하기 위한 MySQL의 일관성 유지 방식이다.
3. READ COMMITTED
3번째 격리 수준이다. 이름에서 알 수 있다시피, commit된 데이터만 조회할 수 있다.
REPEATABLE READ와 마찬가지로 undo 로그를 이용해서 하나의 트랜잭션이 커밋되기 전까지 그 전의 내용을 조회한다. 하지만 A 트랜잭션에서 이미 커밋이 완료된 경우, undo 로그를 사용하지 않기 때문에, B에서는 조회 결과가 달라질 수 있다.
READ COMMITED의 시나리오
(초기 상태(sample 테이블)는 id = 10 에는 30이라고 가정)
- A가 변경 내용을 commit하지 않은 상태로 B가 조회하는 시나리오
- A : SELECT WHERE id = 10 -> 30
- B : UPDATE SET 40 WHERE id = 10 수정
- A : SELECT WHERE id = 10 -> 30 (언두 로그를 보고 변경 전의 데이터를 조회)
- A가 변경 내용을 commit한 뒤에 B가 조회하는 시나리오
- Non-Repeatable Read(반복 읽기 불가능) 문제 발생!!
- A : SELECT WHERE id = 10 FOR UPDATE -> 30
- B : UPDATE SET 40 WHERE id = 10 수정 & commit
- A : SELECT WHERE id = 10 FOR UPDATE -> 40 (트랜잭션 내에서 조회 결과가 달라짐)
READ COMMITED에서는 commit된 내용을 조회할 수 있도록 허락하기 때문에, 같은 트랜잭션 내에서 조회 결과가 달라져서 데이터의 일관성을 떨어뜨린다.
4. READ UNCOMMITTED
커밋되지 않은 데이터도 접근할 수 있는 최악의 격리 수준이다.
다른 트랜잭션에서 롤백이 발생할 경우, 조회됐던 데이터가 사라지는 혼란(ex. Dirty Read)을 줄 수 있기 때문에, RDBMS 표준에서도 인정하지 않는 방식이다.