[DB] 트랜잭션 격리수준

calm0_0·2023년 10월 31일
0

Database

목록 보기
5/6

트랜잭션 격리 수준(Transaction Isolation Level)


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

격리 수준은 크게 4가지로 나뉜다.

  • READ UNCOMMITED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

순서대로 뒤로 갈 수록 각 트랜잭션 간 데이터 격리 정도가 높아지며, 동시 처리 성능도 떨어진다.


READ UNCOMMITTED


READ UNCOMMITTED 에서는 트랜잭션에서의 변경 내용이 커밋이나 롤백 여부에 상관없이 다른 트랜잭션에 보여진다.

다음과 같이 사용자 A의 트랜잭션 작업이 완료되지 않았는데, 사용자 B가 해당 데이터에 접근하고 있다.

그런데 사용자 A는 커밋이 아닌 롤백을 수행했고, id = 51 의 데이터로 작업 중이던 사용자 B가 다시 데이터를 조회했을 때는 결과가 존재하지 않게 되었다.

이렇게 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상을 더티 리드(Dirty read) 라고 한다. 더티 리드는 데이터가 나타났다가 사라졌다 하는 현상을 초래하므로 시스템에 상당한 혼란을 준다.

때문에 RDMS 표준에서는 트랜잭션의 격리 수준으로 인정하지 않을 정도로 정합성에 문제가 많으며, 최소한 READ COMMITTED 이상의 격리 수준을 사용할 것을 권장한다.


READ COMMITTED


READ COMMITTED 는 커밋된 데이터만 조회할 수 있는 격리 수준이다.

데이터를 변경했더라도 커밋이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있기 때문에 더티 리드 같은 현상은 발생하지 않는다.

사용자 A가 트랜잭션을 시작하여 데이터를 변경하고 아직 커밋되지 않은 상태이다. 테이블은 먼저 갱신되고 언두 로그로 변경 전의 데이터가 백업된다.

이때 사용자 B가 해당 데이터를 조회하면 READ COMMITTED 에서는 커밋된 데이터만 조회할 수 있으므로, 언두 로그에서 변경 전의 데이터를 찾아서 반환한다.

사용자 A가 트랜잭션을 커밋하면 그때부터 다른 트랜잭션에서도 새롭게 변경된 값을 참조할 수 있다.

하지만 Non-Repeatable Read(반복 읽기 불가능) 이라는 부정합 문제가 발생한다.

사용자 B가 트랜잭션을 시작하고 name ="Minkyu" 인 레코드를 조회했다. 현재 테이블에는 조건에 맞는 레코드가 없으므로 결과 데이터가 없다.

그러다가 사용자 A가 UPDATE 문을 수행하여 해당 조건을 만족하는 레코드가 생겼고, A는 커밋까지 완료했다.

이때 사용자 B가 다시 동일한 조건으로 레코드를 조회하면, 이번에는 결과가 나오게 된다. 이렇게 반복 읽기를 수행할 때 다른 트랜잭션의 커밋 여부에 따라 조회 결과가 달라지는 현상을 Non-Repeatable Read 라고 한다.

이러한 현상은 일반적으로는 크게 문제가 되지 않을 수 있지만 하나의 트랜잭션에서 동일 데이터를 여러 번 읽고 변경하는 작업이 금전적인 처리와 연결되면 문제가 될 수도 있다.
예를 들어, 어떤 트랜잭션에서는 오늘 입금된 총합을 계산하고 있는데, 다른 트랜잭션에서 계속해서 입금 내역을 커밋하는 상황이라면, READ COMMITTED 에서는 같은 트랜잭션일지라도 조회할 때마다 입금된 내역이 달라지므로 문제가 생길 수 있다.
따라서 격리 수준이 어떻게 동작하는지, 어떠한 결과가 나오는지 예측할 수 있어야 한다.


REPEATABLE READ


REPEATABLE READ 는 MySQL의 InnoDB 스토리지 엔진에서 기본적으로 사용되는 격리 수준이다. 이 격리 수준에서는 NON-REPEATABLE READ 부정합이 발생하지 않지만, 새로운 레코드가 추가되는 경우에 부정합이 생길 수 있다.

(InnoDB 스토리지 엔진을 사용할 경우 PHANTOM READ 부정합이 발생하지 않는다.)

