트랜잭션의 격리 수준

In 데이터베이스 시스템 by Choi Kyung-sikLeave a Comment

트랜잭션의 격리 수준을 이해하기 위한 배경 지식으로 트랜잭션, 고립성, 이상현상을 먼저 살펴본다.

트랜잭션의 개요

데이터베이스의 여러 연산을 묶어 놓은 것을 사용자의 관점에서는 하나의 단위로 여겨질 수 있다. 예를 들어, 다른 계좌로 돈을 이체하는 것은 사용자의 관점에서 하나의 작업이지만, 데이터베이스 시스템 내에서는 여러 연산을 결합한 것이다. 이처럼, 하나의 논리적인 작업 단위를 위한 연산들의 모음을 트랜잭션(transaction)이라 한다.

데이터의 무결성(integrity)을 보장하려면, 데이터베이스 시스템은 트랜잭션의 다음과 같은 특성을 관리해야 한다.

  • 원자성(atomicity): 트랜잭션의 모든 연산을 데이터베이스에 반영하든지, 아니면 전혀 반영하지 않아야 한다.
  • 일관성(consistency): 고립 상태에 있는 트랜잭션의 실행은 데이터베이스의 일관성을 유지해야 한다. 고립 상태란 동시에 실행하는 다른 트랜잭션이 없다는 것을 의미한다.
  • 고립성(isolation): 여러 개의 트랜잭션을 동시에 실행하더라도, 각 트랜잭션은 동시에 실행하고 있는 다른 트랜잭션을 인식하지 못해야 한다.
  • 지속성(durability): 트랜잭션이 성공적으로 끝나면, 그 트랜잭션이 데이터베이스 시스템에 수정한 내용은 시스템 장애가 발생하더라도 영속적이어야 한다.

위의 특성 중에 격리 수준과 관련한 고립성에 대해 좀 더 알아보자.

고립성

여러 개의 트랜잭션을 동시에 실행하면 성능이 좋아진다. 그러나 여러 트랜잭션의 연산이 원하지 않는 방향으로 겹쳐 불일치 상태(inconsistent state)가 발생할 수 있다. 동시에 실행하는 트랜잭션의 문제를 해결하는 방법은 트랜잭션을 순차적으로 실행하는 것이다. 트랜잭션의 고립성(isolation)은 여러 개의 트랜잭션을 동시에 실행하여도, 이 트랜잭션들을 순차적으로 실행한 것과 같은 상태가 되도록 하는 특성이다.

이상현상

이상현상(Phenomena)은 동시 실행하는 여러 트랜잭션이 상호 작용하면서 나타날 수 있는 문제를 의미한다. 읽기 이상현상으로 dirty read, non-repeatable read, phantom read 세 가지가 있다. 쓰기 이상현상으로 lost update가 있다. 이상현상을 예제로 보이기 위해 간단한 student 테이블을 사용할 것이다.

id name age
1 홍길동 20
2 임꺽정 25
Dirty Read

dirty read는 한 트랜잭션이 진행 중에, 다른 트랜잭션이 갱신은 하였으나 아직 commit하지 않은 데이터를 읽을 때 발생한다.

T1 T2
SELECT age FROM students WHERE id = 1;
UPDATE students SET age = 21 where id = 1;
SELECT age FROM students WHERE id = 1;
/* 21을 읽는다 */
ROLLBACK;

T2 트랜잭션에서 갱신은 하였으나 아직 커밋하지 않은 나이인 21을 T1 트랜잭션이 읽는다. T2에서 롤백하면 결국 T1은 이상한 값을 읽어 들인 셈이다.

Non-repeatable Read

non-repeatable read는 한 트랜잭션이 읽었던 데이터를 다시 읽을 때 데이터가 변경되는 것으로, 다시 읽기 전에 다른 트랜잭션이 데이터를 갱신하고 commit하기 때문에 발생한다. dirty read와 비슷해 보이지만 다른 트랜잭션에서 commit을 한다는 차이점이 있다.

T1 T2
SELECT age FROM students WHERE id = 1;
/* 20을 읽는다 */
UPDATE students SET age = 21 where id = 1;
COMMIT;
SELECT age FROM students WHERE id = 1;
/* 21을 읽는다 */
COMMIT;

T1 트랜잭션 내에서 age가 20과 21 두 개의 값을 갖는다. 의도하지 않은 결과가 발생할 수 있을 것이다.

Phantom Read

