Post

<오라클 성능 고도화 원리와 해법1> Ch02-03 비관적 vs. 낙관적 동시성 제어

오라클 성능 고도화 원리와 해법1 - Ch02 트랜잭션과 Lock - 03 비관적 vs. 낙관적 동시성 제어

동시성 제어를 위해, 앞에서 설명한 트랜잭션 고립화 수준을 변경하는 DBMS 기능을 사용할 수 없는 경우가 있다. 특히, n-Tier 구조가 지배적인 요즘 같은 개발 환경에서 더욱 그렇고, 그럴 때는 트랜잭션의 동시성 제어를 개발자가 직접 구현해야만 한다.

동시성 제어는 비관적 동시성 제어와 낙관적 동시성 제어로 나뉜다.

비관적 동시성 제어(Pessimistic Concurrency Control)는 사용자들이 같은 데이터를 동시에 수정할 것이라고 가정한다. 따라서 한 사용자가 데이터를 읽는 시점에 Lock을 걸고 조회 또는 갱신 처리가 완료될 때까지 이를 유지한다. Locking은 첫 번째 사용자가 트랜잭션을 완료하기 전까지 다른 사용자들이 그 데이터를 수정할 수 없게 만들기 때문에 비관적 동시성 제어를 잘못 사용하면 동시성을 저해받게 된다. “잘못 사용하면”이라고 한 데에 주목하자. 잘 사용하면 약이 될 수도 있다는 뜻이며 글을 계속 읽다 보면 이해하게 될 것이다.

반면, 낙관적 동시성 제어(Optimistic Concurrency Control)는 사용자들이 같은 데이터를 동시에 수정하지 않을 것이라고 가정한다. 따라서 데이터를 읽을 때는 Lock을 설정하지 않는다. 그런데 낙관적 입장에 섰다고 해서 동시 트랜잭션에 의한 데이터의 잘못된 갱신을 신경 쓰지 않아도 된다는 것은 절대 아니다. 읽는 시점에 Lock을 사용하지 않았지만, 데이터를 수정하고자 하는 시점에 앞서 읽은 데이터가 다른 사용자에 의해 변경되었는지를 반드시 검사해야 한다.

낙관적 동시성 제어를 사용하면 Lock이 유지되는 시간이 매우 짧아져 동시성을 높이는 데에 유리하다. 하지만 다른 사용자가 같은 데이터를 변경했는지 검사하고 그에 따라 분기해 나가야 하는 귀찮은 처리 절차가 뒤따른다. 정말 귀찮아서일까, 아니면 동시성 제어의 필요성을 모르는 것일까? 튜닝을 다니면서 개발 팀에서 작성한 SQL들을 많이 분석하게 되는데, ‘낙관적 동시성 제어’를 해야 하는 상황에서 대부분 ‘동시성 제어 없는 낙관적 프로그래밍’을 하고 있다.

예를 들어, 온라인 쇼핑몰에서 특정 상품을 조회해서 주문을 시작하고 결제를 완료하는 순간까지를 하나의 트랜잭션으로 정의했다고 가정하자. 상품 조회 시에는 1,000원이었던 상품이 주문을 진행하는 동안 가격이 수정돼서 결제를 완료하는 순간에는 1,200원일 수 있다. 따라서 최종 결제 버튼을 클릭하는 순간 상품 가격의 변경 여부를 체크하고, 변경되었다면 해당 주문을 취소시키거나 사용자에게 변경 사실을 알리고 처리 방향을 확인받는 프로세스를 거쳐야만 한다. 물론 업무적으로 ‘주문’을 시작하는 시점의 가격을 기준으로 주문을 처리하는 것이 옳다면 그에 맞는 단계에서 일관성 체크를 해서 처리하면 된다.

(1) 비관적 동시성 제어

하지만 위에서와 같이 select 문에 for update를 사용해서 해당 고객 레코드에 Lock을 걸어둔다면 데이터가 잘못 갱신되는 문제를 방지할 수 있다.

select 시점에 Lock을 거는 비관적 동시성 제어는 자칫 시스템 동시성을 심각하게 떨어뜨릴 우려가 있다. 그러므로 wait 또는 nowait 옵션을 함께 사용하면 Lock을 얻기 위해 무한정 기다리지 않아도 된다.

wait 또는 nowait 옵션을 사용하면, 다른 트랜잭션에 의해 Lock이 걸렸을 때 Exception을 만나게 되므로 “다른 사용자에 의해 변경 중이므로 다시 시도하십시오”라는 메시지를 출력하면서 트랜잭션을 종료할 수 있다. 따라서 오히려 동시성을 증가시키게 된다.

금융권에서 개발하는 프로그래머들은 for update 문을 자주 사용한다. 금전을 다루는 업무가 아니더라도 면밀하게 분석해보면 이런 식으로 동시성 제어를 해야만 하는 경우가 부지기수다. 독자가 맡은 업무에 이를 적용할 부분이 있는지 꼼꼼히 따져보기 바란다.

(2) 낙관적 동시성 제어

앞선 select 문에서 읽은 컬럼들이 매우 많다면 update 문에 조건절을 일일이 기술하는 것이 여간 귀찮은 일이 아닐 것이다. 만약 update 대상 테이블에 최종 변경일시를 관리하는 컬럼이 있다면 이를 조건에 넣어 간단히 해당 레코드의 갱신 여부를 판단할 수 있다.

낙관적 동시성 제어에서도 update 전에 아래 select 문(nowait 옵션을 사용한 것에 주목)을 한 번 더 수행한다면 다른 트랜잭션에 의해 설정된 Lock 때문에 동시성이 저하되는 것을 예방할 수 있다.

오라클 10g부터 낙관적 동시성 제어를 위해 활용할 수 있는 기능을 한 가지 소개하려고 한다. 위에서처럼 별도의 Timestamp 컬럼을 두고 동시성 제어를 하려면 테이블 레코드를 insert/update/delete 할 때마다 변경일시 컬럼을 변경하도록 빠짐없이 구현해야만 한다. 그런데 애플리케이션을 통하지 않고 사용자가 직접 값을 바꾸는 일이 생긴다면, 변경일시 컬럼까지 변경하는 규칙을 지키지 않았을 때 Lost Update 문제가 다시 재발하게 된다.

오라클 10g부터 제공되는 Pseudo 컬럼 ora_rowscn을 활용한다면 Timestamp를 오라클이 직접 관리해주므로 쉽고 완벽하게 동시성을 제어할 수 있다.

결론적으로 말해, 변경일시 컬럼을 단순히 동시성 제어를 위해서만 사용한다면 ora_rowscn을 활용하는 것이 효과적이지만, 변경일시 정보를 다른 용도로도 활용해야 한다면 기존 방식대로 구현해야 한다.

중대한 버그
본서가 출간되고 2쇄를 시작하기 직전, 방금 설명한 기능(ora_rowscn을 이용한 동시성 제어)에 중대한 버그(bug 5270479)가 있음을 발견하였다.

안타까운 것은, 11gR2(11.2.0.1)에서도 Bug Fix가 되지 않았다는 사실(Bug 7338384)이다. 결론적으로, (언제일지 모르지만) 버그가 고쳐질 때까지 이 기능을 이용한 동시성 제어를 할 수 없게 되었다. 버그 리포트(bug 5270479)에서 오라클이 제시하는 대안은 다음과 같다. 즉, 기존 방식대로 동시성 제어를 하라는 뜻이다.

“Include a sequence column in the table and increment that on every UPDATE then use that in predicates to ensure that the row has not been modified.”
This post is licensed under CC BY 4.0 by the author.