Post

<오라클 성능 고도화 원리와 해법1> Ch01-07 Consistent vs . Current 모드 읽기

오라클 성능 고도화 원리와 해법1 - Ch01 오라클 아키텍처 - 07 Consistent vs. Current 모드 읽기

본 절에서 설명하는 내용은 다음과 같다.

  1. Consistent 모드 읽기와 Current 모드 읽기의 차이점
  2. Consistent 모드로 갱신할 때 생기는 현상
  3. Current 모드로 갱신할 때 생기는 현상
  4. Consistent 모드로 읽고, Current 모드로 갱신할 때 생기는 현상
  5. Consistent 모드로 갱신 대상을 식별하고, Current 모드로 갱신
  6. 오라클에서 일관성 없게 값이 갱신되는 사례

(1) Consistent 모드 읽기와 Current 모드 읽기의 차이점

먼저, Consistent 모드 읽기(gets in consistent mode)는, SCN 확인 과정을 거치며 쿼리가 시작된 시점을 기준으로 일관성 있는 상태로 블록을 액세스하는 것을 말한다. 이 모드로 데이터를 읽을 때는 쿼리가 1시간 걸리든 10시간 걸리든 항상 쿼리가 시작된 시점의 데이터를 가져온다.

Current 모드 읽기(gets in current mode)는, SQL 문이 시작된 시점이 아니라 데이터를 찾아간 바로 그 시점의 최종 값을 읽으려고 블록을 액세스하는 것을 말한다. 블록 SCN이 쿼리 SCN보다 높고 낮음을 따지지 않으며, 그 시점에 이미 커밋된 값이라면 그대로 받아들이고 읽는다.

(2) Consistent 모드로 갱신할 때 생기는 현상

EMP 테이블 7788번 사원의 SAL 값이 현재 1,000인 상황에서 아래 TX1, TX2 두 개의 트랜잭션이 동시에 수행되었다. 양쪽 트랜잭션이 모두 완료된 시점에 7788번 사원의 SAL 값은 얼마이어야 할까?

물론 TX2 update는 t2 시점에 시작하지만 TX1에 의해 걸린 Lock을 대기하다가 t3 시점에 TX1이 커밋된 후에 진행을 계속한다. 아주 쉬운 문제처럼 보이지만 여러 사람에게 물어보면 확신하면서 선뜻 정답을 맞히는 경우는 흔치 않다. 분명히 답은 1,200과 1,300 둘 중 하나인데, 설명은 제각각이다. 재미있는 것은, 오라클만의 독특한 읽기 일관성 모드에 익숙한 사람들이 더 시간을 끌면서 고민하고 심지어 틀린 답을 내기도 한다는 사실이다.

그 이유를 곰곰이 생각해 보면 오라클이 특이하게도 두 가지 읽기 모드를 제공하는 데에서 비롯된다. 즉 쿼리 시작 시점을 기준으로 값을 읽는다는 사실은 대부분 알고 있다. 그러다 보니 오라클 사용자들은 항상 Consistent 모드 읽기 중심으로 생각한다.

이런 Lost Update 문제를 회피하려면 갱신 작업만큼은 Current 모드를 사용해야 한다. 정리하면, <상황1>에서 TX2 update는 Exclusive Lock 때문에 대기했다가 TX1 트랜잭션이 커밋한 후 Current 모드로 그 값을 읽어 진행을 계속한다. 그럼으로써 Lost Update 문제를 피할 수 있다.

(3) Current 모드로 갱신할 때 생기는 현상

또 다른 예로서, “delete from 로그” 문장이 수행되는 도중에 다른 트랜잭션에 의해 새로 추가된 로그 데이터까지 지워질 수도 있는데, update/delete 도중에 갱신/삭제 대상이 그때그때 변할 수 있다는 것은 오라클 사용자 입장에서는 참 놀라운 사실이다.

(4) Consistent 모드로 읽고, Current 모드로 갱신할 때 생기는 현상

Current 모드로 갱신을 수행할 때 어떤 현상이 생기는지 살펴보았다. 이 문제를 피하려고 오라클은 Consistent 모드로 읽고, Current 모드로 갱신한다.

하지만 다른 DBMS와 마찬가지로 오라클에서도 TX2의 갱신은 실패한다(갱신 레코드 건수 = 0).

(5) Consistent 모드로 갱신 대상을 식별하고, Current 모드로 갱신

그렇다면 실제 오라클은 어떻게 두 개의 읽기 모드가 공존하면서 update를 처리하는 것일까?