phantom read는 우리말로 ‘유령 읽기’ 정도로 바꿀 수 있겠다. 유령 읽기는 한 트랜잭션이 진행 중에, 다른 트랜잭션이 추가하거나 삭제행(row)의 데이터를 읽어 발생한다.

T1 T2
SELECT * FROM students WHERE age BETWEEN 10 AND 30;
INSERT INTO students(id, name, age) VALUES(3, ‘장길산’, 28);
COMMIT;
SELECT * FROM students WHERE age BETWEEN 10 AND 30;
COMMIT;

T1 트랜잭션에서 의도하지 않은 ‘장길산’ 이름을 가진 3번째 행이 나온다.

Lost Update(Dirty Write)

Lost Update는 ‘갱신 손실’로 대역할 수 있겠다. 갱신 손실은 한 트랜잭션이 데이터를 갱신한 후 다른 트랜잭션이 그 갱신한 값을 덮어쓸 때 발생한다. 다음의 예는 한 고객이 잔고가 1000원 계좌에서 500원을 인출하고 있는데 동업자가 400원을 동시에 출금하는 예이다.

Transaction 1 Transaction 2
read(balance)
balance = balance – 500
1000
500
read(balance)
balance = balance – 400
1000
400
write(balance)
500
write(balance)
600

잔고에 100원이 남아야 정상이지만 갱신 손실로 600원이 남았다. 위의 예에서 SQL 문을 사용하지 않고 수식으로 표현하였다. MariaDB, PostgreSQL에서 확인한 바로는 한 트랜잭션이 갱신을 한 상태에서 다른 트랜잭션이 동일 데이터에 갱신을 시도하면 대기를 하여야 한다. 즉, 데이터를 갱신한 트랜잭션이 commit이나 rollback을 하기 전에는 다른 트랜잭션은 동일 데이터를 갱신할 수 없다. 대부분의 DBMS 제품들은 모든 격리 수준에서 갱신 시 테이블이나 행(row)에 락(lock)을 걸어 lost update가 발생하지 않도록 하고 있다.

격리 수준(Isolation Level)

동시에 실행하는 트랜잭션들을 순차적인 실행 상태로 만드는 것은 성능에 좋지 않다. 성능과 일관성을 고려하여 트랜잭션에 격리 수준을 지정할 수 있다. SQL 표준은 네 종류의 트랜잭션 격리 수준을 정의하고 있다. 다음 항목에서 아래로 내려갈수록 일관성은 좋아지지만, 성능은 떨어진다.

  • read uncommited: 다른 트랜잭션에서 commit하지 않은 데이터를 읽을 수 있다. 데이터의 정확도가 중요하지 않으면서 트랜잭션의 수행 시간이 긴 경우에 지정하여 사용할 수 있다.
  • read commited: 다른 트랜잭션에서 commit한 데이터만을 읽는다.
  • repeatable read: read commited와 동일하게 다른 트랜잭션에서 commit한 데이터만을 읽는다. 추가로 트랜잭션 진행 중에 읽었던 데이터를 다시 읽을 때, 그 중간에 다른 트랜잭션이 그 데이터를 갱신할 수 없다.
  • serializable: 동시 진행하는 트랜잭션들이 순차적으로 실행한 것과 같은 결과가 나와야 한다. 직렬성(serializability)에 더하여 연쇄 복귀(rollback)도 없어야 한다.

SQL 표준은 serializable이 디폴트이다. 이 수준은 트랜잭션 간에 상호 작용이 전혀 없어야 하는 데 현실과는 맞지 않는다. 일반적인 데이터베이스 시스템은 read commitedrepeatable read를 디폴트로 사용한다. 또한, DBMS 제품마다 표준을 구현하는 방법은 다를 수 있다.

아래의 표는 각 격리 수준에서 발생할 수 있는 이상현상을 나타낸 것이다.

Dirty Read Non-repeatable Read Phantom Read
Read Uncommitted 발생할 수 있음 발생할 수 있음 발생할 수 있음
Read Committed 발생하지 않음 발생할 수 있음 발생할 수 있음
Repeatable Read 발생하지 않음 발생하지 않음 발생할 수 있음
Serializable 발생하지 않음 발생하지 않음 발생하지 않음

MySQL의 InnoDB엔진은 repeatable read 격리 수준에서 phantom read가 발생하지 않는다.

PostgreSQL도 마찬가지로 repeatable read 격리 수준에서 phantom read가 발생하지 않는다. 또한 트랜잭션을 read uncommitted로 설정은 할 수 있지만, dirty read는 발생하지 않는다.

참고 자료