여러 사용자가 동시에 Database를 원활하게 사용하기 위해선, Database에서 Concurrent Sharing 기능을 제공해주어야 한다. 여러 명의 사용자를 동시에 처리하기 위해 아래의 두 가지 방법을 고려해 볼 수 있다.
① Interleaved Processing(동시성)
② Parallel Processing(병렬성)
아래의 그림에서 A와 B가 처리되는 방식은 Interleaved Processing에, C와 D가 처리되는 방식은 Parallel Processing에 해당한다.
병렬적인 처리는 실행시간을 획기적으로 단축시키지만, 여러가지 문제를 발생시키기도 한다. 동시성을 제어하지 않았을 때 생길 수 있는 문제점은 아래와 같다.
① Dirty Read
② Non-Repeatable Read
③ Phantom Read
Write가 수행되는 도중에 Read를 허용할 것인지에 대한 여부는 DBA의 design choice에 맡긴다. 만약 Read를 허용할 경우 이전의 Snapshot에 대해 Read 하는 것이 허용되는데, 이를 Shared Lock이라 한다. 반대로 Read를 불허하는 경우는 Exclusive Lock이라 하며, Read 작업이 원천적으로 금지된다.
① Lost Update
② Inconsistency
③ Cascading Rollback
위와 같은 문제를 해결하기 위해 Concurrency Control이 필요한 것이다.
데이터베이스에서 여러 트랜잭션이 동시에 수행될 때, 수행되는 트랜잭션의 순서에 따라 결과가 달라질 수 있다. 이 때, 트랜잭션을 처리하는 순서를 Transaction Schedule이라 한다. Transaction Schedule은 아래의 세 종류로 구분될 수 있다.
① Serial(직렬) Scedule
② Non-Serial(비직렬) Schedule
③ Serializable(직렬화 가능) Schedule
당연히 Serial Schedule이 격리성이 가장 높기 때문에 항상 정확한 결과를 만들어낸다. 그러나 처리 속도가 매우 느리고 비효율적이라는 점에서 문제가 있다(격리성과 처리속도 사이의 Trade-Off). 만약 Non-Serial Schedule이 직렬 스케줄과 같은 결과를 낼 수 있으면, 이는 Serializable 한 스케줄이라고 말한다. 즉, Serializable Schedule은 Serial Schedule과 Non-Serial Schedule의 장점을 합친(빠른 처리 속도와 정확한 결과를 보장하는) Schedule인 셈이다.
직렬화 가능성을 검사하기 위한 방법으로 Graph를 이용할 수 있다. 각 트랜잭션은 그래프의 노드로 표현하고, 아래와 같은 경우에 대해서는 T1→T2의 간선으로 표현한다.
T1이 read(x)를 수행한 후 T2가 read(x)를 수행하는 경우는 무순서성이 보장되므로, 간선으로 표현하지 않는다. 이렇게해서 완성된 그래프에 사이클이 없고, 모든 트랜잭션 간의 간선이 일관된 방향을 가질 경우, 해당 스케줄은 Serializable 하다고 판단할 수 있다.
이 때, 모든 트랜잭션이 꼭 일렬로 정렬될 필요는 없으며, 위상 정렬만 가능하다면 해당 스케줄은 Serializable 하다. 즉, 아래와 같은 그래프도 직렬화가 가능하다.
아래의 예시에서 직렬화 가능성을 검사해보자.
위 트랜잭션을 그래프로 나타내면 아래와 같다.
Interleaved 방식으로 실행한 결과가 직렬 스케줄(T1 → T2)의 결과와 동일하긴 하지만, 그래프에 사이클이 발생하기 때문에 직렬 불가능한 스케줄이다. 참고로, 실행 결과가 직렬 스케줄과 동일한 것은 두 트랜잭션 모두 덧셈만 수행하기 때문에 우연히 같았을 뿐이다. 정리하면, 아래의 두 경우에 대해서는 주어진 스케줄이 Serializable 하지 않다고 말할 수 있다.
그러나 모든 스케줄의 직렬 가능성을 일일이 검사하는 것은 어려운 일이다. 그러므로, 직렬 가능성을 검사하는 방식 대신, 직렬 가능성을 보장하는 방식을 사용하는데, 이 때 사용되는 규약이 바로 Locking Protocol이다.
① 트랜잭션에서 read(x) 또는 write(x)를 수행하려면 반드시 먼저 lock(x)를 수행해야 한다.
② 트랜잭션에서 수행한 lock(x)에 대한 unlock(x)는 반드시 트랜잭션이 종료되기 전에 수행되어야 한다.
③ 트랜잭션은 다른 트랜잭션이 걸어놓은 lock(x)에 대해 재차 lock(x)를 수행할 수 없다.
④ lock(x)를 수행한 트랜잭션 외의 다른 트랜잭션에서는 unlock(x)를 수행할 수 없다.
그러나, Lock을 수행한다고 해서 스케줄의 직렬화 가능성을 정확히 판단할 수 있는 것은 아니다. 아래의 예시를 보자.
x=100, y=200이라 할 때, 위 스케줄의 실행 결과는 x=300, y=500이지만, 직렬화 스케줄 <T1, T2>의 실행 결과는 x=400, y=600이므로, 이 스케줄은 Serializable 하지 않을 것으로 판단된다. 그러나 사실 이 스케줄은 아래에서 이야기할 2PLP를 적용하여 직렬화 가능하다. 즉, 단순히 위의 Locking Protocol을 적용하는 것만으로는 직렬 가능성을 완전히 보장할 수 없다.
위와 같은 문제가 발생하는 근본적인 원인은 하나의 트랜잭션 내에 lock과 unlock을 수행하는 단계가 엄격히 구분되지 않기 때문이다. 쉽게 말해서 x에 대한 연산과 y에 대한 연산을 수행할 때, lock(x) → unlock(x) → lock(y) → unlock(y)의 순서로 실행하는 것이 아닌, lock(x) → lock(y) → unlock(x) → unlock(y)의 순서로 실행해야 한다는 것이다.
이처럼 lock만 수행하는 단계와 unlock만 수행하는 2개의 단계로 구분하는 방식을 2PL(Two Phase Locking)이라고 부른다. 2PL의 두 단계를 조금 더 자세히 설명하면 아래와 같다.
① Growing Phase(확장 단계)
② Shrinking Phase(축소 단계)
Locking Protocol의 문제점 부분에서 다룬 문제에 2PLP를 적용하여, 직렬화 가능성을 검사해보자. 아래는 그 문제이다.
위 스케줄에 2PLP를 적용한 것이 아래의 스케줄이다. Trans 1은 x+100, y+100, Trans 2는 x*2, y*2를 수행한다고 할 때, 2PLP를 적용한 스케줄에선 어떤 점이 달라졌는지 찾아보자.
① Trans 1
② Trans 2
위 스케줄이 정말 Serializable 한지 확인해보자. x=100, y=200이라 하고, 스케줄의 결과를 계산해보면, x=400, y=600이 된다. 직렬화 스케줄 <T1, T2>의 실행 결과도 마찬가지로 400, 600이므로, Interleaved 방식으로 처리하였음에도 Serial Schedule과 동일한 결과를 도출해낸다. 따라서 위 스케줄은 Serializable 하다.
스케줄 내의 모든 Transaction이 2PLP를 준수한다면, 해당 스케줄은 Serializable 하다. 이 명제에 대해 두 가지 주의해야 할 점이 있다.
먼저, 첫번째 주의 사항에 대해 생각해보자. 2PLP는 직렬화 가능성에 대한 충분조건일 뿐, 필요조건은 아니기 때문에 2PLP를 만족하지 않더라도 Serializable 한 스케줄이 존재할 수 있다. 아래의 예시를 보자.
Trans 1과 Trans2 모두 2PLP를 만족하지 않지만, <T1, T2>의 실행결과와 동일한 결과를 도출한다. 따라서, 2PLP를 만족하지 않더라도 Serializable 한 경우가 존재할 수 있다.
그럼에도 2PLP는 거의 모든 스케줄의 직렬화 가능성을 보장할 수 있다. 스케줄이 2PLP를 만족하지 않으면서, Serializable 한 경우는 많지도 않을 뿐더러, 명백하게 Seriazable 이라는 것이 드러나는 경우가 많기 때문에(덧셈, 뺄셈만 수행하는 등) 2PLP는 Serializable Check에 충분히 유용하다.
조금 복잡한 이야기일 수는 있지만, Serializable 한 모든 스케줄은 2PLP를 적용한 스케줄로 변환할 수 있다. 따라서, 2PLP를 만족하지 않지만 Serializable 한 스케줄에 대해 2PLP를 적용한 스케줄로 변경하라는 문제가 출제될 수 있다. 위 문제의 스케줄을 2PLP를 적용한 스케줄로 변환하면 아래와 같다. (2PLP의 개념 부분에서 다루었던 이미지이다.)
다음으로 두번째 주의사항에 대한 예시를 살펴보자.
위 예시에서 Trans 2는 2PLP를 만족하지만, Trans 1은 2PLP를 만족하지 않으므로, 직렬화 가능성을 보장할 수 없다. 실제로 x=100, y=200을 넣어 계산한 결과는 직렬화 스케줄 <T1, T2>와 다른 결과를 보이므로, 위 스케줄은 Serializable 하지 않다.
Isolation Level(트랜잭션 격리 수준)은 동시에 여러 트랜잭션이 동일한 데이터에 접근할 때, 다른 트랜잭션에서 변경하거나 조회하고 있는 데이터를, 읽을 수 있도록 허용할지 결정하는 것을 의미한다.
아래로 갈수록 높은 수준의 격리 수준을 제공한다. 아래는 MySQL 기준의 Isolation Level을 나타낸 것이다.
① READ UNCOMMITTED
② READ COMMITTED
③ REPEATABLE READ
④ SERIALIZABLE
① 조회
SELECT @@GLOBAL.transaction_isolation; // 전역 설정
SELECT @@SESSION.transaction_isolation; // 현재 세션에 대한 설정
② 변경
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;