Consistent 모드에서 수행한 조건 체크를, Current 모드로 액세스하는 시점에 한 번 더 수행한다는 점이다.

이 단계에서 Current 모드로 다시 한번 조건을 체크하고, 갱신할 값을 읽어 수정/삭제한다.

단계 1은 update/delete가 시작된 시점 기준으로 수정/삭제할 대상을 식별하려고 Consistent 모드 읽기를 사용할 뿐이며, 거기서 읽은 값을 단계 2에서 갱신하는 데 사용하지는 않는다. 단계 1이 필요한 이유는, 갱신이 진행되는 동안 추가되거나 변경을 통해 범위 안에 새로 들어오는 레코드를 제외하고자 하는 것이다. 이미 범위 안에 포함돼 있던 레코드는, 단계 2에서 변경이 이루어지는 바로 그 시점 기준으로 값을 읽고 갱신한다. 이 때는 블록 SCN이 쿼리 SCN보다 높고 낮음을 따지지 않으며, 그 시점에 이미 커밋된 값이라면 그대로 받아들이고 읽는다. 이 때문에(update 문이 시작된 이후에 1,000에서 1,100으로 값이 변경되었기 때문에) <상황4>에서 TX2의 update가 실패하는 것이다.

복잡하다고 느끼는 독자를 위해 짧게 요약하면, 다음과 같다.

  1. select는 Consistent 모드로 읽는다.
  2. insert, update, delete, merge는 Current 모드로 읽고 쓴다. 다만, 갱신할 대상 레코드를 식별하는 작업만큼은 Consistent 모드로 이루어진다.

이 두 가지 사실만 기억하면 그다지 어렵지 않게 동시 트랜잭션의 최종 결과를 예측할 수 있다.

Write Consistency
분명히 갱신 대상이었는데, 값이 달라졌다고 아무런 처리 없이 지나가도 상관없는 걸까? 그렇지 않다. 데이터 정합성에 문제가 생기는 경우가 있고, 이를 방지하려고 오라클은 ‘Restart 메커니즘’을 사용한다. 그때까지의 갱신을 롤백하고 update를 처음부터 다시 실행하는 것이며, Thomas Kyte는 그의 저서에서 이것을 ‘Write Consistency’라고 명명하고 있다.

이 기능은 데이터베이스 일관성을 유지하려고 오라클이 사용하는 아주 내부적인 메커니즘이므로 중요하게 다룰 내용은 아니다. Consistent 모드로 찾은 레코드를 Current 모드로 읽어 갱신한다는 기본 컨셉만 이해해도 충분하다. 갱신 대상 레코드의 값이 중간에 바뀌었다고 항상 Restart 메커니즘이 작동하는 것도 아니다. where 절에 사용된 컬럼 값이 바뀌었을 때만 작동한다.

그리고 Restart 메커니즘이 작동하더라도 대개는 처리 결과가 달라지지 않는다.

Restart 없이 처리했을 때, 일관성에 문제가 있다고 느끼는가? 읽기 작업이 쿼리 시작 시점 기준으로 일관성 있게 진행하는 것처럼, 쓰기 작업도 기준 시점이 존재해야 한다. 그런데 조금 전 트랜잭션 처리 결과는 그렇지가 못하다. 2번과 3번 두 레코드가 모두 갱신 대상에서 제외되었는데, 어느 시점으로 보더라도 이 두 레코드가 동시에 ‘coll <= 5’ 조건을 만족하지 않았던 적이 없었기 때문이다.

이 문제를 해결하려고 오라클은 ‘Restart 방식’을 선택하였고, Restart 시점이 일관성 기준 시점이 된다. 물론, 기준 시점이 바뀌었으므로 처음 update 시작 시점과 Restart 시점 사이에 제3의 트랜잭션이 레코드를 추가/변경/삭제했다면 그것도 최종 update 결과에 반영된다. 그리고 상당히 많은 갱신 작업이 이루어진 이후에 이 기능이 작동함으로써 겪는 성능상 불이익은 사용자가 감내해야 할 몫이다.

참고로, 이후 update 과정에서 Restart가 또 다시 발생하는 불상사를 막기 위해 오라클은 조건에 부합하는 레코드를 모두 SELECT FOR UPDATE 모드로 Lock을 설정하고 나서 update를 재시작한다.

