동시 제어에서 발생하는 이상 현상
이전에 동시성 제어에 대해서 알아보았다.
트랜잭션을 병렬적으로 처리할 수 있게 되면서 많은 이점을 가져갈 수 있지만 반대로 그에 따른 이상 현상이 발생할 수 있다.
대표적인 3가지 이상 현상을 알아보고 그에 따른 고립화 수준(Isolation Level)과 다른 이상 현상까지 알아보자.
대표적인 현상 3가지
- Dirty read
Dirty read 어떤 트랜잭션에서 commit 되지 않은 데이터를 다른 트랜잭션에서 읽었을 때 발생하는 문제이다.
위의 그림을 가지고 예를 들어보자.
트랜잭션 1과 트랜잭션 2를 편하게 T1과 T2로 생각하고 정리를 해보았다.
먼저 T1에서 x를 read 하면 10이라는 결괏값이 나온다. 그다음 T2에서 y에 70을 저장하는 write 작업을 진행했다.
그 후 다시 T1에서 y를 read하면 이전에 T2에서 write 했기 때문에 y의 값은 70으로 출력된다.
T1은 x 값에 y 값을 더하는 트랜잭션이기 때문에 x의 값인 10과 y의 값인 70을 더하여 x에 80이라는 새로운 값을 write 하고 commit 하여 최종적으로 반영해 준다.
여기까지만 보면 별 문제없어 보일 수 있지만 T2에서 문제가 발생하여 abort 되면 문제가 발생한다.
T2에서 abort가 발생하여 롤백을 진행하게 되면 T2 트랜잭션이 실행되기 전으로 돌아가게 되면서 y의 값은 다시 20으로 바뀐다.
y의 값이 바뀌면서 T1의 x 값도 달라져야 하지만 T1 트랜잭션은 이미 commit을 한 상태이기 때문에 일관성이 없는 상태로 남아있게 된다.
위의 예제로 다른 상황에서의 Dirty Read 문제를 살펴보자.
x가 y에게 40만큼 이체해야 되기 때문에 먼저 x 값을 read 하여 50이라는 값을 확인한다.
그다음 x에서 40만큼 빠져야 하기 때문에 x에 10 값을 저장하는 write 작업을 한다.
여기서 T2 트랜잭션이 시작되었다고 가정해 보자.
T2는 x와 y를 읽는 트랜잭션이기 때문에 x와 y를 read 하여 각각 10과 50의 값을 확인하고 commit 한다.
다시 T1으로 넘어와 y를 read 하여 50이라는 잔액을 확인한다.
그다음 이전에 x에서 이체했던 40만큼 y에 더하여 90이라는 값을 write 해주고 commit 하여 반영해 준다.
여기서 알 수 있는 문제는 아직 T1에서 이체 작업이 완료되지 않아 y의 값이 업데이트되기 전인 상황에서 T2가 y의 값을 읽었기 때문에 데이터의 일관성을 잃게 된다.
- Non-Repeatable Read
Non-Repeatable Read는 같은 데이터의 값이 달라지는 현상을 말하는데 간단하게 설명하면 하나의 트랜잭션에서 동일한 값을 반복하여 읽었는데 값이 달라지는 상황이다.
각 트랜잭션마다 Isolation 속성을 지켜야 하는데 이러한 이상 현상에서는 Isolation이 깨지게 된다.
예시를 통해 알아보자.
먼저 T1은 x 값을 두 번 읽는 트랜잭션으로 x를 read 하여 10이라는 값을 확인한다.
그다음으로 T2가 실행되어 x 값을 read 하고 결과로 나온 10에 40을 더하여 기존의 x 값에 50을 저장하는 write 작업을 수행한다.
T2의 작업이 다 끝나면서 마지막으로 commit 하여 반영해 준다.
T1의 작업은 아직 x의 값을 한 번 더 읽어야 돼서 마지막으로 x의 값을 read 하면 50이라는 값을 읽게 되고, 최종적으로 commit 하여 반영한다.
앞서 설명했던 것처럼 트랜잭션은 Isolation 속성을 유지해야 하기 때문에 각 트랜잭션은 마치 독립적으로 실행되는 것처럼 보여야 한다.
T1의 작업은 단순하게 x의 값을 두 번 읽는 것 말고는 없기 때문에 두 번의 결과가 똑같이 10이 나와야 하지만 10과 50 총 2개의 결과가 나오게 된다.
이처럼 같은 데이터를 반복해서 조회했을 때 다른 값이 나오는 현상을 Non-Repeatable Read라고 한다.
- Phantom read
Phantom read는 아직 commit 되지 않은 데이터를 다른 트랜잭션에서 참고하여 write 작업을 하게 되면서 없던 데이터가 생기는 현상이다.
예제를 통해 알아보자.
위의 그림과 같이 트랜잭션 T1과 T2가 있고 데이터베이스에는 t1, t2라는 튜플이 있다고 가정해 보자.
먼저 처음에 T1에서 v값이 10인 튜플을 read 하면 t1 만 반환된다.
그다음 T2에서 t2 튜플이 가지고 있는 v값에 10을 저장하는 write 작업을 진행하고 commit 한다.
다시 T1으로 돌아와 v 값이 10인 튜플을 조회하면 t1와 t2가 반환된다.
마지막으로 commit 하여 반영해 준다.
여기서 보면 원래 T1에서는 v 값이 10인 튜플을 read 할 때 t1 만 반환돼야 하지만 T2에서 write 작업을 하게 되면서 t1과 t2가 같이 반환되는 이상 현상이 발생하게 되는데 이러한 현상을 Phantom read라고 한다.
대표적인 3가지 현상들에 대해서 알아보았다.
DBMS의 동시성을 제어하기 위해 이러한 이상 현상들이 발생하지 않게 해야 한다.
여태까지 살펴본 현상들이 모두 발생하지 않게 만들 수 있지만 그러면 제약사항이 많아져서 동시 처리 가능한 트랜잭션 수가 줄어들게 된다.
DB의 처리량을 높이기 위해서 동시성을 사용하는데 오히려 이상 현상을 줄이겠다고 제약사항이 많아지면 DB의 전체 처리량이 하락하게 되고, 결국에는 동시성을 사용하는 이점을 얻을 수 없게 돼버린다.
고립화 수준(Isolation Level)
고립화 수준은 앞서 살펴본 이상 현상을 줄이겠다고 제약 사항을 추가하면 동시성을 하는 이유가 희미해져 버리니 일부 이상 현상을 허용하는 수준을 만든 것이다.
고립화 수준에는 총 4가지가 있는데 각 수준마다 허용하는 이상 현상들이 다르다.
Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read |
Read Uncommitted | O | O | O |
Read Committed | X | O | O |
Repeatable Read | X | X | O |
Serializable Read | X | X | X |
고립화 수준을 통해서 일정 부분 일관성을 버리더라도 DB의 처리량을 높일 수 있다.
그 외 발생할 수 있는 현상
처음에 살펴봤던 대표적인 3가지 이상 현상 외에도 여러 가지가 있다.
- Dirty Write
Dirty Write는 트랜잭션에서 commit 안된 데이터를 다른 트랜잭션에서 write 할 때 발생하는 현상이다.
트랜잭션 T1과 T2가 있고 x라는 데이터에 접근하여 write 작업을 한다고 가정해 보자.
먼저 T1에서 x에 10을 저장하는 write 작업을 수행한다.
그다음 T2에서 x의 값에 100을 저장하는 write 작업을 수행한다.
여기서 만약 T1 트랜잭션에 어떠한 문제가 발생하여 abort를 통해 write 하기 전의 상태로 돌아가게 되면 x의 값은 10이 된다.
하지만 T2에서도 문제가 발생하여 abort가 된다면 T2의 write 작업 전에 x의 값은 10였기 때문에 최종적으로 x에 10이라는 값으로 바뀌게 된다.
트랜잭션 롤백 시 정상적인 recovery는 매우 중요하기 때문에 모든 isolation level에서 dirty write를 허용하면 안 된다.
- Lost Update
Lost Update는 트랜잭션에서 아직 commit 되지 않은 데이터를 다른 트랜잭션에서 업데이트했을 때 해당 업데이트 작업이 덮어 써지면서 업데이트를 했는데도 반영이 안 된 경우이다.
예제를 통해서 Lost Update를 알아보자.
먼저 T1에서 x의 값을 read 하여 50이라는 값을 확인한다.
그다음 T2에서 x에 150을 더하여 업데이트하는 작업을 진행하게 되는데 x를 read 하여 50을 확 안 하고 해당 값에 150을 더하여 x에 저장하는 write 작업을 해준다.
T2의 작업이 완료되면서 commit 하여 반영해 준다.
다시 T1으로 돌아와 이전에 read 해서 나왔던 50의 값에 50을 더하여 x 값에 100을 저장하는 write 작업을 해준다.
마지막으로 T1의 작업이 모두 완료되면서 commit으로 반영해 준다.
원래대로라면 x의 값은 250이 돼야 할 것 같지만 최종적으로 x에는 100이 저장된다. 이를 통해 알 수 있는 것은 T2가 수행했던 x값을 업데이트하는 작업은 아예 손실되면서 없어져버린 것이다.
이러한 업데이트 작업이 손실되는 경우를 Lost Update라고 하며 매우 중요한 문제가 된다.
- Read Skew
Read Skew는 Non-Repeatable Read와 비슷한 현상으로 데이터가 inconsistent 한 데이터를 읽을 경우에 발생한다.
예제를 통해서 살펴보자.
먼저 T2에서 x의 값을 read 하여 50이라는 값을 확인한다.
그다음으로 T1에서 이체 작업을 진행하게 되는데, 먼저 x의 값을 read 하여 50을 확인한다.
그러고 나서 x에서 y로 40만큼 이체해야 돼서 x에 40만큼 뺀 값인 10을 x에 저장하는 write 작업을 진행한다.
이제 이체하는 대상인 y를 read 하여 50을 확인하고, 이전에 x에서 빠졌던 40만큼을 y값에 더하여 y에 90이라는 값을 저장하는 write 작업을 진행한다.
최종적으로 T1의 이체 작업이 완료되면서 commit 하여 반영해 준다.
T1이 끝나고 다시 T2로 넘어와서 y의 값을 read 하면 90이라는 값을 알 수 있게 된다.
위의 예제에서 발생한 문제는 바로 트랜잭션의 일관성이다.
일관성은 x에서 y로 일정 값만큼 이체를 해도 둘의 합은 같아야 한다는 성질인데 예시로 보면 x에서 y로 40만큼 이체를 진행해서 x가 10이 되고, y가 90이 되더라도 둘의 합은 최초의 합과 동일하다.
하지만 T2에서 read 한 x와 y값을 더하면 100이 아닌 140이 나와버리면서 일관성이 없어지게 되는 상황이 발생하게 된다.
- Write Skew
Write Skew는 정상적으로 처리된 것처럼 보이지만 데이터 불일치를 만드는 현상을 말한다.
예시를 보면 먼저 트랜잭션 T1, T2가 있고, x + y는 0보다 크거나 같아야 하는 제약조건이 있다고 가정하자.
먼저 T1에서 80을 인출하기 전에 x와 y를 read 하여 제약조건을 통과하는지 확인한다.
이 상황에서 T2도 동시에 실행되면서 90을 인출하기 위해 x와 y를 read 한다고 해보자.
T1에서 x와 y를 read 하여 더하면 100으로 0보다 크기 때문에 80만큼 인출하기 위해 x에서 80을 뺀 -30을 저장하는 write 작업을 진행한다.
T2도 마찬가지로 x와 y를 read하여 더하면 100이므로 0보다 크기 때문에 90만큼 인출하기 위해 y에서 90을 뺀 -40을 저장하는 write 작업을 진행한다.
T1과 T2 모두 트랜잭션을 마치고 commit을 하게 되는데 제약조건이 위배되는 상황이 발생하게 된다.
이처럼 정상적으로 실행되는 것처럼 보이나 데이터의 불일치가 발생하게 되는 현상을 Write Skew라고 한다.
참고 자료
'CS > 데이터베이스' 카테고리의 다른 글
데이터베이스 - 스키마 (0) | 2024.08.12 |
---|---|
데이터베이스 - 인덱스 (0) | 2024.08.08 |
데이터베이스 - 동시성 제어 (0) | 2024.08.05 |
데이터베이스 - 트랜잭션 (0) | 2024.08.04 |
데이터베이스 - ERD (0) | 2024.08.01 |