REPEATABLE READ 는 언두(Undo) 영역에 백업된 이전 데이터를 통해 트랜잭션 내에서는 동일한 결과를 보여 주도록 보장한다. READ COMMITTED 에서도 언두 영역에 백업된 이전 데이터를 보여 주지만, 두 격리 수준에는 언두 영역을 활용하는 방식이 다르다. REPEATABLE READ 격리 수준은 ‘언두 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 버전을 보여 주냐’에 차이가 있다.

쉽게 말하면 언두 영역에 백업된 모든 데이터에는 변경을 발생한 트랜잭션의 번호가 포함되어 있는데, REPEATABLE READ 격리 수준에서는 실행 중인 트랜잭션보다 작은 트랜잭션에서 변경한 데이터만 보게 하여 NON-REPEATABLE READ 문제를 해결한다.

사용자 B의 트랜잭션은(10) 사용자 A의 트랜잭션(12)이 시작하기 전에 이미 시작되었다. 이때 REPEATABLE READ 에서는 트랜잭션 번호를 참고하여 자신보다 먼저 실행된 트랜잭션의 데이터만을 조회한다. 때문에 동일한 결과를 반환하고 있다.

앞서 언급했듯이 REPEATABLE READ 는 새로운 레코드의 추가까지는 막지 않는다. 따라서 SELECT 로 조회한 경우 트랜잭션이 끝나기 전에 다른 트랜잭션에 의해 추가된 레코드가 발견될 수 있는데, 이를 유령 읽기(Phantom Read) 라고 한다. 하지만 MVCC 덕분에 일반적인 조회에서 Phantom Read 는 발생하지 않는다. 왜냐하면 자신보다 나중에 실행된 트랜잭션이 추가한 레코드는 무시하면 되기 때문이다.

그렇다면 Phantom Read 가 발생하는 상황은 언제일까? 바로 잠금이 사용되는 경우이다.

SELECT … FOR UPDATE 구문은 베타적 잠금(비관적 잠금, 쓰기 잠금)을 거는 것이다. 읽기 잠검을 걸려면 SELECT FOR SHARE 구문을 사용한다. 잠금은 트랜잭션이 커밋 또는 롤백될 때 해제된다.

사용자 B가 데이터를 조회하는데 SELECT FOR UPDATE를 이용해 쓰기 잠금을 걸었다. 그리고 사용자 A가 새로운 레코드를 INSERT 하고 커밋했다. id = 50인 레코드만 잠금이 걸린 상태이고, 사용자 A의 요청은 잠금 없이 즉시 실행된다.

이때 사용자 B가 동일한 쓰기 잠금 쿼리로 다시 한번 데이터를 조회하면, 이번에는 2건의 데이터가 조회된다. 동일한 트랜잭션 내에서도 새로운 레코드가 추가되는 경우에 조회 결과가 달라지는데, 이렇듯 다른 트랜젹션에서 수행한 작업에 의해 레코드가 안보였다 보였다 하는 현상을 Phantom Read(유령 읽기)라고 한다.

SELECT FOR UPDATE 쿼리는 SELECT 하는 레코드에 쓰기 잠금을 걸어야 하는데, 언두 레코드는 잠글 수 없다. 따라서 SELECT FOR UPDATE나 SELECT FOR SHARE로 레코드를 조회하는 경우에는 언두 영역의 데이터가 아니라 테이블의 레코드를 가져오게 되고, 이로 인해 Phaontom Read 가 발생하는 것이다.


SERIALIZABLE


SERIALIZABLE 은 가장 엄격한 격리 수준으로 트랜잭션을 순차적으로 진행시킨다.

읽기 작업도 공유 장금(읽기 잠금)을 획득해야만 하며, 여러 트랜잭션이 동일한 레코드에 동시에 접근할 수 없다.

때문에 어떠한 데이터 부정합 문제도 발생하지 않지만 트랜잭션이 순차적으로 처리되어야 하므로 동시 처리 성능이 매우 떨어진다.

가장 안전하지만 가장 성능이 떨어지므로 극단적으로 안전한 작업이 필요한 경우가 아니라면 사용하지 않는다.


요약




Reference
https://mangkyu.tistory.com/299
https://steady-coding.tistory.com/562
Real MySQL 8.0 (1권)

profile
공부한 내용들을 정리하는 블로그

0개의 댓글