SimpleIsBest.NET

유경상의 닷넷 블로그

About COM+ Transaction Isolation Level

by 블로그쥔장 | 작성일자: 2005-09-13 오후 10:30:00
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.
문득 생각나서 키보드를 잡았습니다. COM+에서 제공하는 트랜잭션... 소위 자동 트랜잭션(automatic transaction)은 트랜잭션 프로그래밍을 대단히 단순화 시켜주는 것은 사실입니다. 하지만 세상 모든 일이 그러하듯이 공짜는 없는 법. COM+ 트랜잭션을 사용하면 수동 트랜잭션 혹은 로컬 트랜잭션이라 불리는 트랜잭션에 비해 성능이 좋지 못한 것 역시 사실입니다. 게다가 COM+ 트랜잭션의 격리 레벨 역시 간과할 수 없는 항목이지요. 시스템 아키텍트(architect) 및 어플리케이션 아키텍트는 COM+를 사용함에 있어서 항상 트랜잭션 격리 레벨을 고려해야 하고 이것을 어플리케이션 설계자들에게 주지 시킴으로써 발생할 수 있는 동시성(currency)의 저하를 막아야 합니다. 이번 포스트에서는 COM+의 트랜잭션 격리 수준에 대해 살펴보도록 하겠습니다.

먼저 트랜잭션의 4가지 격리 수준에 대해 상세히 살펴보고 난 후에 COM+ 의 격리 수준에 대해 살펴볼 것이므로 끝까지 읽어 보시기를 권합니다. 중요하며 꼭 알아두어야 할 사항은 뒷 부분에 나오니까요... 트랜잭션의 4가지 격리 수준에 대해 잘 아시는 분은 앞부분은 건너 뛰셔도 됩니다. 지루할 수 있으니까...

COM+ Transaction Isolation Level

트랜잭션의 가장 꽁꼬 깊숙한 핵심을 ACID 규칙이라고들 한다. 뭐 트랜잭션은 원자적(Atomicity)이어야 하느니, 일관성(Consistency)이 있어야 한다는 둥, 격리(Isolation)되어 보호되어야 하며, 또한 견고(Durability)해야 한다는... 뭐 그런 의미이다. 이것들을 여기서 다 얘기할려면 날샐터이니... 오늘은 트랜잭션의 격리에 대해만(!) 노가리를 풀어볼까 한다. 뭐 쉽게 말하면 한 넘(트랜잭션)이 아가씨(자원, 테이블, 테이블의 한 행 등등)한테 열심히 작업 걸고 있는데 다른 넘(트랜잭션)이 와서 깽판을 놓도록 방관해서는 안 된다는 말이다.

4 Isolation Level

이러한 트랜잭션의 격리의 핵심적인 역할은 잠금(locking)이 도맡아 한다. 즉, 한 넘이 아가씨한테 작업 거는 동안 아가씨가 있는 룸을 잠궈버려서 다른 넘이 아가씨에게 접근하지 못하도록 한다는 것이 되겠다. 그런데... 이 잠금을 수행하는데 얼마나 빡시게 잠금을 하는가가 바로 격리 수준(Isolation Level) 이다. 일반적으로 트랜잭션의 격리 수준에는 4가지가 있다. READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERAILIZABLE. 이제 요 4가지 격리 수준에 대해 살펴본 후에, COM+ 상에서는 어떤 격리 수준이 사용되며 주의사항은 무엇인가 살펴보도록 하자.

쪼금 더 썰을 풀기 전에... 필자는 데이터베이스 전문가가 아니다. 그냥 COM+ 프로그래머의 관점에서 잠금에 대해서 논할 것이다. DBA의 관점에서는 필자의 이야기가 우스울 수도 있다. 우매한 넘이 지껄이는 것이라 생각하고 참아주길 바란다. 그렇다고 말도 안 되는 이야기를 우겨댈 것은 아니니 걱정은 안 해도 된다. 그리고 또 한가지... 이 글에서는 오라클에 대해서도 언급할 것이다. 다시 한번 말하지만 필자는 데이터베이스 전문가가 아니며 더우기 오라클은 더욱 더 그렇다. 오라클 OTN 에서 찾아 본다고 찾아보고 테스트도 해본 결과를 적는 것이니 혹시나 잘못된 부분이 있다면 똥꼬 깊쑤키 지적해 주기 바란다.

READ COMMITTED & READ UNCOMMITTED

한 트랜잭션이 수행하는 변경사항(INSERT, UPDATE, DELETE)들은 트랜잭션의 원자성(automicity)에 의거하여 모두가 성공하거나 모두가 실패해야 한다. 따라서 다른 트랜잭션이 이 트랜잭션의 중간 상황 즉, COMMIT 혹은 ROLLBACK 이 아직 수행되지 않은 상황을 읽는 다는 것은 트랜잭션의 원자성과 일관성을 위배하는 것이 된다. 이 때문에 어떤 트랜잭션이 변경 작업이 수행 중이며 아직 완료되지 않은 상황에서 다른 트랜잭션이 그 트랜잭션의 상태를 읽지 못하도록 하는 격리 수준이 READ COMMITTED 이다.

트랜잭션이 READ COMMITTED 격리 수준에서 수행 중일 때, 이 트랜잭션이 테이블에 추가/수정/삭제를 수행하면 이 변경작업에 해당하는 행(row)들은 잠긴다(lock). 따라서 다른 트랜잭션이 이 들 행에 접근하는 것은 블럭(block)되며 원래 트랜잭션이 종료(COMMIT 혹은 ROLLBACK)될 때까지 기다리게 된다. READ UNCOMMITTED 격리 수준은 트랜잭션이 아직 완료되지 않았더라도 트랜잭션의 중간 상태를 읽을 수 있다. 비록 다른 트랜잭션이 INSERT/UPDATE/DELETE 작업에 의해 행을 잠그더라도 그것에 얽매이지 않고 행을 읽어 낸다는 말이 되겠다(NOLOCK 혹은 READUNCOMMITTED 힌트를 쓰는 것과 동등하다). 하지만 READ UNCOMMITTED 격리 수준일지라도 다른 트랜잭션이 수정하고 있는 행을 수정할 수는 없다(당연 빤쑤다. 격리 수준 이름을 보더라도 읽기에만 관여 되었다는 걸 눈치 까야할 것이다).

READ COMMITTED 격리 수준은 INSERT/UPDATE/DELETE 에 대해 해당 행이 잠기므로, 여러 트랜잭션이 동시에 하나의 행을 액세스할 때에는 아무래도 동시성(concurrency)가 떨어지게 마련이다(다른 넘이 끝나기를 기다리는 경우가 많이 발생한다는 얘기이다). 반면에 READ UNCOMMITTED는 SELECT 작업에 다른 트랜잭션의 INSERT/UPDATE/DELETE 영향을 받지 않기 때문에 동시성은 매우 높아지게 된다. 반면 트랜잭션의 무결성은 훼손될 가능성이 높다.

READ COMMITTED 격리 수준은 가장 무난한 격리 수준으로서 SQL Server와 Oracle의 디폴트 트랜잭션 격리 수준이다. 한편, SQL Server는 READ UNCOMMITTED 격리 수준을 지원하지만 Oracle은 READ UNCOMMITTED 격리 수준을 지원하지 않는다는 점을 기억할 필요가 있다. Oracle은 커밋 되지 않는 데이터의 읽기를 허용하지 않으며 항상 커밋 된 데이터만을 읽을 수 있다.

뭐 여기까지는 데이터베이스 관련서적이나 여타 인터넷 자료에서 등장하는 내용으로서 새삼스러울 것도 없다. 이렇게 내용만 달랑 읽고 넘어가면 삘(feel)이 오지 않으므로 직접 테스트를 해 봄으로써 정말 내 지식으로 만들어 보자.

READ COMMITTED Isolation Level Test

이 글에서 보여주는 모든 테스트는 SQL Server에 대한 테스트이다. 오라클에서도 동등한 테스트를 수행할 수 있지만 수행 결과가 동일하지는 않을 것이다. 이 글의 테스트는 트랜잭션 격리 수준에 대한 모든 사항을 보여주기 위한 테스트가 아니다. 이 글의 테스트에서 독자들이 직접 테스트 해보기 위한 아이디어를 제공하는 수준으로 이해해 주면 좋겠다.

