본문 바로가기
개념 공부/DBMS

[DBMS] 트랜잭션 격리 레벨(isolation level)

by clean01 2024. 5. 18.

트랜잭션 격리 레벨이란?

트랜잭션 격리 레벨(isolation level)이란 동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 서로 고립되어 있는지를 나타내는 것입니다.

즉 다르게 말하면 ⭐️여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것입니다.

트랜잭션의 격리 수준에는 아래 4가지가 있습니다.

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

아래로 갈수록 트랜잭션 간의 고립 정도가 높아지고 성능이 떨어집니다.
일반적으로 READ COMMITED나 REPEATABLE READ 중 하나를 사용한다고 합니다.

(대표적으로 mysql, mariadb는 REPEATABLE READ, oracle은 READ COMMITTED를 디폴트로 쓰고 있습니다.)

이 글에서는 위의 네가지를 하나하나 살펴보며 각각의 격리 레벨이 어느 정도로 트랜잭션 간의 고립을 나타내는지 알아보려고 합니다.


READ UNCOMMITED

우선 격리 레벨이 가장 낮은 READ UNCOMMITED입니다.
READ UNCOMMITTED 격리 레벨에서는 어떤 트랜잭션의 변경 내용이 COMMIT, ROLLBACK 되었는지와 상관없이 다른 트랜잭션에게 보여집니다. (즉, 커밋 전인데도 다른 트랜잭션이 변경 사항을 볼 수가 있습니다.)

이 레벨에서는 아래와 같이 동작했을 때 문제가 생길 수 있습니다.

🚨Ex)

  1. 1번 트랜잭션이 10번 사원의 나이를 27->28로 변경.
  2. 아직 1번 트랜잭션이 커밋되지 않고 다른 쿼리문을 실행.
  3. 2번 트랜잭션이 시작되었고, 10번 회원의 나이를 조회.
  4. 28살이 조회. (이를 더티 리드(Dirty Read)라고 합니다.)
  5. 1번 트랜잭션에서 문제가 발생해 ROLLBACK.
  6. 2번 트랜잭션은 10번 사원이 28살이라고 생각하고 나머지 로직을 수행.

이런식으로 데이터 부정합 문제가 자주 발생할 수 있으므로 RDMS 표준에서 격리 수준으로 인정하지도 않는다고 합니다.


READ COMMITTED

READ COMMITTEDREAD UNCOMMITTED 다음으로 격리 수준이 낮은 레벨입니다. 이 레벨에서는 어떤 트랜잭션의 변경 내용이 commit 되어야만 다른 트랜잭션에서 조회할 수 있습니다.
오라클 DBMS에서 기본으로 사용하고 있고, 온라인 서비스에서 가장 많이 선택되는 격리 수준이라고 합니다.

📌 Ex) READ COMMITTED 동작 예시
사용자 A, B가 있다고 할 때.

  1. 사용자 A가 트랜잭션을 시작하여 10번 직원의 나이를 28->27로 변경하였다.
    READ COMMITTED에서는 우선 테이블을 변경한 후, undo log에 변경 전의 데이터가 백업되게 된다.
  2. 사용자 B가 10번 직원의 나이 데이터를 조회하려고 하면 READ COMMITTED에서는 커밋된 변경사항만 조회할 수 있으므로 undo log에서 변경 전의 데이터를 찾아 반환하게 된다.
  3. 사용자 A가 트랜잭션을 커밋하면 다른 트랜잭션에서도 변경된 데이터를 참조할 수 있다.

이 레벨에서는 Phantom Read, Non-Repeateable Read(반복 읽기 불가능), 문제가 발생할 수 있습니다.

🚨 Ex) 반복 읽기 불가능 발생 예시

  1. 원래 테이블에서 10번 직원의 이름이 "clean"이었다.
  2. 사용자 A가 트랜잭션을 시작하고 10번 직원의 이름을 "sejeong"으로 변경하였다. 10번 직원의 기존 이름 "clean"은 언두로그에 저장된다.
  3. 사용자 B가 새로운 트랜잭션을 시작하고 10번 직원의 이름을 조회하였다. 아직 사용자 A의 트랜잭션이 커밋되지 않았으므로, 언두 로그에서 "clean"이라는 값을 읽어온다.
  4. 사용자 A의 트랜잭션이 커밋되었다. (이제 다른 트랜잭션에서도 "sejeong"이라는 변경된 값을 볼 수 있다.)
  5. 사용자 B의 트랜잭션에서 다시 10번 직원의 이름을 조회하였고, "sejeong"이라는 값을 읽었다. 같은 트랜잭션에서 같은 데이터를 첫번째 조회했을 때의 값은 "clean"인데, 두번째 조회했을 때의 값은 "sejeong"이 되었다. -> 일관성 X. 데이터 부정합

READ COMMITTED에서 반복 읽기를 수행하면 다른 트랜잭션의 커밋 여부에 따라 조회 결과가 달라질 수 있습니다.


REPEATABLE READ