데이터를 일관성 있게 갱신하려면 처음부터 SELECT FOR UPDATE 모드로 Lock을 설정하고 나서 진행해야 안전하지만(-> 비관적 동시성 제어), 대상 범위를 두 번 액세스하는 부하 및 동시성 저하가 발생한다. 따라서 오라클은 일단 update를 진행해보고(-> 낙관적 동시성 제어) 일관성을 해칠 만한 사유가 발생(-> 조건절 컬럼 값이 변경됐음을 발견)한 때만, 그때까지의 처리를 롤백하고 원안대로 다시 시작하는 것으로 이해하면 쉽다.

그러면 상황4에서, t3 시점에 TX2에 대한 블로킹이 해제되면서 로그 테이블에 insert가 진행되지만 정작 emp 레코드에 대한 갱신은 실패해 데이터베이스가 비일관된 상태에 놓이게 된다. 이때 Restart 메커니즘이 작동해 이제까지의 갱신(로깅 데이터도 포함)을 롤백하고 update를 다시 진행한다면 그런 비일관성을 해결할 수 있다.

(6) 오라클에서 일관성 없게 값이 갱신되는 사례

오라클만의 독특한 Consistency 모델을 이해하지 못한 채 프로그램을 작성하고 그로 말미암아 종종 일관성 없는 상태로 값이 갱신되는 오류(주로 사용자 정의 함수/프로시저, 트리거 등을 사용할 때 발생하는 경우가 발견되곤 하는데, 여기서 그런 사례 중 하나를 소개하려고 한다.

스칼라 서브쿼리는 특별한 이유가 없는 한 항상 Consistent 모드로 읽기를 수행한다. 따라서 첫 번째 문장에서 계좌 2의 잔고는 Current 모드로 읽는 반면 계좌 1의 잔고는 Consistent 모드로 읽는다. 위 update 문장이 진행되는 도중에 계좌 1에서 변경이 발생했더라도 update 문이 시작되는 시점의 값을 찾아 읽고, delete가 발생했더라도 지워지기 이전 값을 찾아 읽는다.

반면 두 번째 문장은, Current 모드로 읽어야 할 계좌 2의 잔고 값을 스칼라 서브쿼리 내에서 참조하기 때문에 스칼라 서브쿼리까지도 Current 모드로 작동하게 된다. 따라서 위 update 문장이 진행되는 도중에 계좌 1에서 변경이 발생했다면 그 새로운 값을 읽고, delete가 발생했다면 조인에 실패해 NULL 값으로 update될 것이다.

따라서 update 문이 수행되는 동안 두 테이블로부터 잔고를 변경하는 트랜잭션이 동시에 진행할 수 있는 상황이라면 업무 특성에 맞게 SQL을 작성해야만 한다. 코딩 스타일에 따라 실제 미묘한 차이가 발생할 수 있다는 사실을 테스트를 통해 확인해 보자.

어떤 일이 발생했는지 확인해보자. TX1 트랜잭션이 시작되기 전 시점에서 7788번 계좌의 총잔고는 2,000이었고, 트랜잭션이 끝난 시점의 총잔고는 2,300이어야 한다. 그런데 배치 프로그램을 통해 잔고 컬럼에 저장된 것은 2,200이라는 엉뚱한 값이다. 이런 일이 발생하는 이유는 무엇일까? 계좌 2의 잔고를 읽을 때는 Current 모드에서 읽지만 계좌 1의 잔고를 읽을 때는 Consistent 모드에서 읽었기 때문이다.

계좌 1의 잔고와 계좌 2의 잔고를 모두 Current 모드로 읽었기 때문에 일관성 있게 갱신된 것을 알 수 있다. 이처럼 오라클에서도 일관성 없게 값이 갱신될 가능성은 존재한다. 하지만 사용자가 오라클만의 독특한 읽기 모드를 정확히 이해하고 주의 깊게 SQL을 작성한다면 피해갈 수 있는 문제다.

하지만 본 절에서 설명하고자 한 핵심 내용은 Consistent 모드와 Current 모드 읽기의 차이점을 밝히는 데에 있으므로, 이에 대한 어느 정도의 이해가 생겼다면 계속 책을 읽어 내려가는 데 전혀 지장이 없으므로 안심해도 된다. 그렇더라도 향후 고급 데이터베이스 프로그래머로 성장하려면 Lock과 이를 지원하는 내부 메커니즘, 그리고 트랜잭션이 동시에 진행되는 상황에서 발생할 수 있는 미묘한 차이점들을 이해하는 것이 필수적이므로, 앞으로 이에 대한 개인적인 고민과 연구가 계속 뒤따라야 할 것이다.

This post is licensed under CC BY 4.0 by the author.