테스트를 위해 테스트용 테이블을 하나 맹글자. Northwind 나 기타 예제 데이터베이스를 써도 좋지만, 간단하게 테스트하고자 함이니 이해바란다. 반드시 이 테이블로 테스트해야 하는 것은 아니다. 독자 꼴린대로 테이블을 선택해도 좋다. 앞으로 이 글에선 이 테이블을 계속 사용할 것이다.

CREATE TABLE [TempTable] (
    [PK] [
intIDENTITY (11NOT NULL ,
    [Data1] [
intNULL ,
    [Data2] [
varchar] (64NULL ,
    
CONSTRAINT [PK_TempTable] PRIMARY KEY  CLUSTERED 
    
(
        [PK]
    )  
ON [PRIMARY
ON [PRIMARY]
GO

INSERT INTO TempTable(Data1, Data2) VALUES(1'DATA 1')
INSERT INTO TempTable(Data1, Data2) VALUES(2'DATA 2')
INSERT INTO TempTable(Data1, Data2) VALUES(3'DATA 3')
INSERT INTO TempTable(Data1, Data2) VALUES(4'DATA 4')
INSERT INTO TempTable(Data1, Data2) VALUES(5'DATA 5')

쿼리1. 테이블 생성 스크립트

이제 Query Analyer (쿼리 분석기)를 수행시키고 2개의 세션을 연다(CTRL-N 핫키를 쓰면 편하다). 그리고 두 세션에 다음과 같이 명령을 주어 트랜잭션 격리 레벨을 READ COMMITTED로 설정한다. 뭐 이 설정이 기본이므로 특별하게 설정하지 않아도 된다.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

이제 한 세션(A라고 지칭하기로 한다)에서 트랜잭션을 시작하고 데이터를 수정해 보자. ROLLBACK을 수행하지 않았음(커맨트 처리)에 유의하자. 이렇게 함으로써 트랜잭션이 종료되는 것을 막고 잠금이 어떻게 진행되는지 살펴볼 수 있다.

BEGIN TRAN

UPDATE 
TempTable SET DATA1=9999 WHERE PK 1

-- ROLLBACK TRAN

쿼리2. 업데이트 스크립트

이렇게 했으니 다른 세션(B라고 지칭하기로 한다)에서 세션 A가 수정한 행을 SELECT 해보도록 한다.

BEGIN TRAN

SELECT 
FROM TempTable WHERE PK 1

-- ROLLBACK TRAN

쿼리3. 조회 스트립트

세션 B가 어떻게 수행되는가? 아마도 아무런 응답이 없이 지구본만 조뺑이 치면서 돌고 있을 것이다. 즉, 커밋되지 않은 세션 A의 트랜잭션을 읽고자 했으므로 잠금에 의해 블록된 것이다. 세션 A의 트랜잭션이 종료(커밋 혹은 롤백)되는 시점에서 잠금이 풀리므로 그 시점에 세션 B의 트랜잭션의 블록 역시 풀리게 될 것이며 세션 A의 트랜잭션이 커밋, 롤백 결과에 따라 다른 결과값을 보이게 될 것이다.

실제로 세션 A를 롤백 해보자. (ROLLBACK TRAN 문장만 마우스나 커서키로 선택하고 F5 키를 누른다) 세션 A가 롤백 하자마자 세션 B는 SELECT 된 결과를 표시할 것이다.

지금의 테스트는 SQL Server를 대상으로 한 것이였다. 이 테스트를 오라클에서 동등하게 수행해보면 사뭇 다른 결과가 나온다. SQL Server 의 경우 커밋되지 않은 데이터를 읽으려고 시도하면 잠김에 의해 세션 B가 블록되지만 오라클에서는 세션 B가 블록되지 않고 수정되기 전의 데이터를 순순히 읽어 왔다. 즉, 세션 B가 SELECT 문을 수행하면 블록없이 DATA1 컬럼의 값을 1 로써 읽어오더라는 것이다. 이것이 READ COMMITTED 격리 수준을 위반한 것이라고 할 수는 없다. 커밋되지 않은 데이터를 읽어 온 것이 아니기 때문이다. 하지만 세션 B가 블록 되어야 하는지 말아야 하는지는 필자도 정확히 말할 수 없다. 오라클 설정에 따라서 세션 B가 블록되거나 블록되지 않을 터인데... 상세한 것은 오라클 전문가에게 물어보기 바란다. 다시 한번 말하지만 필자는 오라클 전문가가 아니다... (사실 필자는 오라클을 좋아 하지 않는다... 쓰봉)

READ UNCOMMITTED Isolation Level Test

겨우 READ COMMITTED 밖에 설명 안 했는데... 벌써 피 냄새가 난다... 무쟈게 길어질 것 같은 이 기분... -_-

READ UNCOMMITTED 테스트를 수행해 보자. 이전 테스트와 동일하게 두 개의 세션을 열고 A 세션에 쿼리 2 와 같은 업데이트를 수행한다. 물론 트랜잭션의 커밋이나 롤백은 아직 수행하지 않는다. A 세션의 트랜잭션 격리 수준은 어떤 것이든 상관 없다. 기본 READ COMMITTED 를 사용하는 것으로 하자.

이제 세션 B에서 다음과 같은 문장으로 트랜잭션 격리 수준을 READ UNCOMMITTED로 낮춘다.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

그리고 나서 쿼리 3 을 수행해 본다. 결과가 어떠한가? READ COMMITTED 격리 수준과 달리 세션 B는 블록되지 않으며 곧바로 결과를 표시할 것이다. 그리고 그 결과값도 현재 세션 A에서 업데이트한 DATA1 컬럼값이 9999로 표시될 것이다. READ UNCOMMITTED 격리 수준이 트랜잭션의 중간 상태 값을 읽을 수 있다는 것을 보여주고 있다. 세션 A를 롤백하고 세션 B에서 조회를 다시 수행해 보자. 이번에는 DATA1 컬럼의 값이 1로 표시될 것이다. 세션이 블록되지 않기 때문에 동시성은 높아질 것이지만 트랜잭션의 무결성이 훼손될 가능성은 높아진다는 것을 다시 한번 머리속에 떠올려 보자. (무슨 수능 강의하는 기분이다... -_-)

시간이 되면 세션 A에서 업데이트를 수행하는 도중에 세션 B에서도 업데이트를 수행해 보아라. 틀림없이 세션 B가 블록될 것이다. 이는 READ UNCOMMITTED 격리 수준일 지라도 다른 트랜잭션이 수정하고 있는 행에 대해서 수정을 가할 수 없음을 알 수 있을 것이다.

오라클은 READ UNCOMMITTED 격리 수준을 지원하지 않는다. ODP.NET 이나 Managed Data Provider Oracle을 사용해서 트랜잭션을 수행하고 IsolationLevel 값을 ReadUncommitted 로 설정하면 예외가 발생한다. 오라클이 READ UNCOMMITTED를 지원하지 않기 때문이다. SQL Server는 트랜잭션 격리 수준이 READ UNCOMMITTED가 아니더라도 NOLOCK 힌트나 READUNCOMMITTED 힌트를 사용할 수 있지만 오라클은 그렇지 않다. 오라클은 커밋되지 않은 데이터(오라클 문서에보면 Dirty Read라고도 표기되어 있다)를 읽지 못하도록 하고 있다.

REPEATABLE READ Isolation Level

READ COMMITTED 격리 수준은 다수의 사용자가 사용하는데 동시성이나 잠금에서 큰 문제를 유발하지 않는다. 하지만 때때로 트랜잭션은 동일한 데이터를 반복해서 여러 번 읽어야 할 때가 있다. 이러한 경우가 발생하면 READ COMMITTED 격리 수준은 반복할 수 없는 읽기(unrepeatable read) 문제를 발생한다. 즉, 한 트랜잭션 내에서 SELECT 를 여러 번 수행했을 때 그 결과가 달라질 수 있다는 문제이다. READ COMMITTED는 다른 트랜잭션이 커밋 한 결과를 읽는데 아무런 제약이 없기 때문이다. 항상 그렇지는 않지만 트랜잭션에 따라서 일관성을 유지하기 위해 트랜잭션 내에서 읽은 데이터는 다음에 읽더라도 동일한 결과를 내어야 하는 경우가 발생하곤 하기 때문이다. 트랜잭션 하면 항상 예제로 등장하는 은행 계좌를 재탕해 보면, 트랜잭션에서 한 계좌의 잔액을 읽어 어떤 연산을 수행한 후에 또 다른 연산을 수행하기 위해 동일한 계좌를 읽고자 할 때, 다른 트랜잭션이 잔액을 수정해 버리면(예금이든 출금이든) 두 번째 읽기의 결과는 첫번째와 다를 것이며 이것은 트랜잭션의 일관성을 위배하는 것이 될 것이다. 뭐 아싸리 말해 누가 무식하게 동일 SELECT를 두 번 하겠냐고 반문하는 독자도 있을 것이다.그러나 조금만 생각해 보면 이렇다. COM+ 컴포넌트에서 데이터(잔액을 생각해보자)를 읽었고 그것을 로컬 변수에 넣어 두었다고 가정해 보자.그리고 트랜잭션이 아직 종료되지 않은 상태에서라면 컴포넌트는 데이터가 변경되었다고 가정하지는 않을 것이다(당연!). 이는 트랜잭션이 완료되지 않은 시점에서 동일 데이터를 다시 읽더라도 동일 값이 나와야 한다는 말과 일맥 상통하는 것이다. 반복할 수 없는 읽기 문제는 트랜잭션을 지원하는 3계층 컴포넌트 기반 어플리케이션에서 컴포넌트가 트랜잭션을 일관되게 수행하기 위해서 반드시 해결해야 하는 문제 중 하나이다.

반복할 수 없는 읽기(Unrepeatable read) 문제를 해결하기 위한 트랜잭션 격리 수준이 REPEATABLE READ 격리 수준이다. 이 격리 수준이 사용되면 SELECT 문장도 테이블의 행에 잠금을 수행한다. 이 잠금은 소위 말하는 공유 잠금(shared lock)을 말하며 읽기 잠금이라고도 하는 잠금으로써, 공유잠금이 걸려있는 자원(행, 테이블, 페이지 등)은 다른 트랜잭션이 공유 잠금을 거는 것은 허용하지만 배타적 잠금(exclusive lock)을 거는 것은 금지한다. 좀더 삘(fill ? no! feel ! -_-)이 오는 말로 하자면, 공유잠금이 걸린 자원은 다른 트랜잭션이 읽을 수는 있지만 수정할 수 없다는 말이다. 트랜잭션에서 반복적으로 읽기를 수행했을 때 항상 동일한 결과가 나오도록 하는 방법은 다름아닌 다른 트랜잭션이 해당 자원을 수정하지 못하도록 하면 되는거 아닌가?

REPEATABLE READ 격리 수준의 트랜잭션은 트랜잭션 내에서 수행하는 SELECT 문이 읽는 행들에 대해 공유 잠금을 수행함으로써 다른 트랜잭션이 이들 행을 수정하는 것을 막음으로써 트랜잭션 내에서 반복적으로 동일한 SELECT가 수행될 때 동일한 결과를 얻게 해준다(repeatable 한 읽기를 보장한다). 이 격리 수준은 트랜잭션이 일관성을 보다 높게 유지하는데 도움이 된다. 반면 이 격리 수준은 한 단계 낮은 READ COMMITTED 격리 수준에 비해 동시성(Concurrency)이 떨어진다. REPEATABLE READ 격리 수준의 트랜잭션이 읽은 데이터를 다른 트랜잭션이 수정할 수 없기 때문에 아무래도 동시성이 떨어지는 것은 대단히 당연하다고 할 수 있다. 이 세상에 공짜가 있던가? 장점이 있으면 단점도 있는법...

 SQL Server는 REPEATABLE READ 격리 수준을 지원하지만 오라클은 지원하지 않는 격리 수준임을 기억해 둘 필요가 있다. 오라클이 왜 이 격리 수준을 지원하지 않는지는 필자도 알 수 없다. 다만 오라클이 다음 설명할 SERIALIZABLE 격리 수준을 지원하므로 반복할 수 없는 읽기 문제를 SERIALIZABLE 격리 수준에서 해결하도록 하고 있다는 점만 알아 두자. (트랜잭션 격리 수준에 대해 설명한 오라클 온라인 문서를 읽어 보라. 어디냐고? 필자도 겨우 찾아 봤다. 필자가 찾을 수 있다면 여러분도 찾을 수 있을 것이다. 찾아 봐라... -_-)

Reproduce unrepeatable read problem

반복할 수 없는 읽기(unrepeatable read) 문제를 재연해 봄으로써 REPEATABLE READ 격리 수준이 어떤 것인가를 이해하기 더 용이하다. 그래서 READ COMMITED 격리 수준에서 어떻게 반복할 수 없는 읽기 문제가 발생하는지 살펴 보도록 하겠다.

앞서 테스트와 동일하게 두 개의 세션을 연다. 그리고 두 세션 모두 READ COMMITTED 격리 수준으로 설정한다. 그리고 세션 A에서 쿼리3를 수행한다. 세션 A에서 트랜잭션이 완료되지 않도록 주의한다. 이제 세션 B에서 쿼리4을 수행하자.

BEGIN TRAN

UPDATE 
TempTable SET DATA1=9999 WHERE PK 1

COMMIT TRAN

쿼리4. 업데이트 쿼리 및 트랜잭션 COMMIT

COMMIT 이 수행되어 세션 B의 트랜잭션을 완료되었음에 유의하자. 다시 세션 A로 돌아와 SELECT 문장만을 선택하여 조회를 수행해보자(요거시 반복 읽기가 되겠다). 결과가 어떻게 나오는가? 세션 B에서 수행한 변경이 세션 A의 트랜잭션 내에서 보일 것이다. 이는 세션 A가 트랜잭션을 완하지 않은 상태에서 첫번째 SELECT와 두번째 SELECT의 결과가 다르게 나타나는, 이른바 Unrepeatable Read 문제가 발생한 것이다.

사실 이 테스트는 반복할 수 없는 읽기 문제를 재현했을 뿐이며 실무에서 발생할 수 있는 반복할 수 없는 읽기 문제는 다양한 SQL Server에 관련된 도서에서 구체적인 예를 찾아 보기 바란다. 다시 한번 말하지만... 필자는 데이터베이스 관련 전문가가 아니다... (메렁~~ -_-)

REPEATABLE READ Isolation Level Test

이제 REPEATABLE READ 격리 수준을 테스트 해 보자. 앞서 Unrepeatable read 테스트와 동일하게 테스트를 수행하되, 세션 A의 격리 수준을 REPEATABLE READ로 설정할 것이다. 먼저 이전 테스트와 동일하게 세션 두 개를 연다. 그리고 세션 A에 트랜잭션 격리 수준을 REPEATABLE READ로 설정한다.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

이제 세션 A에서 쿼리3을 수행한다. 여전히 트랜잭션이 아직 종료(COMMIT 혹은 ROLLBACK)되지 않았음에 유의하자. 그리고 세션 B에서 앞서와 마찬가지로 쿼리4를 수행해 보자. 결과가 어떻게 나오는가? 아마 세션 B에서는 수행 결과가 나오지 않을 것이며 쿼리는 계속 수행 중으로 표시될 것이다. 그 이유는 세션 A의 REPEATABLE READ 격리 수준에 의해 해당 ROW에 잠금이 발생했고 이 잠금에 의해 세션 B의 업데이트 문장이 블록되었기 때문이다. 세션 A에서 SELECT 문자을 반복해서 여러번 수행하더라도 항상 같은 결과가 반환됨을 확인하자. 그리고 세션 A의 트랜잭션을 종료(COMMIT 혹은 ROLLBACK)해 보자. 세션 A의 트랜잭션이 종료함과 동시에 세션 B의 블록이 풀리면서 역시 1개의 행이 업데이트 되었음을 알리는 메시지가 나타날 것이다. 이것은 세션 A의 트랜잭션이 종료하면서 행의 잠금을 풀게 되고 이로 인해 세션 B가 수행되어 업데이트가 수행되었기 때문이다. 세번째 세션 C를 열어보자. 그리고 쿼리4를 수행하되 WHERE 절을 바꾸어 PK = 2 가 되도록 바꾼 후에 수행해 보자. 세션 C가 블록되는가? 그렇지 않다. 세션 C는 세션 A가 잠금을 건 행이 아닌 다른 행을 업데이트하고자 했으므로 블록 없이 곧바로 업데이트가 완료된다.

또 세션 C에서 쿼리3을 수행해보자. 이 역시 블록 없이 잘 수행되는데, 이는 세션 A가 수행한 잠금이 읽기를 허용하는 공유 잠금임을 확인할 수 있다. 실제로 세션 A가 쿼리 3을 수행한 직후에 TempTable과 연관된 잠금을 살펴보면 특정 키 (PK=1)에 대한 공유잠금(shared lock)이 걸려있음을 SQL Server 엔터프라이즈 관리자를 통해 확인해 볼 수 있다.

SERIALIZABLE Isolation Level

트랜잭션 격리 수준의 가장 높은 격리 수준인 SERIALIZABLE 격리 수준은 REPEATABLE READ 격리 수준이 발생할 수 있는 유령 데이터(Phantom data) 문제를 해결하기 위한 격리 수준이다. 유령 데이터? 이게 무슨 말이고 이것을 SERIALIZABLE 격리 수준이 어떻게 해결하는가를 차근 차근 살펴보자.

유령 데이터(Phantom Data)란 트랜잭션 내에서 분명히 수정했음에도 불구하고 수정하지 않은 것 처럼 보이는 데이터나 트랜잭션 내에서 삭제 했음에도 삭제되지 않은 것 처럼 보이는 데이터를 말한다. 이것이 가능할까? 필자가 구라를 치는 것이 아니라면 가능할 것이다. 구체적으로 예를 들어 보자. 트랜잭션 안에서 DELETE 문장을 수행하여 DATA1 컬럼의 값이 1인 행들을 모두 삭제했다. 그리고 나서 DATA1 컬럼의 값이 1인 행들을 조회한다고 가정해 보자. 그때 떡하니 DATA1 컬럼의 값이 1인 행들이 나타날 수 있다. 이것이 가능한 이유는 DELETE 문장과 SELECT 문장 사이에서 다른 트랜잭션이 DATA1 컬럼의 값을 1로 하는 INSERT 문장을 수행할 수도 있기 때문이다. 유령 데이터는 트랜잭션의 일관성을 해치는 문제로 작용할 수 있다. 트랜잭션에서 어떤 데이터들의 개수(행의 개수)를 읽어 다른 테이블에 기록하고자 할 때, SELECT 문장과 UPDATE 문장 사이에 다른 트랜잭션이 INSERT를 수행하는 문제가 발생할 수도 있다. 이 때 트랜잭션의 입장에서 봤을 때 다른 트랜잭션이 INSERT 한 데이터는 자기가 분명히 SELECT 해서 읽어온 데이터에는 보이지 않는 유령 데이터가 되어 버리는 것이다.

이러한 유령 데이터 문제를 해결하는 트랜잭션 격리 수준이 SERIALIZABLE 격리 수준이다. 이 격리 수준은 REPEATABLE READ 보다 높은 격리 수준이며 트랜잭션의 일관성 유지를 위해 보다 빡신 잠금을 수행한다. 당연하게도 SERIALIZABLE 격리 수준은 트랜잭션의 동시성이 REPEATABLE READ 보다 더욱 떨어진다. 대신 트랜잭션의 일관성은 매우 높게 유지해 준다는 장점이 있다.

SERIALIZABLE 이 수행하는 잠금은 소위 말하는 KEY 잠금이다. 오라클에서 이 KEY 잠금을 지원하는지는 필자도 모르겠다. 적어도 SQL Server는 KEY 잠금을 지원한다. SERIALIZABLE 격리 수준에서의 KEY 잠금은 엄밀히 말해 인덱스에 대해서 잠금을 수행하는 것이다. SERIALIZABLE 격리 수준의 트랜잭션 내에서 수행되는 SELECT/INSERT/UPDATE/DELETE 문장의 WHERE 절에 의해 선택되는 인덱스의 범위에 대해 잠금을 수행하여 이 잠금의 결과로 다른 트랜잭션은 이 인덱스 범위에 대한 INSERT/UPDATE/DELETE는 제약을 받게 되는 것이다.

예를 들어보자. SERIALIZABLE 격리 수준의 트랜잭션이 SELECT * FROM TempTable WHERE DATA1 = 1 인 쿼리를 수행했다고 가정해 보자. 이 때 SQL Server는 DATA1 = 1 을 만족하기 위해 선택된 인덱스의 범위에 대해 공유잠금(SELECT 이므로!)을 수행한다. 인덱스가 공유잠금에 의해 잠겼으므로 DATA1 = 1을 만족하는 모든 행들에 대한 다른 트랜잭션의 업데이트는 물론이요 INSERT/DELETE 까지 제약을 받게 되는 것이다. 반면 이들 데이터에 대한 다른 트랜잭션의 SELECT는 제약을 받지 않는다.

Phantom Data Test

먼저 유령데이터가 어떻게 발생할 수 있는지 직접 테스트해 보자. 명확한 테스트를 위해 다음 쿼리5를 사용하여 테이블을 다시 생성하고 데이터 역시 2000 개를 추가해 보자. 이 스크립트는 인덱스도 생성하고 있는데 이 인덱스의 의미는 나중에 다시 설명하기로 한다. 일단 예제 대로 따라서 해보기 바란다.

-- 테이블 제거
DROP TABLE TempTable
GO

-- 테이블 생성
CREATE TABLE 
[TempTable] (
    [PK] [
intIDENTITY (11NOT NULL ,
    [Data1] [
intNULL ,
    [Data2] [
varchar] (64NULL ,
    
CONSTRAINT [PK_TempTable] PRIMARY KEY  CLUSTERED 
    
(
        [PK]
    )  
ON [PRIMARY
ON [PRIMARY]
GO

-- 인덱스 생성
CREATE 
  INDEX [IX_Data1] ON [dbo].[TempTable] ([Data1])
GO

-- 데이터 생성
BEGIN TRAN

DECLARE 
@I as integer
DECLARE @Data1 as integer
DECLARE @Data2 as varchar(64)

SET @I 1
WHILE ( @I < 1000BEGIN
    SET 
@Data1 @I
    
SET @Data2 '데이터 ' CONVERT(varchar(32),@Data1 + 1000)
    
INSERT TempTable(Data1, Data2) VALUES(@Data1, @Data2)
    
SET @I @I + 1
END

SET 
@I 1
WHILE ( @I < 1000BEGIN
    SET 
@Data1 @I
    
SET @Data2 '데이터 ' CONVERT(varchar(32),@Data1)
    
INSERT TempTable(Data1, Data2) VALUES(@Data1, @Data2)
    
SET @I @I + 1
END

COMMIT TRAN

쿼리 5. 테이블 다시 생성 및 데이터 생성 스크립트

언제나처럼 세션 2개를 열자. REPEATABLE READ 격리 수준에서 발생할 수 있는 유령 데이터를 테스트 하기 위해 세션 A에 트랜잭션 격리 수준을 REPEATABLE READ로 설정한다.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

그리고 다음 쿼리6을 수행한다. 쿼리6이 아직 트랜잭션을 종료(COMMIT 혹은 ROLLBACK) 하지 않았음에 유의하자. 쿼리6의 결과는 DATA1 컬럼의 값이 1인 2개의 데이터가 DATA2 칼럼의 값이 XXX로 설정되어 조회될 것이다.

BEGIN TRAN

UPDATE 
TempTable SET DATA2 'XXX' WHERE DATA1 1

SELECT FROM TempTable WHERE DATA1 1

-- ROLLBACK TRAN

쿼리 6. 업데이트 후 조회 쿼리

이제 세션 B에서 INSERT 를 수행해 보자. 세션 B의 트랜잭션 격리 수준은 무엇이든 상관 없다. 세션 B에서 쿼리7을 수행해 보자. 결과가 어떤가? INSERT는 아무런 문제 없이 수행될 것이며 트랜잭션 역시 COMMIT이 잘 된다. 세션 A의 트랜잭션이 아직 종료되지 않았음에도 INSERT는 수행된다. 물론 DATA1 = 1인 행들에 대해 UPDATE를 수행해보면 세션 B는 블럭될 것이다.

BEGIN TRAN

INSERT INTO 
TempTable(DATA1, DATA2) VALUES(1'ZZZ')

COMMIT TRAN

쿼리 7. 데이터 추가 쿼리

쿼리7이 수행된 후에 세션 A로 돌아가서 SELECT 문장만을 선택하여 수행해보자. 분명 이전 UPDATE 문장에서 2개를 업데이트 했고, 그 다음 SELECT 를 했을 때도 2개가 조회되었었다. 하지만 세션 B에서 트랜잭션이 수행된 이후 쌩뚱맞게도 트랜잭션이 종료되지 않았는데도 SELECT의 결과는 3개가 나온다. 그리고 새로이 나타난 데이터는 바로 세션 B에서 수행된 INSERT의 결과다.

이렇게 트랜잭션에서 유령같이 나타나는 데이터를 유령 데이터(Phantom data)라고 하며, REPEATABLE READ 격리 수준으로는 유령 데이터 문제를 해결할 수 없다.

SERIALIZABLE Isolation Level Test

이제 SERIALIZABLE 격리 수준에 대해 테스트 해보자. 명확한 테스트를 위해 테이블을 지우고 다시 생성하는 것이 좋다. 쿼리5를 다시 수행시켜서 기존 테이블을 날리고 새로이 생성하자. 이전 테스트와 동일하게 세션 2개를 열고 세션 A의 격리 수준을 SERIALIZABLE 로 설정하자.

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

이제 이전 유령 데이터 테스트에서 수행했던 테스트를 동일하게 반복해 보자. 먼저 쿼리 6을 수행시켜 세션 A가 SERIALIZABLE 격리 수준에서 UPDATE를 수행하도록 한다. 물론 트랜잭션은 아직 종료되지 않은 상태로 두어야 한다.

세션 B에서는 쿼리 7을 수행시킨다. 쿼리 7이 수행되는가? 세션 B는 블록되어 결과가 나오지 않을 것이다. 세션 A가 SERIALIZABLE 격리 수준에서 수행중이기 때문에 일련의 인덱스의 범위에 대해 잠금을 수행한다. 즉, DATA1 = 1 을 만족하는 인덱스에 대해 잠금을 수행하기 때문에 쿼리 7이 수행하는 DATA1 = 1 인 새로운 데이터 INSERT 작업 역시 블로킹된다는 말이 되겠다. 이로 인해 유령 데이터는 발생하지 않는다. 세션 A의 트랜잭션이 종료되면(COMMIT 혹은 ROLLBACK), 그제서야 세션 B의 수행이 완료될 것이다.

한가지 테스트를 더 해보자. 세션 A에서 쿼리 6을 수행시킨 다음, 세션 B에서 쿼리 7을 약간 수정하여 DATA1 의 값을 2로 설정해 보도록 하자.

BEGIN TRAN

INSERT INTO 
TempTable(DATA1, DATA2) VALUES(2'ZZZ'-- 추가할 DATA1 컬럼의 값이 2 임에 유의

COMMIT TRAN

쿼리 8. 데이터 추가 쿼리

그리고 세션 B에서 수정된 쿼리 8을 수행하면 어떻게 될까? 결과는 세션 B가 블로킹되지 않고 아무런 제약 없이 쿼리 8을 수행해 낼 것이다. 왜 그럴까? 쿼리 8은 DATA1 컬럼의 값을 1이 아닌 2로 하여 INSERT를 수행한다. 이는 세션 A의 트랜잭션이 잠근 인덱스가 DATA1 = 1 을 만족하는 인덱스만을 잠궜기 때문이다. DATA1 = 2 인 인덱스는 잠겨 있지 않으므로 세션 B의 트랜잭션이 곧바로 수행된 것임을 잘 알아야 한다. SERIALIZABLE 격리 수준이 무분별하게 데이터들을 잠궈서 동시성을 무식하게 저하시키지 않는다는 것을 보여주는 테스트가 되겠다.

Consideration

SERIALIZABLE 격리 수준을 위해 KEY 잠금이 사용된다고 했고 KEY 잠금이란 것이 인덱스의 범위에 대한 잠금이라고 했다. 그렇다면 만약, 적절한 인덱스가 잡혀 있지 않다면? 테이블 전체에 잠금이 생기는 경우도 발생하곤 한다. 오~쉣 !!! 무슨 개떡 같은 말인가? 흥분하지 말고 천천히 생각해 보자. SERIALIZABLE 격리 수준은 반복할 수 있는 읽기와 유령 데이터 방지를 위해 인덱스에 대해 잠금을 수행한다. 하지만 잠금을 위해 해당 인덱스를 적절히 선택할 수 없게 되면 PK에 의한 인덱스나 다른 인덱스를 사용하게 된다. 이때 SELECT 문장이나 UPDATE 문장을 위해 수행하는 인덱스 SCAN, 인덱스 SEEK에 의해 검색되는 인덱스가 죄다 잠기게(lock)되는 것이다.

잘 이해가 안갈테니 테스트를 해보자. 지금까지 테스트 했던 TempTable에는 PK에 의한 기본 인덱스와 DATA1 컬럼에 대한 인덱스가 잡혀 있다. 이중 DATA1에 잡혀 있는 인덱스를 제거해 보자.

DROP INDEX TempTable.IX_Data1

그리고 나서 언제나처럼 세션 두 개를 연다. 세션 A의 격리 수준을 SERIALIZABLE로 설정하고 쿼리 6을 수행하자. 세션 B에서는 쿼리 8을 수행한다. 쿼리8은 쿼리6이 수정하는 DATA1=1 인 행을 추가하는 것이 아닌 DATA1=2인 행을 추가하고 있다. 그리고 이전 테스트에서 쿼리8은 아무런 블로킹 없이 수행되었었다.

이제는 어떤가? DATA1 컬럼에 걸려있던 인덱스를 제거한 후에 수행되는 세션 B의 쿼리 8은 여지없이 블로킹되고 만다. 그 이유가 뭘까? 세션 A에서 업데이트가 수행되면 DATA1 컬럼에 대한 인덱스가 존재하지 않기 때문에 다른 인덱스 중에서 가장 적절한 인덱스를 찾는다. 이 경우에는 PK 컬럼에 존재하는 인덱스를 사용하게 되고 인덱스를 스캔하게 된다. DATA1 컬럼에 대한 인덱스가 존재하지 않기 때문에 전체 테이블 스캔이 발생하고 그 결과로 모든 PK 인덱스에 대한 잠금이 발생하고 결과적으로 테이블 전체가 잠기는 결과가 나온다. 따라서 세션 A의 트랜잭션이 종료될 때까지 TempTable에 대한 모든 SELECT/INSERT/UPDATE/DELETE는 블로킹되게 된다(뭐, 쿼리에 NOLOCK 힌트나 READ UNCOMMITTED 격리 수준이라면 블로킹 되지는 않을 것이다). 방금의 테스트에서 쿼리6이 UPDATE가 빠진 SELECT 만을 수행하더라도 적절한 인덱스가 잡혀 있지 않다면 INSERT/UPDATE/DELETE는 수행되지 않는다(SELECT는 수행될 것이다. 공유 잠금이기 때문에...).

SERIALIZABLE 격리 수준은 인덱스의 범위에 대해 잠금을 수행하기 때문에 이 격리 수준에서 발생하는 쿼리들이 적절한 인덱스를 사용하지 않으면 테이블 스캔이 발생하고 이로 인해 전체 테이블이 잠기는 결과를 낳기도 한다. 항상 이점을 조심하지 않으면 대략 낭패가 된다.

SERIALIZATION Isolation Level in Oracle

오라클은 READ COMMITTED 격리 수준과 SERIALIZABLE 격리 수준만을 지원한다. 오라클이 왜 REPEATABLE READ를 지원하지 않는지는 필자도 정확히 알 수 없지만, 추측컨데 REPEATABLE READ를 사용하느니 SERIALIZABLE 격리 수준을 사용하는 경우가 더 많기 때문이 아닐까 싶다. 사실 필자의 경우도 REPEATABLE READ 격리 수준을 사용해 본 경험이 거의 없다. 어찌 되었건 앞서 SQL Server를 대상으로 한 테스트와 최대한 동등한 테스트를 해 보았다.

먼저, 반복할 수 없는 읽기 문제를 오라클 SERIALIZABLE 격리 수준이 어떻게 해결하는지 살펴보았다. 세션 A의 트랜잭션 격리 수준을 SERIALIZABLE로 설정하고, SELECT * FROM TempTable WHERE DATA1 = 1 쿼리를 수행했다. 그리고 세션 B에서 DATA1=1 인 행들을 UPDATE 해 보았다. SQL Server와는 달리 오라클은 세션 B를 블로킹 하지 않고 업데이트를 수행했다. 그리고 세션 A에서 반복적으로 SELECT 문을 수행하더라도 트랜잭션이 종료(COMMIT 혹은 ROLLBACK)되지 않은 상태에서는 세션 B가 수행한 변경 사항이 보이지 않았다. 즉, REPEATABLE READ가 가능했던 것이다. 차이점은 오라클은 세션 B를 블로킹 하지 않았고 SQL Server는 세션 B를 블로킹 했다. 다른 트랜잭션을 블로킹 하는 것이 맞는지 블로킹 하지 않는 것이 맞는지는 필자도 말하기 힘들다(전문가에게 물어봐라). 하지만 한 가지 명확한 것은 두 데이터베이스가 SERIALIZATION 격리 수준에서 반복할 수 없는 읽기 문제가 발생하지 않는다는 점이다. 뭐 굳이 한마디 더 하자면 동시성 면에서는 오라클의 접근 방법이 더 좋아 보이지만 그만큼 데이터베이스 서버가 복사본을 가지고 있어야 하는 등의 더 많은 작업이 필요하지 않을까 생각해 볼 뿐이다.

다음은 유령 데이터를 테스트 해 보았다. 세션 A의 트랜잭션 격리 수준을 SERIALIZABLE로 설정하고 쿼리6과 같은 업데이트를 수행하였다. 그리고 세션 B에서 쿼리7과 같은 INSERT를 수행해 보았다. 앞서 테스트 처럼 세션 B는 블로킹 되지 않았으며 INSERT가 수행되었다. 하지만 세션 A에서 SELECT를 다시 해 보면 세션 B에서 INSERT된 데이터는 나타나지 않았다. 즉, 유령 데이터 문제가 발생하지 않은 것이다. 이 테스트 역시 오라클은 다른 트랜잭션을 블로킹하지 않으면서 SERIALIZABLE 격리 수준이 요구하는 트랜잭션 격리를 만족시키고 있는 것이다.

마지막으로 오라클의 SERIALIZABLE 격리 수준이 다른 점을 말하자면, 오라클은 다른 트랜잭션을 블로킹하지 않는 경우가 많기 때문에 SERIALIZABLE 격리 수준이 "ORA-08177: 이 트랜잭션에 대한 직렬화 액세스를 할 수 없습니다" 오류를 발생하는 경우가 있다. 이는 트랜잭션의 격리 수준이 SERIALIZABLE 인 경우, 다른 트랜잭션이 수정하고 커밋한 데이터를 수정하고자 하는 경우 발생한다. 보다 구체적인 예를 들어보면, 세션 A의 트랜잭션 격리 수준이 SERIALIZABLE 이고 이 트랜잭션에서 DATA1=1인 데이터를 SELECT 했을 때, 세션 B에서 DATA1=1 인 데이터들을 UPDATE 했다고 가정해 보자. 세션 A에서 SELECT가 진행되면 아직 커밋 되지 않은 상태 이므로 변경된 값이 보이지 않을 것이다. 세션 B가 커밋을 했다면 세션 A의 트랜잭션에서는 여전히 변경된 값이 아닌 이전 값이 될 것이다. 왜냐면 REPEATABLE READ가 가능해야 SERIALIZABLE 격리 수준을 만족하기 때문이다. 이때, 트랜잭션 A가 DATA1=1인 데이터 중 하나라도 수정하고자 하면 ORA-08177 오류가 발생한다. 오라클 개발자 가이드 문서를 읽어보면 이러한 오류 발생시 트랜잭션을 ROLLBACK 하고 재 시도하라는 지침이 있을 뿐이다. 오라클이 데이터 행에 잠금을 수행하지 않고 스냅샷을 이용하여 SERIALIZABLE 격리 수준을 지원하고 있음이 분명하다.

물론 SQL Server는 이러한 직렬화 오류를 유발하지 않는다. 앞서 언급한 ORA-08177 오류 상황을 그대로 SQL Server에 재연해 보면 세션 B의 UPDATE는 블로킹되어 버린다. 왜냐면 오라클은 데이터의 스냅샷을 통해 SERIALIZABLE 격리 수준을 구현하지만 SQL Server는 데이터에 대한 잠금을 통해 트랜잭션 격리를 구현하기 때문이다. 오라클은 동시성이 높은 반면 ORA-08177 같은 오류를 발생해 버리는 반면 SQL Server는 동시성은 상대적으로 낮지만 오류를 발생시키지 않는다. 어느 것이 더 좋은 방법인지는 판단하기 어렵다. SQL Server 2005에서는 스넵샷 트랜잭션 격리 수준이 추가되는 것으로 보아 오라클의 방법도 나쁘지 않지만 ORA-08177 같은 오류가 발생할 가능성이 낮지 않게 때문에 무조건 오라클이 좋다고 말할 수도 없다.

오라클 이건 SQL Server 이건 SERIALIZATION 격리 수준이 두 데이터베이스가 다른 트랜잭션을 블로킹 하는가 그렇지 않은가의 차이가 발생했을 뿐 반복할 수 없는 읽기 문제와 유령 데이터 문제를 해결하고 있는 것은 명확하다. 언틋 보기엔 오라클이 동시성 면에서 더 나아 보이지만, 서로 다른 트랜잭션이 바라보는 데이터가 서로 다른 시점이 발생하므로 오라클 서버가 데이터의 복사본을 유지해야만 할 것 같은 생각이 든다(필자의 생각일 뿐이다). 그래서 어느 데이터베이스가 더 낫다라고 평가할 수는 없고(필자에겐 그럴만한 능력이 읍다...), 다만 개발자로서 데이터베이스 마다 SERIALIZABLE 격리 수준을 사용했을 때 다른 점이 무엇인가 만을 잘 기억하자.

다시 한번 강조하지만 필자는 데이터베이스 전문가가 아닐 뿐 더러, 더우기 오라클은 문외한이다. 오라클 가지고 필자에게 시비거는 일이 없기 간절히 기도한다. (웬 책임 회피냣 !!! 자폭하랏 !!!)

COM+ & Transaction Isolation Level

헥...헥...힘들게 트랜잭션의 격리 수준 4가지에 대해 살펴보았다. 그렇다면, COM+를 통해 분산 트랜잭션이 시작되면 이 트랜잭션의 격리 수준은 무엇일까? 널리 알려진 바(?)와 같이 COM+ 디폴트 트랜잭션의 격리 수준은 SERIALIZABLE 이다. COM+는 미들웨어로서 트랜잭션의 일관성 유지를 위해 가장 높은 격리 수준을 사용한다. 그 이유는 이렇다. COM+ 컴포넌트가 어떤 데이터를 읽어 비즈니스 로직에 의해 이 데이터를 수정하고 다시 업데이트 한다고 가정해 보자. 이 컴포넌트가 데이터를 읽은 후(이 데이터는 당연히 DataSet이나 Recordset과 같은 메모리 형태로 존재할 것이다), 수정을 하는 동안 다른 트랜잭션들이 데이터베이스상의 데이터에 대해 UPDATE를 수행하거나 INSERT를 수행한다면 컴포넌트가 수행하는 트랜잭션의 일관성을 해치는 일이 될 것이다. 3계층 어플리케이션 아키텍처에서 미들웨어는 항상 이러한 트랜잭션의 일관성에 위협을 받기 마련이다. COM+ 컴포넌트를 개발할 때 널리 권장되는 코딩 패턴 중에서 왜 컴포넌트가 상태를 갖지 않도록(stateless) 하며, 또한 JITA(Just-In-Time Activation)를 적극 수용하도록 권장하는가의 이유가 바로 트랜잭션의 일관성 유지하고도 관련이 있다(이에 대한 좋은 설명은 Understanding COM+라는 책을 보면 잘 소개되어 있다. 다만 번역서가 절판이어서 구하기 힘들어서 그렇지... 쩝)

일관성 유지도 좋지만 SERIALIZATION 격리 수준은 REPEATABLE READ 격리 수준과 함께 단순한 읽기(SELECT) 쿼리 마저 행들을 잠그게 된다. 게다가 앞서 고려 사항에서 언급한대로 인덱스를 제대로 잡지 않으면 테이블 전체에 잠금이 발생하기도 한다. 이러한 읽기 쿼리에 대한 광범위한 잠금은 다른 트랜잭션이 INSERT/UPDATE/DELETE 를 수행하는 것 마저 방해할 수 있기 때문에 전체적인 동시성 혹은 병행성을 낮추는 역할을 해 버린다. 쉽게 말해 동시에 여러 사용자가 하나의 행을 읽거나 수정하는 것이 어려워 진다는 것이다. 이는 곧 어플리케이션의 확장성(scalability)을 낮추는 요인일 뿐더러 전체적인 처리량(throughput)을 낮추기도 한다.

대개의 데이터베이스 전문가들은 트랜잭션의 격리 수준을 READ COMMITTED로 수행하고 필요한 경우에만 쿼리상에 힌트를 주거나 트랜잭션 격리 수준을 올리라고 말하고 있다. 하지만 COM+의 디폴트 트랜잭션 격리 수준이 SERIALIZABLE 이므로 COM+ 를 사용할 때는 무언가 조치를 취해주어야 하지 않을까?

Change COM+ Transaction Isolation Level

COM+의 트랜잭션 격리 수준을 바꾸는 것은 매우 쉽다. 단순히 구성 요소 서비스 MMC를 통해 컴포넌트의 속성에서 트랜잭션의 격리 수준을 지정할 수 있다.


화면 1. COM+ 트랜잭션 격리 수준 설정

한가지 기억해야 할 부분은 COM+의 트랜잭션 격리 수준이 트랜잭션을 시작하는 컴포넌트(트랜잭션 루트 컴포넌트)의 설정을 따른다는 점이다. 이 말이 뭔 말인고 하면, 클라이언트가 컴포넌트 A를 호출하고 A가 다시 컴포넌트 B를 호출할 때, A가 READ COMMITTED 이고 B가 SERIALIZABLE 이라면, B 호출은 오류를 발생한다. B 컴포넌트는 A 컴포넌트(트랜잭션 루트)보다 높은 격리 수준을 설정할 수 없다. 이 때문에 COM+에서 제공되는 제3의 격리 수준이 ANY (한글로 '모두'라고 되어 있다. -_-) 격리 수준이다. 이 ANY 격리 수준은 앞서의 예에서 B 컴포넌트 처럼 항상 트랜잭션 루트에 의해 호출되는 컴포넌트를 위해 적절한 설정이라 볼 수 있겠다. ANY 설정의 컴포넌트는 항상 호출자 컴포넌트의 격리 수준을 따르게 되기 때문이다. 만약 트랜잭션 루트 컴포넌트(A 컴포넌트가 되겠다)가 ANY로 설정되어 있다면 이 때는 SERIALIZABLE이 선택되어 지므로 유의해야 할 것이다.

닷넷 코드에서 트랜잭션 격리 수준을 설정하는 것도 가능하다. 사실 닷넷 코드에서의 설정은 닷넷 어셈블리가 COM+에 등록되는 시점에서 COM+ 카탈로그(catalog)에 기록되기 때문에 최종 유효한 값은 구성요소 서비스 MMC에서 보는 내용이지만 코드상에 명기해 놓는 것이 여러모로 편리하므로 가능하면 컴포넌트의 트랜잭션 격리 수준을 명시해 주는 것이 좋다. 일반적으로 사용하는 Transaction 특성(attribute)에 Isolation 속성을 TransactionIsolationLevel 열거자(enumeration) 값들 중 하나로 설정하면 되는 것이다. 말로만 하면 못알아 들으니 예제로... -_-

[Transaction(TransactionOption.Required, Isolation TransactionIsolationLevel.ReadCommitted)]
public class BSLBase : System.EnterpriseServices.ServicedComponent
{
    
// 생략.......
}

COM+의 격리 수준을 설정할 수 있는 것은 아쉽게도 COM+ 1.5 버전부터 가능하다. 즉, COM+ 1.0을 사용하는 Windows 2000에서는 트랜잭션 격리 수준을 SERIALIZABLE에서 변경할 수 없다. (물론 COM+ 1.0의 전신인 Windows NT 상의 MTS 2.0 역시 마찬가지이다). 따라서 COM+ 1.0 기반으로 컴포넌트를 개발할 때는 불필요한 잠금과 블로킹이 발생하지 않도록 쿼리에 신경을 써야하며 필요하다면 힌트를 사용해야 할 것이다.

Recommendation

휴... 이 권고사항을 위해서 지금까지 필자는 그렇게 썰을 푼 것이다(흑흑... 거의 2주일 동안 하나의 포스트에 매달려 있었다는... T_T). COM+를 사용할 때 시스템 아키텍트와 개발자는 항상 트랜잭션 격리 수준을 고려해야만 한다. 필자의 권고 사항은 다음과 같다. 어디까지나 필자의 경험과 여러 자료를 기반으로 한 권고이기 때문에 필자에 동의하지 않거나 잘못된 점이 있다면 지적해 주기 바란다. 블로그 아래에 달려있는 Feedback은 멋으로 있는 것이 아니다. -_-

첫째 권고로, 단순한 조회에 트랜잭션이 걸리지 않도록 주의해야 한다. COM+가 기본으로 사용하는 격리 수준이 SERIALIZABLE 이므로 단순한 SELECT 쿼리 마저 다른 트랜잭션을 블로킹하는 잠금이 발생하기 때문이다. 달랑 한 개 혹은 몇 개의 조회 쿼리를 수행하는 COM+ 컴포넌트에 트랜잭션을 거는 것은 매우 우매한 짓이며 시스템의 성능을 저해하는 지름길이 되는 것이다. 이 권고 사항은 권고 사항 같지 않게 너무도 당연한 것이지만 사실 잘 지켜지지 않는 부분이기도 하다.

둘째 권고는 데이터베이스 전문가들이 권장하듯이 SERIALIZABLE 격리 수준을 사용하지 않는 것이다. Windows 2003을 서버로 사용하고 있다면 반드시 트랜잭션 격리 수준을 명시적으로 주어 READ COMMITTED 격리 수준을 사용하도록 설정하는 것이 좋다. 특별히 유령 데이터나 반복 가능한 읽기가 필요한 트랜잭션을 수행해야 한다면 해당 컴포넌트만이 SERIALIZABLE 격리 수준을 사용하도록 설정한다. 아무런 설정을 하지 않으면 SERIALIZABLE 격리 수준이 사용됨을 명심 또 명심하자. 오라클을 사용할 때도 동시성이 낮지 않더라도 SERIALIZABLE 격리 수준은 ORA-08177 오류를 유발할 수 있으므로 이 오류를 처리해야만 한다. 오라클이건 SQL Server 이건 SERIALIZABLE 격리 수준은 단점을 갖고 있기 때문에 READ COMMITTED 격리 수준을 기본으로 사용하되, 반복적인 읽기(repeatable read)가 필요하거나 유령 데이터를 막고자 하는 경우에만 일시적으로 쿼리에 힌트를 사용하여 SERIALIZABLE 효과만 발생시켜 주는 것이 좋다고 할 수 있겠다.

셋째 권고는 인덱스를 적절히 생성하도록 주의한다. 우리나라와 같이 개발자가 DB, COM+, 웹폼, 윈폼을 죄다 개발하는 상황에서 개발자가 DB의 인덱스까지 신경쓰기 쉽지 않지만 SQL Server의 인덱스 마법사 같은 기능을 십분 활용하여 인덱스를 잡도록 하면 될 것이다. 아무튼 테이블의 인덱스는 매우 중요하다.

넷째로, Windows 2000과 같이 트랜잭션의 격리 수준을 바꿀 수 없는 경우에는 앞서 언급한 바 대로 불필요한 트랜잭션이 없도록 최대한 주의하고 힌트를 이용하여 불필요한 잠금이 발생하지 않도록 한다. 예를 들어 SELECT 문장에 READCOMMITTED 힌트를 주면 조회가 READ COMMITTED 격리 수준에서 작동하는 것과 동일한 효과를 발휘하게 된다. 또, 트랜잭션 격리 수준을 바꿀 수 없어서 항상 SERIALIZABLE 격리 수준이 사용되므로 다시 한번 인덱스에 신경 쓸 필요가 있다.

COM+는 분산 트랜잭션 및 자동 트랜잭션 기능으로 개발자들이 트랜잭션에 신경을 쓰지 않고 비즈니스 로직을 작성할 수 있도록 해준다. 치사하지만 세상엔 공짜는 없드라. COM+를 사용하는 만큼 COM+의 특성을 잘 파악해야만 한다. 잘못된 COM+ 코딩 지침은 전체 프로젝트의 성패를 좌우할 만큼 크게 작용하곤 한다. 성능적인 면에서나 안정성 면에서 COM+ 트랜잭션 격리 수준은 다시 한번 재고해야 할 사항이다. 지금까지 COM+ 트랜잭션 격리 수준에 대해 고려해 보지 않았다면 지금이라도 다시 한번 충분히 고려해 보아야 할 것이다.


정말 빡신 포스트였다. 줴길슨... 8월 말에 시작한 포스트가 이제서야 끝이 나다뉘... 중간에 이사로 인해 몸살이 나서 며칠 블로그에 신경을 못쓴데다가 끝이 보이지 않는 토픽을 잡아 버리는 바람에... -_- 더욱 글 쓰기가 싫어져서... 질...질... 끌다가... 아~흐~

앞으론 내용이 길 것 같으면 아조 시작을 안 해야 긋다... 쯔읍... 내가 이렇게 빡시게 글을 쓴다고 누가 빳빳한 돈을 주거나, 휴가를 주는 것도 아닌데... 가끔 내가 이 짓을 왜 하나 싶기도 하다... 아웅...



Comments (read-only)
#re: About COM+ Transaction Isolation Level / 찌유니아빠 / 2005-09-14 오후 1:56:00
안그래도 한번 부탁해서 볼라고 했던 건데..^^
#re: About COM+ Transaction Isolation Level / 블로그 쥔장 / 2005-09-14 오후 2:10:00
아찌는 역시 내 블로그에서 없어선 안 될 뿌락찌양...
페이지 뷰를 마니 올려준다는... 흐흐흐
자주 와줘서 고마워요... ^^
#re: About COM+ Transaction Isolation Level / 지나가는 나그네 / 2005-10-26 오전 11:45:00
com+ Isolation에 대해서 이해하기 힘들었는데 정리 잘 해주셔서 감사합니다.
오라클을 주로 다루는 개발자 입장에서 Isolation Level을 이해하기 힘들었는데, 많은 도움 받고 갑니다.

개인적 입장에선 오라클의 트랜젝션 처리방법이 훨씬 이해하기 편하다는 느낌이 드는군요.
오라클은 단순합니다.
commit되기전의 data는 다른사용자에게는 이전data를 보여주고
commit이후에는 모든 사용자에게 변경된data를 보여줍니다.
(오라클이 READ COMMITTED라고 하셨는데, lock이 걸려 있어도 다른 사용자들이 이전 data를
select 할 수 있다는 것이 차이점이네요)
이러한 점을 이해한다면 REPEATABLE READ는 오라클에서 지원하지 않는다는 것과
오라클 프로그램시 REPEATABLE READ사용하지 않고 한번 SELECT한 데이타는 변수에 담아
처리해야 한다는 것을 아실 수 있겠죠. (대부분 이렇게 사용하지 않나요?)

오라클에서 commit이나 rollback되기 전의 수정된 data는 세션별로 관리합니다.
(이게 맞는지 기억이 가물가물.)
다른 사용자들은 메모리(sga)영역에 올라온 data를 읽게 되는 거구요.
commit되면 메모리에 올라온 data를 변경하게 되어 그 순간부터 모든 사용자는 같은
data를 보게 되는 겁니다.

오라클 가지고 시비걸지 말라는 쥔장님의 당부에도 불구하고
송구스럽게 한마디 올리고 갑니다.
#re: About COM+ Transaction Isolation Level / 나그네2 / 2006-05-01 오후 9:17:00
추가로...
Read commit 로 다른 트렌젝션이 Update하는 중에 또다른 트렌젝션이 읽기를 할때 이전값고 동일한 update문장의 경우에는 wait없이 바로 읽어버리는 특징도 있더군요..
#re: About COM+ Transaction Isolation Level / 블로그쥔장 / 2006-05-02 오후 5:06:00
정말 그렇군요. 해당 키에 잠금이 발생함에도 불구하고 Update 문장이 이전 값과 같은 값으로 Update를 수행하는 경우
다른 트랜잭션이 block 없이 읽기(SELECT)가 수행되는 군요.
READ COMMITTED 라는 의미를 해치는 것이라 볼 수는 없고... 불필요한 블럭을 줄이고자하는
일종의 최적화(optimization)으로 보입니다. 좋은 정보 군요.

이외에도 SQL 2005는 스냅샷(snapshot) 격리 수준이 추가되었습니다. 이 격리 수준은 오라클의 READ COMMITTED 행동과
매우 유사하게 스냅샷을 뜨는 방식을 취합니다.
#re: About COM+ Transaction Isolation Level / 크릉크릉 / 2008-09-16 오전 10:57:00
정말 정말 잘보았습니다. 앞으로도 좋은글 부탁드립니다.
#re: About COM+ Transaction Isolation Level / H2M / 2008-10-13 오후 12:05:00
이야.. 정말 이해가 잘되네요~
이곳저곳 자료 찾아보면서 무언가 계속 부족했는데.. 확실하게 이해하고 갑니다~
감사해요!
#re: About COM+ Transaction Isolation Level / kuaaan / 2009-01-14 오후 8:56:00
(SQL Server)에서 Update 문장이 이전 값과 같은 값으로 Update를 수행하는 경우
--> 이 경우엔요... 실제로 테스트 해보면... 레코드에 Lock이 발생하지 않습니다.
BEGIN TRAN
UPDATE TABLE TAB_A SET COL_A = 'A' WHERE COL_A = 'A'

EXEC sp_lock @@spid

위의 SQL로 트랜잭션 중에 lock을 확인해보면... 어떠한 레코드에도 락이 걸리지 않음을 알 수 있습니다.
Lock이 걸리지 않으니 SELECT도 됩니다. ^^

#re: About COM+ Transaction Isolation Level / beat203 / 2009-02-03 오후 4:38:00
잘보고 갑니다. 수고하세요...
#re: About COM+ Transaction Isolation Level / lvsin / 2009-02-05 오전 10:43:00
퍼가고싶은글인데..ㅠ_-

이 글이 사라지지 않기를 바래요 ㅠ_ㅠ;;