프로젝트가 끝나고 운영체제와 네트워크 강의를 들으며 얀전히(?) CS 지식을 쌓고 있었다. 이는 개발자로서 기본소양을 쌓기 위한 목적이었지만, 당연히 기술 면접에 대한 준비의 일환이기도 했다. 꾸준히 지식을 쌓고 있으면 면접에서도 당당할 수 있지 않을까 했는데 너무 안일한 생각이었다. 인터넷에서 기술면접에서 단골로 나온다는 질문 리스트를 찾아보니, 대부분 한마디도 대답할 수 없는 것들이었기 때문이다.
그래도 다른 질문들은 검색해보면 어렴풋이는 알 것 같은 개념이긴 했는데, "MVCC 가 무엇인가?" 라는 질문은 검색해서 관련 포스팅을 읽고 나서도 전혀 오리무중이었다. 발등에 불이 떨어진 기분으로, DB 에서의 동시성 제어(Concurrency Control) 의 기본적인 공부들을 했고, 지식이 MVCC 가 무엇인지 답변할 수 있을 수준에 도달하는 것이 결코 간단하지 않다는 것을 깨달았다.
솔직히 MVCC 질문이 지금 시점에서 얼마나 시급한 것인지는 모르지만, 배운 것은 조금씩 정리해 두고자 한다.
동시성 제어(Concurrent Control)
CPU 가 여러 프로세스의 연산을 동시에 처리하듯, DB 또한 동시에 여러개의 트랜잭션을 처리하도록 설계되어 있다. 이 때, 복수의 트랜잭션이 한번에 같은 데이터에 대한 접근을 하는 경우 의도되지 않은 결과를 낳기도 한다. 동시성 제어란 이와 같이 여러 개의 요청이 동시에 들어온 상황에서 요청에 대한 수행 결과가 논리적으로 일관되면서도, 효율적으로 동작하도록 요청을 적절하게 스케줄링하는 것을 의미한다.
1. Serial / Non-Serial Schedule
1) Serial Schedule
Schedule 은 트랜잭션을 수행되는 절차를 의미하며 어떤 Schedule이 Serial 하다는 말은 하나의 트랜잭션이 순차적으로 수행되는 것을 말한다. (즉, 하나의 트랜잭션이 완전히 끝나고 다음 트랜잭션이 수행) DB 가 Serial Schedule 에 따라 요청을 처리하는 경우, 그 수행 결과는 DB 의 정합성을 깨트리지 않는다. (순차적으로 수행되므로 동시성에 대한 개념을 고려할 필요도 없다.) 그러나 이러한 방식은 DB 의 성능을 심각하게 떨어트리게 되므로 일반적으로 사용되지 않는다.
2) Non-Serial Schedule
Serial 하지 않은 Schedule 을 말한다. 하나의 트랜잭션이 수행되면서 또 다른 트랜잭션도 병렬적으로 수행된다. CPU 에서 여러 프로세스가 병렬적으로 실행되는 것과 마찬가지로, 이러한 병렬적인 트랜잭션 처리는 I/O 작업 시간을 효율적으로 사용할 수 있게 되어 성능이 향상된다. 그러나 앞서 서술한대로 여러 트랜잭션에서 동시에 같은 리소스에 접근하게 되는 경우 의도치 않은 결과가 나타나게 된다.
3) Serializability
위 두가지 방식은 데이터 정합성과 성능 이라는 지표를 두고 Trade-Off 관계를 갖게 된다. 다시 말해 Serial 방식은 완벽한 정합성을 보장하는 대신 성능이 저하되며, Non-Serial 방식은 정합성을 다소 포기하는 대신 빠른 성능을 갖는다. 그러나 대부분의 경우 성능과 데이터 정합성을 어느정도 보장되기를 요구받게 된다. 즉, 동시에 여러 트랜잭션을 처리하면서도, Serial 방식으로 처리하는 것과 동일한 결과를 가지는(즉, 완벽한 정합성을 가지는) 방식의 트랜잭션 처리를 요구한다.
많은 경우, Non-Serial 한 절차로 트랜잭션을 처리하면서도, Serial 방식의 결과를 얻게되는 이상적 절차가 존재할 수 있으며, 이를 Serializable Schedule 이라고 지칭한다. 또한, 그리고 특정 Schedule 이 이러한 특성을 가질때 Serializability 이 있다 라고 할 수 있다.
2. Conflict
1) Conflict
Non-Serial 방식의 스케줄링에서 의도치 않은 결과를 초래하는 원인은 트랜잭션간 동일한 자원에 접근하기 때문이다. 이러한 접근을 Conflict(충돌) 이라고 하면, Conflict 은 다음과 같이 3가지 종류로 구분된다.
- 읽기-쓰기 충돌(Read-Write Conflict): 트랜잭션 A가 데이터를 읽고, 트랜잭션 B가 같은 데이터를 쓰는 경우.
- 쓰기-읽기 충돌(Write-Read Conflict): 트랜잭션 A가 데이터를 쓰고, 트랜잭션 B가 같은 데이터를 읽는 경우.
- 쓰기-쓰기 충돌(Write-Write Conflict): 트랜잭션 A가 데이터를 쓰고, 트랜잭션 B가 같은 데이터를 쓰는 경우.
다음은 Conflict 가 문제를 일으키는 간단한 예시이다.
두 연산 모두 읽고 쓰는 연산이 있기 때문에, 읽기-쓰기, 쓰기-쓰기 충돌이 모두 발생하고 있다. 정상적으로 기대된 연산 결과는 X 의 값이 85 이어야 하나 위의 경우엔 최종적으로 X = 115 이라는 결과가 나온다.
+)
이는 두가지 update 연산이 동시에 수행될 때 그 중 하나가 사라진다고 하여, lost update 라고 불리는 현상이다. 추후 설명하겠지만 트랜잭션 격리수준을 Repeatable Read 로 설정하면 해당 현상을 방지할 수 있다.
2) Conflict Serializable
Conflict 발생이 우려되는 Non-Serial 한 스케줄링에도 트랜잭션이 보장되고 데이터가 정합성을 가지게 될 때 이를 Conflict Serializable 이라고 한다. 어떤 조건을 만족해야 Serializable 하게 될까? Serial 스케줄링에서 이루어지는 연산과 동일한 선후 관계를 유지하면 된다. 아래 예시를 보자
- A 트랜잭션 = X 와 Y 에 각각 10 씩을 더함
- B 트랜잭션 = Y 에 10을 뺌
Serial
- A tx : ------------------------------> R X = 10 --> W X = 20 --> R Y = 20 --> W Y =30 --> C --- |
- B tx : -> R Y = 10 --> W Y = 20 --> C ---------------------------------------------------------- |
Non-Serial (Conflict Serializable)
- A tx : ---------> R X = 10 ------> W X = 20 ---> R Y = 20 ----> W Y =30 ----> C--- |
- B tx : --> R Y = 10 -------> W Y = 20 ------------------------------------------> C -- |
충돌 대상이 되는 Y에 대한 연산은 서로 동일한 선후관계를 가지는 것을 볼 수 있다.