REPEATABLE READ 격리 수준은 간단히 말해 트랜잭션이 시작하기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리 수준입니다.
트랜잭션의 번호를 비교하여 커밋된 트랜잭션 중에서 자신보다 트랜잭션 번호가 작은 트랜잭션이 변경한 내용만 조회할 수 있습니다. 만약 테이블에 자신보다 이후에 실행된 트랜잭션의 데이터가 존재한다면 언두 로그를 참고하여 데이터를 조회합니다.
(모든 InnoDB의 트랜잭션은 고유한 트랜잭션 번호(순차적을 증가)를 가지고 있으며, 언두 영역에 백업된 모든 레코드는 변경을 발생시킨 트랜잭션의 번호가 포함되어 있기 때문에 가능합니다.)
Mysql에서 기본 격리 수준으로 사용하고 있고, 이 격리 수준에서는 앞에서 보았던 반복 읽기 불가 문제가 발생하지 않습니다.

하지만 REPEATABLE READ레벨에서는 다른 트랜잭션에서 데이터를 추가/삭제하고, 내가 쓰기잠금을 걸고 데이터를 읽어올 때 Phantom Read라는 데이터 부정합이 발생할 수 있습니다.
Phantom Read란 다른 트랜잭션에서 수행한 작업에 의해 어떤 레코드가 보였다 안보였다하는 상황을 의미한다.

🚨 Ex) Phantom Read가 발생하는 상황

  1. 사용자 B가 트랜잭션(trx-id=10) 시작.
  2. 트랜잭션 10에서 SELECT .. WHERE emp_no >= 50000 FOR UPDATE 쿼리 실행. 결과는 'JuBal' 하나 리턴
  3. 사용자 A가 트랜잭션(trx-id=12) 시작.
  4. 트랜잭션 12에서 INSERT INTO employees VALUES (50001, 'Lara'); 쿼리를 실행
  5. 트랜잭션 12가 커밋됨
  6. 트랜잭션 10에서 아까와 동일한 SELECT .. WHERE emp_no >= 50000 FOR UPDATE 쿼리 실행. 결과가 ('JuBal', 'Lara') 2개 리턴 -> 부정합 발생

MVCC 덕에 일반적인 조회(아무런 락을 걸지 않는 그냥 SELECT)에서는 팬텀 리드가 발생하지 않습니다. 테이블에 나보다 나중에 시작한 트랜잭션이 변경한 내용이 있다면 무시하고 언두에서 읽어오기 때문입니다.

이런 팬텀 리드가 발생하는 경우는 바로 잠금을 걸 때 입니다. SELECT ... FOR UPDATE같은 구문은 잠금을 거는 읽기는 언두 영역이 아닌 테이블에서 값을 읽어오기 때문입니다. (언두 영역에는 락을 걸 수가 없음)

참고: Mysql의 갭락

mysql에서는 갭락 때문에 팬텀리드가 거의 발생하지 않습니다.
위의 예시 상황에서, emp_id >= 50000인 레코드를 SELECT .. FOR UPDATE로 조회하면, emp_id=50000에는 레코드락을, 50001부터는 갭락으로 넥스트 키락을 걸어둡니다. 그리고 다른 트랜잭션이 emp_id가 50001 이상인 레코드를 추가하려고 하면, 쓰기 잠금을 걸었던 트랜잭션이 커밋 또는 롤백 될 때까지 기다리게 합니다. 기다리다가 대기 시간이 너무 길면 타임아웃이 나기도 합니다.
따라서 mysql은 위의 예시 같은 케이스에서 팬텀리드가 발생하지 않지만, trx10이 SELECT -> trx12가 INSERT -> trx12가 커밋 -> trx10이 SELECT .. FOR UPDATE 이런 순서로 실행된다면 팬텀리드가 발생합니다.


SERIALIZABLE

SERIALIZABLE은 가장 엄격한 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 실행하며, 한 트랜잭션이 같은 레코드에 동시 접속할 수 없습니다.
그렇기 때문에 어떤 데이터 부정합 문제도 발생하지 않습니다. 하지만 트랜잭션이 하나하나 순차적으로 실행되므로 동시 처리 성능이 매우 떨어집니다.

mysql에서 SELECT .. FOR UPDATE/SHARE 쿼리는 레코드에 각각 쓰기/읽기 잠금을 걸고 접근합니다. 하지만 다른 격리 레벨들에서는 그냥 SELECT는 아무 락도 걸지 않고 접근합니다.

하지만 SERIALIZABLE 레벨에서는 그냥 SELECT도 읽기 락을 걸고 레코드에 접근한다. 이렇게 읽기 잠금이 걸릴 레코드에는 다른 트랜잭션이 추가, 수정, 삭제 등을 할 수가 없습니다. -> 이것 때문에, Serializable 격리 레벨은 데드락에 걸릴 가능성도 존재합니다.

SERIALIZABLE은 안전하지만 성능이 떨어지는 방법이므로, 극단적으로 안전한 작업이 필요한 상황이 아니라면 잘 사용하지 않습니다.


트랜잭션 격리 레벨 별 발생할 수 있는 문제 요약


Reference