SimpleIsBest.NET

유경상의 닷넷 블로그
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.
이 글은 월간 마이크로소프트웨어(일명 마소) 2005년 7월호 닷넷 칼럼에 기고한 글입니다. 이 글은 .NET Framework 2.0 Beta2 를 기준으로 작성되었습니다만, 정식 버전이 나온 지금(2006년 8월) 상황에서 이 글의 내용은 정식 버전에도 동일하게 적용됩니다. 약간 눈에 거슬리더라도 베타2라는 말을 2.0 정식 버전으로 생각하셔도 무방합니다.

주) 이 글은 마소 편집부에 제가 발송한 원고를 약간 편집하여 올린 글입니다. 따라서 마소에 실린 글과는 약간의 차이가 있을 수 있습니다.


About System.Transactions

Subtitle : .NET Framework 2.0의 새로운 트랜잭션 네임스페이스, System.Transactions

개발환경은 꾸준히 변해 왔다. IT 환경이 발달해 감에 따라 컴퓨터 응용 프로그램들은 보다 강력한 기능을 요구 받아왔고 이러한 요구사항들을 만족시키기 위해 응용 프로그램 개발 환경 역시 강력하고 다양한 기능을 제공하면서도 보다 높은 개발 생산성을 내는 방향으로 발전해 왔다고 할 수 있다. 필자가 처음 ‘프로그램’ 이란 것을 개발할 때의 PC란 것은 8비트 컴퓨터였다. 2MHz 8비트 CPU, 64KB의 메모리와 360KB의 플로피 디스크, 그리고 BASIC 언어와 매우 간단한 inline 어셈블러(assembler)가 18년 전의 필자의 개발환경 이였다. 하지만 지금 필자가 사용하는 노트북(!)은 2GHz의 32비트 모바일 CPU에 2GB의 메모리 그리고 80GB의 하드 디스크와 DVD RW를 갖고 있으며, 4GB 정도의 설치 용량을 요구하는 통합 개발환경을 갖추고 있다. 실로 눈부신 발전이 아닐 수 없다.

닷넷 프레임워크 1.0이 발표된 지 3년이 지났고 2.0 버전의 베타 2가 나온 지금, 3년간의 짧은 시간 동안 닷넷 프레임워크 기반의 개발 환경 역시 발전을 해왔다. 닷넷 환경의 트랜잭션 처리 환경 역시 2.0에 들어서 큰 변화와 발전(?)을 이루었고 이 발전적인 변화의 결과가 새로운 네임스페이스인 System.Transactions 네임스페이스이다.

이번 칼럼도 필자의 주요 메뉴인 트랜잭션 이야기냐고 물으면 별로 할말이 없지만… 트랜잭션만큼이나 중요한 것도 드물지 않은가? 게다가 이번에 다룰 분야는 닷넷 프레임워크 2.0에 새로이 추가된 트랜잭션 네임스페이스니 만큼 충분한 가치가 있다고 본다. 지금까지 닷넷 개발환경에서의 트랜잭션 처리를 벗어나는 새로운 기능들로 무장한 System.Transactions 네임스페이스를 탐험해 보자. 베타 버전을 쓸 때 항상 등장하는 경고 문구처럼 이 글에 대해서도 경고를 하자면… 베타 버전에 대한 내용인 만큼 이 글의 내용이 최종버전에서는 바뀔 수도 있음을 유의하자. 험… 험…

필자주)
이 글의 내용은 .NET Framework 2.0 정식 버전에서도 동일하게 적용됩니다.

System.Transactions 등장의 배경

닷넷 프레임워크의 현재 버전인 1.1 까지의 환경에서 트랜잭션 처리는 크게 두 가지 방법이 있을 수 있다. 첫째는 소위 로컬 트랜잭션(local transaction)혹은 수동 트랜잭션(manual transaction)이라 불리는 트랜잭션 처리 방법이다. 로컬 트랜잭션은 단일한 자원(데이터베이스, MSMQ 등)에 대한 트랜잭션 처리이므로 매우 빠른 트랜잭션 처리를 자랑한다. 반면 트랜잭션을 명시적으로 시작하고 종료하는 코드를 개발자가 직접 명시해 주어야 한다는 귀차니즘을 동반하며, 특히 컴포넌트 개발에 불편한 점을 동반한다(이에 관련한 이야기는 후에 다시 하기로 하자).

두 번째 트랜잭션 처리 방법은 분산 트랜잭션(distributed transaction) 혹은 자동 트랜잭션(automatic transaction)이라 불리는 COM+/DTC 기반의 트랜잭션 처리 방법이다. COM+ 기반의 트랜잭션 처리는 항상 MS-DTC(Distributed Transaction Coordinator)를 동반한 처리로서 상당한 오버헤드를 유발하며 레지스트리 등록, COM+ 카탈로그 등록 등의 설치 과정을 요구하기도 한다. 그렇지만 개발자는 트랜잭션 처리를 위해 특별한 코드를 작성할 필요 없이 단순한 트랜잭션 속성을 선언함으로써 트랜잭션 처리를 수행할 수 있으며 이러한 선언적 트랜잭션 처리 모델은 비즈니스 컴포넌트 작성에 대단히 유리한 프로그래밍 모델을 제공한다는 장점을 갖고 있다.

지금까지의 트랜잭션 처리 모델은 All or Nothing 이였다. 즉, COM+ 기반의 자동 트랜잭션 모델을 사용한다면 반드시 DTC 기반의 분산 트랜잭션을 사용해야 했다. 비록 단일 데이터베이스를 사용하더라도 COM+가 관여되면 불필요한 오버헤드를 발생시키는 DTC가 관여되었으며, 그 대가는 성능 저하였다. 반대로 성능을 위해 COM+를 배제하면 로컬 트랜잭션만을 이용하여 트랜잭션을 처리해야 했다. 행여 2개 이상의 자원(데이터베이스, 파일, MSMQ 등)이 트랜잭션에 참여해야(enlist) 한다면, 개발자는 이러한 개개 트랜잭션을 묶는 코드를 작성해야 했으며, 이 코드가 일관성을 유지해 주는지는 의문 사항이었다. 더욱 심각한 것은 데이터 액세스에 관련된 코드를 컴포넌트화 하기 어려웠다. 트랜잭션 처리를 위해 데이터 액세스 클래스의 메쏘드들은 항상 SqlTransaction 혹은 OracleTransaction 같은 트랜잭션 객체를 매개변수로 취하여 트랜잭션의 시작 여부에 따라 서로 다른 코드를 작성해야만 한다.

세상은 공평하다 누가 그랬던가? COM+가 주는 자동 트랜잭션의 이점으로 개발 생산성을 올리고 보다 나은 설계 구조를 유지할 수 있지만 적지 않은 성능 오버헤드가 존재한다. 로컬 트랜잭션은 보다 빠르게 트랜잭션을 처리할 수 있지만, 컴포넌트 기반의 설계가 용이하지 않으며 개발자가 더 많은 신경을 기울여야 하며 작성해야 할 코드 역시 더 많다.

닷넷 프레임워크 2.0에는 새로운 네임스페이스로서 System.Transactions 네임스페이스가 포함되어 있다. 이 네임스페이스에는 TransactionScope, Transaction, CommtableTransaction 등의 클래스가 포함되어 있으며, 이들 새로이 추가된 클래스, 인터페이스 등은 이전 버전의 닷넷 프레임워크의 트랜잭션 처리 모델을 보완/발전 시키고 있다. 즉, 로컬 트랜잭션을 사용하면서도 자동 트랜잭션처럼 코드를 단순화 하며 컴포넌트 모델을 적용시킬 수 있는 방법을 제공할 뿐 더러, 필요에 따라 분산 트랜잭션의 사용 역시 용이하게 해준다. 개발자들의 오랜 염원(적어도 필자에게는)이 System.Transactions 네임스페이스를 통해 이루어 지게 된 것이다.

System.Transactions 네임스페이스 탐험기

System.Transactions 네임스페이스를 보다 잘 이해하기 위해서는 이 네임스페이스에서 사용하는 개념인 TM(Transaction Manager) 개념을 파악하는 것이 좋지만 구구절절이 나열하는 것 보다 구체적인 클래스와 예제 코드를 보여 가면서 설명하는 것이 좋을 듯하다. 고로… 필요한 이론이나 개념, 용어등은 구체적인 코드를 설명하면서 그때그때 하도록 하겠다.

트랜잭션의 시작과 끝, TransactionScope 클래스

트랜잭션은 시작점과 종료 점을 가지고 있다. 로컬 트랜잭션을 사용한다면 BeginTransaction 메쏘드 호출이 트랜잭션의 시작점이 되며, Commit 혹은 Rollback 메쏘드 호출이 트랜잭션의 종료 점이 될 것이다. COM+를 사용한다면 Required 혹은 Requires New 속성이 주어진 컴포넌트가 활성화(activate) 되는 시점이 트랜잭션의 시작점이며 이 컴포넌트가 비활성화(deactivate) 되는 시점이 트랜잭션의 종료점이 된다.

필자주) COM+ 의 트랜잭션 시작점에 관해서 부연 설명하자면, 이미 트랜잭션이 시작된 상태에서 트랜잭션 옵션이 Required 인 컴포넌트가 활성화 되는 경우에는 기존 트랜잭션에 참여(enlist)되므로 트랜잭션 시작이 아니다.

이와 같이 트랜잭션의 시작과 종료를 묶는데 사용하는 System.Transactions 네임스페이스의 클래스가 TransactionScope 클래스이다. 이 클래스의 인스턴스가 생성되는 시점이 트랜잭션의 시작이며 이 클래스가 Dispose 되는 시점이 트랜잭션의 종료가 되는 것이다. 명시적으로 트랜잭션을 시작하거나 종료하는 메쏘드는 존재하지 않는다. 리스트 1을 살펴 보자. Using 키워드를 사용하여 TransactionScope 클래스의 인스턴스를 만들었으며 이 클래스의 인스턴스가 만들어짐으로써 트랜잭션은 시작된 것이다. 그리고 using 블록이 끝나는 시점에서 자동으로 Dispose 메쏘드가 호출되므로 트랜잭션이 종료된다.

string ConnStr = "SERVER=(local);UID=Tester;PWD=test";

string query = "INSERT INTO TestTable VALUES(99, 'TempData'); " +

            "SELECT @@IDENTITY";

using (TransactionScope scope = new TransactionScope()) {

    SqlConnection conn = new SqlConnection(ConnStr);

    SqlCommand cmd = new SqlCommand(query, conn);

 

    conn.Open();

    decimal id = (decimal)cmd.ExecuteScalar();

    Console.WriteLine(“New data id = {0}”, id);

    conn.Close();

    scope.Complete();

}

리스트1. TransactionScope 클래스 사용 예제

리스트 1를 아무리 잘 뜯어보더라도(너무 짧고 간단해서 뜯어 볼 것도 없지만…) 아무리 찾아봐도 트랜잭션을 시작 하거나 종료하는 코드는 없다. 하지만 트랜잭션은 시작되고 종료된다. 트랜잭션이 시작되고 종료되는 것을 확인하기 위해 SQL 서버의 프로파일러(profiler)로 이 코드의 수행을 캡처 해 보면(화면 1) 트랜잭션이 시작되고 종료되는 것을 알 수 있다. 게다가 멋지게도 SQL 서버가 수행하는 트랜잭션은 분산 트랜잭션이 아닌 로컬 트랜잭션이지 않은가? (로컬 트랜잭션은 명시적으로 BEGIN TRANSACTION과 COMMIT/ROLLBACK TRANSACTION 문장이 수행된다)


화면1. 리스트1을 수행한 내용에 대한 프로파일링 결과

간단한 코드를 작성해 봄으로써 System.Transactions 네임스페이스는 COM+ 같이 트랜잭션 코드를 선언적(?)으로 간략하게 작성할 있으면서도 성능 면에서 우수한 로컬 트랜잭션을 사용하고 있음을 있었다. 이것 만으로도 지금까지 사용하던 트랜잭션 처리 기법을 뛰어넘는 새로운 기능이 닷넷 프레임워크 2.0에서 지원되고 있음을 있을 것이다.

필자주) System.Transactions 네임스페이스가 지원하는 트랜잭션은 COM+(ServicedComponent)와 같이 완전히 클래스 수준에서 특성(attribute)를 통해 선언적으로 트랜잭션을 제어하는 것은 아니다. TransactionScope 클래스를 생성하고 Dispose 하는 수준의 코드는 필요하다. 필자는 이것이 COM+에 비해 System.Transactions이 불편한 점이 되며 약점으로 생각하고 있다.

또한 나중에 설명이 나오겠지만 트랜잭션의 Commit/Rollback을 지정하는 방법 역시 COM+의 AutoComplete를 사용했을 때와는 달리 코드를 작성해야 하는 단점도 있다.

Transaction 결과 결정

그렇다면 트랜잭션을 롤백(rollback)하는 방법은 무엇일까? 리스트 1의 코드를 찬찬히 다시 살펴보면 코드의 끄트머리에 으슥하게 자리잡고 있는 Complete 메쏘드 호출이 해답이 된다. TrasactionScope 클래스는 Complete 메쏘드가 호출되면 트랜잭션은 성공한 것으로 간주한다. TransactionScope 클래스의 이러한 행동은 COM+의 SetComplete/SetAbort 메쏘드 호출과 매우 비슷하다. 그렇다면 이 클래스는 Abort 메쏘드도 가지고 있는가? 그렇지 않다. TransactionScope 메쏘드는 Complete 메쏘드가 호출되지 않으면 트랜잭션이 실패한 것으로 간주할 뿐이며 Abort 류의 메쏘드는 존재하지 않는다.

이는 개발자가 트랜잭션 실패시 처리해야할 코드의 양을 줄여주기 위한 것으로 생각된다. 실재로 TransactionScope 클래스는 내부적으로 트랜잭션 성공 여부를 나타내는 플래그를 가지고 있다. 이 플래는 기본적으로 false 값을 가지고 있으며 Complete 메쏘드 호출로서 true로 바뀐다. 이미 false인 트랜잭션 성공 여부 플래그를 굳이 리셋하는 메쏘드는 개발자를 혼란스럽게 할 뿐이다. 또한 일단 TransactionScope에 Complete 가 호출된 후, 트랜잭션을 다시 Rollback하게 만드는 방법은 없다. 따라서 개발자는 Complete 메쏘드 호출 시점에 주의해야 한다. 트랜잭션의 속성상 모든 작업이 성공한 경우에만 트랜잭션이 커밋되어야 한다는 점을 고려해보면 Complete 메쏘드 호출이 1회성만을 가진다는 것이 당연하다고 볼 수 있겠다.

트랜잭션 속성 설정

TransactionScope 클래스가 명시적인 트랜잭션 시작/종료 코드 없이 트랜잭션을 시작하고 종료해 준다면, COM+ 컴포넌트에 트랜잭션 속성을 주듯이 TransactionScope에도 트랜잭션 속성을 줄 수 있지 않을까? 그렇다. TransactionScope 클래스에는 TransactionScopeOption 열거자를 통해 Required, Requires New, Supress 3가지 옵션을 줄 수 있다.

Required 옵션은 COM+의 Required 옵션과 동일하게 트랜잭션이 이미 시작되어 있지 않으면 새로운 트랜잭션을 시작하고, 이미 트랜잭션이 시작되었다면 기존 트랜잭션에 참여(enlist)한다. Required 옵션은 TrasactionScope의 디폴트 값이며, 리스트 1의 코드에서 명시적으로 트랜잭션 속성을 주지 않았을 때 트랜잭션이 수행된 이유이기도 하다. Requires New 옵션 역시 COM+의 그것과 동일하게 기존 트랜잭션 존재 여부와 무관하게 항상 새로운 트랜잭션을 생성하고 트랜잭션에 참여하는 옵션이다. Suppress는 COM+의 Not Supported 옵션과 동등한 것으로서 기존 트랜잭션에 존재 여부와 무관하게 항상 트랜잭션 바깥에서 수행되는 옵션이다. COM+를 이용해 트랜잭션 컴포넌트를 한번이라도 작성해 본 독자라면 어렵지 않게 이해가 될 것이다.

그런데, COM+의 Supported 옵션에 해당되는 것은 TransactionScopeOption 열거자에는 없지 않은가? 그렇다. TransactionScopeOption 열거자에 COM+의 Supported 옵션에 해당하는 열거자는 존재하지 않는다. 그 이유를 천천히 생각해 보면, 불필요하기 때문에 삭제된 것이다. Supported 옵션의 의미는 “트랜잭션이 이미 시작되었다면 기존 트랜잭션에 참여하고 그렇지 않다면 트랜잭션에 참여하지 않는다” 이다. 리스트 2의 예제 코드를 보면 BLC(Business Logic Component) 클래스가 DAC(Data Access Component) 클래스의 Method2 를 호출할 때 BLC가 이미 트랜잭션을 시작했기 때문에 DAC의 코드는 트랜잭션 문맥 내에서 수행된다. 만약 DAC의 Method2가 BLC가 아닌 클라이언트(ASP.NET 이나 윈폼 코드)에 의해 직접 호출되는 경우에는 트랜잭션과 무관하게 작동한다. 이러한 코드 패턴은 COM+ 세계에서 극히 흔하게 볼 수 있는 코드 예제이다.

[Transaction(TransactionOption.Required)]

public class BLC : ServicedComponent

{

    [AutoComplete]

    public void Method1()

    {

        using(DAC obj = new DAC()) {

            obj.Method2();

        }

    }

}

 

[Transaction(TransactionOption.Supported)]

public class DAC : ServicedComponent

{

    [AutoComplete]

    Public void Method2()

    {

        // 데이터 액세스 코드 (생략)

    }

}

리스트2. COM+를 사용하는 전통적인 코드

이제 리스트 2와 동등한 역할을 하는 코드를 TransactionScope로 작성해 보자(리스트 3). 동일하게 BLC, DAC를 만들되 BLC의 Method1 에서는 트랜잭션을 위해 TransactionScope의 인스턴스를 만든다. 그리고 Method2 를 호출한다. 리스트 3의 코드는 리스트 2와 정확하게 동등한 기능을 제공한다. BLC를 통해 DAC가 호출된다면 DAC의 코드는 트랜잭션 문맥 하에서 수행될 것이다. 또한 클라이언트가 직접 DAC를 호출한다면 TransactionScope 로 묶이지 않았기 때문에 당연히 트랜잭션과 무관하게 코드가 수행된다. 사실 TransactionScope는 COM+와 같이 ServicedComponenet 클래스를 강제하지 않기 때문에 Supported 란 옵션은 전혀 불필요한 것이 된다.

public class BLC

{

    public void Method1()

    {

        using (TransactionScope scope = new TransactionScope()) {

            Method2();

        }

    }

}

 

public class DAC

{

    public void Method2()

    {

        // 데이터 액세스 코드 (생략)

    }

}

리스트3. TrasactionScope 를 사용하여 작성한 리스트 2와 동등한 코드

TransactionScope 클래스에 트랜잭션 옵션을 주는 방법은 이 클래스의 인스턴스를 생성할 때 생성자의 매개변수를 통해 TransacitonScopeOption을 명시할 수 있다. 앞서 언급한 대로 명시적으로 TransactionScopeOption이 주어지지 않으면 Required 가 디폴트 값으로 사용된다.

TransactionScope scope = new TransactionScope(TransactionScopeOption.Required);

기타 트랜잭션 옵션

COM+의 트랜잭션 옵션처럼 트랜잭션 격리수준(isolation level)이나 타임아웃 시간 역시 명시할 수 있을까? TransactionScope 클래스는 TransactionOptions 구조체(System.EnterpriseService 네임스페이스의 TransactionOption 열거자와 혼동하지 말자)를 통해 트랜잭션 격리수준과 타임아웃을 명시할 수 있도록 하고 있다. 필요한 격리수준과 타임아웃을 명시하고 이것을 TransactionScope 생성자를 통해 설정하면 된다.

string ConnStr = "SERVER=(local);UID=Tester;PWD=test";

string query = "INSERT INTO TestTable VALUES(99, 'TempData'); " +

            "SELECT @@IDENTITY";

 

TransactionOptions options = new TransactionOptions();

options.Timeout = TimeSpan.FromSeconds(30);

options.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;

 

TransactionScope scope = new TransactionScope(

                            TransactionScopeOption.Required,

                            options);

try {

    SqlConnection conn = new SqlConnection(ConnStr);

    SqlCommand cmd = new SqlCommand(query, conn);

 

    conn.Open();

    decimal id = (decimal)cmd.ExecuteScalar();

    Console.WriteLine("New data id = {0}", id);

    conn.Close();

    scope.Complete();

}

finally {

    scope.Dispose();

}

리스트4. 트랜잭션 옵션 설정과 try~finally 코드 패턴

리스트 4는 트랜잭션 옵션을 설정하는 예를 보여주고 있다. 또한 using 키워드를 사용하지 않고 이와 동등한 try~finally 문장을 사용한 예를 보여주고 있다.

이외에도 TransactionScope 클래스는 EnterpriseServicesInteropOption 열거자를 옵션으로 명시할 수 있다. 이 옵션은 TransactionScope 클래스가 COM+ 내에서 사용될 때 TransactionScope가 생성하는 트랜잭션이 System.Transactions 네임스페이스가 관리하는 트랜잭션을 사용할 것인가 아니면 COM+가 관리하는 트랜잭션을 사용할 것인가를 결정하는 옵션을 가지고 있다. None, Auto, Full 세가지 옵션 중 하나를 줄 수 있으며, None 은 COM+와 TransactionScope가 전혀 다른 트랜잭션을 사용함을 의미한다. 즉, COM+가 이미 트랜잭션을 시작했건 안했건 상관없이 TransactionScope 클래스는 TransactionScopeOption 열거자가 명시하는 바 대로 트랜잭션을 생성한다. 반면 Full 옵션을 사용하면 TransactionScope 클래스는 COM+의 컴포넌트와 가장 비슷한 행동을 한다. 예를 들어, COM+가 이미 트랜잭션을 시작했고 TransactionScope에 Required 가 주어져있다면 TransactionScope 클래스는 이미 수행중인 COM+ 관리하의 트랜잭션을 사용한다. 혹은 COM+가 아직 트랜잭션을 시작하지 않았고 TransactionScope에 Required 옵션이 주어져 있다면, 이 클래스는 COM+ 트랜잭션(Service Without Component의 ServiceDomain 이용)을 생성해 낸다.

Auto 옵션은 약간 복잡하다. COM+가 아직 트랜잭션을 시작하지 않았다면 Auto 옵션은 None 과 동일하다. 즉, COM+가 아직 트랜잭션을 시작하지 않았고, TransactionScope가 Required 라면 System.Transactions 네임스페이스가 관리하는 트랜잭션을 시작한다. 반면 COM+가 이미 트랜잭션을 시작했다면 Auto 옵션은 Full 과 동일한 행동을 한다. 다시 말하자면, COM+가 이미 트랜잭션을 시작했다면 TransactionScope 옵션은 COM+가 시작한 트랜잭션을 사용하여 트랜잭션에 참여(enlist) 하게 된다.

COM+와 System.Transactions 네임스페이스와의 관계에 대해서는 나중에 한번 더 언급하기로 하고 여기서는 그만 줄이기로 하자.

Transaction Promotion

앞서 리스트 1화면 1에서 TransactionScope 클래스를 사용하면 BeginTransaction 메쏘드나 Commit/Rollback 메쏘드 호출 없이도 트랜잭션, 그것도 로컬 트랜잭션이 사용된다고 했다. 그렇다면 TransactionScope 클래스를 사용하면서 COM+를 사용하는 것처럼 분산 트랜잭션을 사용할 수는 없는 것일까?

왜 없어 ! 빠람바 밤빠~~ 빠람빠 밤빠~~ 닷사마(닷넷 프레임워크) 2.0은 트랜잭션 프로모션(Transaction Promotion)이라는 멋진 기능을 갖고 있다. 트랜잭션 프로모션이라는 기능은 필요에 따라 로컬 트랜잭션이 분산 트랜잭션으로 프로모션 될 수 있다. 오오~ 멋지지 아니한가? (자뻑에 해당한다고 볼 수 있겠다…)

그렇다면 언제 트랜잭션은 프로모션이 될까? 전형적인 경우는 트랜잭션 내에서 2개 이상의 트랜잭션 자원(데이터베이스, MSMQ 등이 되겠다)을 액세스하는 경우, 두 번째 자원에 액세스 하는 코드가 수행되는 시점에서 트랜잭션은 분산 트랜잭션으로 프로모션 된다. 리스트 5를 보자. TransactionScope 클래스가 사용되었고 트랜잭션 영역에서 먼저 SQL Server 2005에 접근하고 있다. 그리고 다시 다른 컴퓨터에 있는 SQL Server 2000을 액세스 한다. 이 코드에서 SQL Server 2000에 대한 연결을 시도하는 conn2.Open() 메쏘드 호출에서 트랜잭션은 분산 트랜잭션으로 프로모션 된다. 리스트 5를 수행시켰을 때 SQL Server 2005의 상태를 모니터링 한 화면은 화면 2와 같다. 구체적으로 트랜잭션이 프로모션 되고 있음을 눈으로 확인할 수 있다. 프로파일러를 통해 트랜잭션 프로모션을 확인할 수도 있지만 COM+ 구성 요소 관리자의 분산 트랜잭션 모니터링 탭을 통해서도 분산 트랜잭션이 진행되고 있음을 확인할 수 있을 것이다.

// local SQL Server 2005 connection string.

static string ConnStr = "SERVER=(local);UID=Tester;PWD=test";

// remote SQL Server 2000 connection string.

static string ConnStr2 =

                        "SERVER=workman;UID=Tester;PWD=test;DATABASE=Northwind";

 

string query = "INSERT INTO TestTable VALUES(99, 'TempData'); " +

            "SELECT @@IDENTITY";

string query2 = "SELECT COUNT(*) FROM Products";

 

using (TransactionScope scope = new TransactionScope()) {

    SqlConnection conn = new SqlConnection(ConnStr);

    SqlCommand cmd = new SqlCommand(query, conn);

 

    conn.Open();

    decimal id = (decimal)cmd.ExecuteScalar();

    conn.Close();

    Console.WriteLine("New data id = {0}", id);

 

    // now... tx will be promoted to DTC transaction

    SqlConnection conn2 = new SqlConnection(ConnStr2);

    SqlCommand cmd2 = new SqlCommand(query2, conn2);

 

    conn2.Open();

    int cnt = (int)cmd2.ExecuteScalar();

    conn2.Close();

    Console.WriteLine("Record Count = {0}", cnt);

 

    scope.Complete();

}

리스트 5. 두 개의 데이터베이스에 접근하는 예제 코드


화면2. 트랜잭션 프로모션 모니터링

이제 좀 김 새는 이야기를 해야 할 것 같다. 트랜잭션 프로모션은 정말 멋진 기능이 아닐 수 없다. 성능 상의 불이익을 초래하는 분산 트랜잭션이 필요할 때만 사용된다는 것은 성능 문제로 인해 COM+를 기피하고 설계와 생산성 상의 불이익을 감수했던 지난 프로그래밍 환경에 획기적인 아키텍처를 선물해 줄 것은 당연하다. 하지만 이 트랜잭션 프로모션은 SQL Server 2005에서만 가능한 기능이다. 트랜잭션 프로모션은 해당 트랜잭션 자원(자원 관리자; Resource Manager)에서 해당 기능을 제공해야만 유효하다. 즉, 기존의 SQL Server 2000, MSMQ, Oralce 등의 트랜잭션 자원은 트랜잭션 프로모션을 지원하지 않는다. 따라서 (오늘의 중요 포인뜨 !) SQL Server 2005를 제외한 다른 데이터베이스, MSMQ 등의 자원 관리자는 TransactionScope를 사용하면 로컬 트랜잭션 없이 곧바로 분산 트랜잭션이 시작된다.

비록 SQL Server 2005를 제외한 현존하는 다른 자원 관리자들이 트랜잭션 프로모션을 제공하지 않더라도 시간이 지나면 트랜잭션 프로모션을 지원하는 자원 관리자들이 속속 등장할 것이다. 그리고 그것은 트랜잭션 처리의 성능 향상을 약속할 것이므로 성급한 실망이나 포기는 금물이다.

TransactionScope 중첩

지금까지 글을 통해서 이미 감을 잡았겠지만 TransactionScope는 직/간접적으로 중첩이 가능하다. 직접적으로 중첩된다는 말은 리스트 6의 Method1 과 같이 TransactionScope 내에 또 다른 TransactionScope가 직접적으로 존재하는 경우가 되겠다. 예상할 수 있듯이 잘 설계되고 구조화되고 모듈화된 코드라면 직접적인 중첩은 발생할 가능성이 대단히 적을 것이다. 간접적인 중첩은 리스트 6의 Method2 와 같이 TransactionScope 내에 TransactionScope이 직접 나타나진 않지만 메쏘드 호출에 의해 간접적으로 중첩되는 것을 말하며 가장 흔하고 자주 발생되는 중첩 상황이 되겠다.

public void Method1()

{

    using (TransactionScope outterScope = new TransactionScope()) {

        // 트랜잭션 관련 코드 (생략)

        using (TransactionScope innerScope = new TransactionScope()) {

            // 트랜잭션 관련 코드 (생략)

            innerScope.Complete();

        }

        outterScope.Complete();

    }

}

 

public void Method2()

{

    using (TransactionScope outterScope = new TransactionScope()) {

        // 트랜잭션 관련 코드 (생략)

        DataAccessMethod();    // 메쏘드 호출

        outterScope.Complete();

    }

}

 

public void DataAccessMethod()

{

    using (TransactionScope innerScope = new TransactionScope()) {

        // 트랜잭션 관련 코드 (생략)

        innerScope.Complete();

    }

 

}

리스트 6. TransactionScope의 직접/간접 중첩

TransactionScope 가 중첩되어 사용되면 트랜잭션의 영역은 어떻게 되며 트랜잭션의 시작과 종료는 어떻게 결정될까? 앞서 언급한 TransactionScopeOption 열거자는 정확히 트랜잭션 중첩을 위한 것으로 볼 수 있다. TransactionScope 가 중첩될 때 안쪽 TransactionScope는 그 속성이 Required이냐 Requires New 이냐 혹은 Suppress 인가에 따라서 바깥 트랜잭션 내에 포함되거나 새로운 트랜잭션을 생성하거나 혹은 전혀 트랜잭션과 무관하게 작동한다. 그리고 이 규칙은 COM+의 그것과 정확히 일치한다. COM+의 트랜잭션 전파 규칙은 2003년 4월호 테크니컬 컬럼에서 이미 다루었으므로 다시 상세하게 설명하진 않겠다. 리스트 7그림 1을 통해 트랜잭션이 중첩되었을 때 트랜잭션의 범위가 어떻게 결정되는지 예제를 보였으니 COM+를 다루어본 독자라면 어렵지 않게 이해할 수 있을 것이다.

public static void Main()

{

    Method1();

    Method3();

}

 

public static void Method1()

{

    using (TransactionScope scope1 = new TransactionScope(

                                        TransactionScopeOption.Required)) {

        // 트랜잭션 코드 (생략)

        Method2();

        scope1.Complete();

    }

}

 

public static void Method2()

{

    using (TransactionScope scope2 = new TransactionScope(

                                        TransactionScopeOption.RequiresNew)) {

        // 트랜잭션 코드 (생략)

        Method3();

        using (TransactionScope scope4 = new TransactionScope(

                                              TransactionScopeOption.Suppress)) {

            // 트랜잭션 코드 (생략)

            scope4.Complete();

        }

        scope2.Complete();

    }

}

 

public static void Method3()

{

    using (TransactionScope scope3 = new TransactionScope(

                                        TransactionScopeOption.Required)) {

        // 트랜잭션 코드 (생략)

        scope3.Complete();

    }

}

리스트 7. 중첩 TransactionScope 예제

그림 1에서 보는 바와 같이 트랜잭션의 시작은 Required 속성이나 RequiresNew 속성을 가진 TransactionScope에 의해서만 가능하다. 이렇게 트랜잭션을 시작한 TransactionScope를 트랜잭션 루트라 부르며, 트랜잭션의 루트 TransactionScope가 Dispose 될 때, 즉 using 영역을 벗어날 때 트랜잭션은 종료된다.

트랜잭션이 종료될 때 트랜잭션의 결과 값, 즉 Commit/Abort 결정은 어떻게 될까? 예상 할 수 있듯이 COM+의 그것과 동일하다. COM+에서 트랜잭션에 참여하는 컴포넌트들이 트랜잭션의 Commit에 찬성/반대하는 것과 동일하게 System.Transactions 네임스페이스에서도 트랜잭션에 참여한 TransactionScopee들이 Complete 호출이 되었는가에 의해 전체 트랜잭션의 Commit/Abort 가 결정된다. 트랜잭션에 참여한 모든 TransactionScope가 Complete를 호출했다면 트랜잭션은 Commit 될 것이고 어느 하나라도 Complete를 호출하지 않았다면(Exception에 의해서건 코드 경로에 의해 호출되지 안았건 관계없이) 트랜잭션은 Abort 된다.

TransactionScope 클래스가 중첩되었을 때의 행동 방식은 살펴본바 대로 COM+의 행동방식과 매우 비슷하다. 특히 COM+의 Service Without Components (2005년 2월호 테크니컬 컬럼 참조)의 그것과는 거의 똑같을 정도로 비슷하다. System.Transactions 네임스페이스와 COM+의 관계는 조금 있다가 더 상세히 논하기로 하자.


그림 1. 리스트 7의 트랜잭션 범위

System.Transactions 의 보다 깊은 곳…

지금까지 System.Transactions 네임스페이스의 내용을 TransactionScope 클래스 위주로 사용법 관점에서 살펴보았다. 보다 원리적인 입장에서 살펴보면 System.Transactions 네임스페이스의 핵심은 TransactionScope 클래스가 아닌 Transaction 클래스이며 TransactionScope 클래스는 개발자가 Transaction 클래스를 직접적으로 액세스 하지 않아도 되도록 해주는 front-end 클래스 일 뿐이다. 또한 트랜잭션 프로모션은 LTM(Lighetweght Transaction Manager)라 불리는 새로운 트랜잭션 관리자에 의해 수행되는 기능이다. 이러한 내용이 System.Transactions 네임스페이스의 깊은 곳에 감추어져 있다. 자… System.Transactions 의 깊은 곳으로 빠져 보실 랍니까? 빠져 봅시다~

Transaction 객체와 Ambient Transaction

System.Transactions 네임스페이스는 트랜잭션을 관리하기 위해 Transaction 클래스를 사용한다. 이 클래스는 ADO.NET이나 MSQM 등의 자원 관리자(resource manager)가 트랜잭션에 참여(enlist)하기 위한 메쏘드와 트랜잭션의 Commit, Rollback 을 위한 메쏘드 등을 포함하고 있다. System.Transactions 네임스페이스 기반에서 트랜잭션이 시작되면 항상 Transaction 객체 (Transaction 클래스 혹은 이 클래스에서 파생된 클래스의 인스턴스)가 생성된다.

또한 System.Transactions 네임스페이스는 환경 트랜잭션(ambient transaction; ambient를 사전적 의미로 번역하자면 ‘환경’ 혹은 ‘주변’ 정도가 된다. 아직 명확한 한글 용어가 없으므로 필자 임의로 환경 트랜잭션이라는 용어를 사용할 것이다. Context transaction 이라는 용어가 개념에는 더 근접하지만 COM+의 context와 혼동을 피하기 위해 abmient란 용어를 사용했으리라 짐작할 뿐이다)이라는 개념을 사용한다. 환경 트랜잭션이란 현재 수행중인 트랜잭션을 의미하며, 환경 트랜잭션에 접근하기 위해서는 Transaction 클래스의 Current 속성을 사용하면 된다. 만약 현재 어떤 트랜잭션이 활성화되어 수행 중이라면 Current 속성은 Transaction 객체를 반환할 것이고 그렇지 않다면 이 속성은 null 을 반환한다. 만약 현재 코드가 트랜잭션 상에서 수행 중인지 아니면 트랜잭션 바깥에서 수행 중인가를 알고 싶다면 Transaction.Currnet 의 값이 null 인가를 확인하면 될 것이다.

환경 트랜잭션은 System.Transactions 네임스페이스에서 매우 중요한 역할을 수행한다. 앞서 리스트 1의 코드에서 ADO.NET 코드는 명시적인 BeginTransaction 호출 없이 트랜잭션을 시작했다. 이렇게 자원 관리자가 자동으로 트랜잭션에 참여(enlist)하는 것을 auto-enlistment 라 부른다. auto-enlistment가 가능한 것이 바로 환경 트랜잭션 때문이다. 마이크로소프트는 System.Transactions 네임스페이스를 위해 ADO.NET 코드를 다시 작성했다고 한다. ADO.NET 코드들은 데이터베이스에 접속하기 전에 환경 트랜잭션을 검사한다. 만약 환경 트랜잭션이 활성화 되었다면 이 트랜잭션에 참여하게 되는 것이다.

TransactionScope 클래스가 하는 일은 간단히 요약할 수 있다(TransactionScope 클래스는 트랜잭션 중첩과 COM+와의 연동과 관련하여 훨씬 더 복잡하고 많은 작업을 수행한다). TransactionScope 객체가 생성되면 이 클래스는 TransactionScopeOption 열거자 값에 의해 자신의 행동을 결정한다. Required 를 예로 들어 보자. 트랜잭션 속성이 Required 이므로 TransactionScope는 환경 트랜잭션이 존재하는가를 검사한다. 만약 존재한다면 이 트랜잭션 하에서 코드를 수행하면 된다. 반면 환경 트랜잭션이 존재하지 않는다면(즉, 트랜잭션이 이미 시작하지 않았다면), 새로운 Transaction 객체를 생성하고 그것을 환경 트랜잭션으로 설정(Transaction.Current에 할당)한다. 이로써 새로운 트랜잭션이 시작된 것이다.

TransactionScope를 사용하지 않고도 트랜잭션 작업을 수행할 수 있을까? Transaction 객체를 생성할 수만 있다면 이 객체를 Transaction.Current 속성에 할당함으로써 환경 트랜잭션으로 생성하면 트랜잭션을 시작할 수 있는 것이다. 하지만 Transaction 클래스에는 핵심적인 메쏘드인 Commit 메쏘드가 존재하지 않는다. 이 클래스는 오로지 Rollback 메쏘드와 트랜잭션에 참여(enlist)하기 위한 메쏘드만이 제공될 뿐이다. 새로운 Transaction 클래스가 필요한 때인 것이다.

CommittableTransaction 클래스

Transaction 클래스는 Commit 메쏘드를 제공하지 않는다. 이유는 간단하다. TransactionScope와 같이 트랜잭션의 영역이 중첩되는 경우에 트랜잭션이 종료되고 Commit을 수행할 수 있는 것은 루트 TransactionScope 뿐이다. 만약 환경 트랜잭션을 통해 Commit 을 호출할 수 있게 된다면 트랜잭션이 종료되지도 않은 시점에서 Commit 호출이 가능해 질 것이고 이 때문에 트랜잭션의 원자성과 무결성은 깨지고 말것이다. 이러한 이유로 Transaction 클래스는 Commit 메쏘드를 가지고 있지 않다. 그렇지만 Rollback 메쏘드가 존재하는 이유는 무엇일까? 커밋과 다르게 롤백은 트랜잭션의 전체 상태에 즉시 영향을 줄 수 있다. 커밋의 경우 트랜잭션의 나머지 작업들(존재 한다면)이 모두 성공해야만 수행할 수 있는 작업인 반면, 롤백의 경우 나머지 작업들에 무관하게 트랜잭션이 실패했음을 결정지을 수 있기 때문이다. 실제로 Rollback 메쏘드가 이미 호출된 이후에 수행되는 데이터액세스 작업은 모두 TransactionException 예외를 발생시킨다. 왜냐면 이미 트랜잭션은 실패할 것으로 예정되어 있으니까…

그렇다면 루트 TransactionScope는 어떻게 Commit 을 수행할까? Commit이 가능한 Transaction 클래스가 있으니 그것이 바로 CommittableTransaction 클래스이다. 이 클래스는 Commit 메쏘드를 갖고 있으며 인스턴스 생성도 가능하다(Transaction 클래스는 인스턴스 생성이 불가능 하다). TransactionScope 클래스는 새로운 트랜잭션이 시작되어야 한다면 CommittableTransaction 객체를 생성하고 내부 변수에 기록해 둔다. 하지만 환경 트랜잭션에는 CommittableTransaction의 복제본 객체를 기록해둔다. 이 복제본 객체는 CommittableTransaction 객체가 아닌 일반 Transaction 객체로서 환경 트랜잭션을 통해 Commit이 가능하지 않도록 하기 위함이다. 복제본이 사용되었다고 해서 트랜잭션이 분리되는 것이 아님은 당연 빤스이다. 후에 TransactionScope가 Dispose 될 때(using에 의해서건 명시적인 Dispose 호출에 의해서건) 내부에 기록해둔 CommittableTransaction 객체를 통해 트랜잭션을 Commit 하는 것이다.

이제 원리를 좀 알았으니 CommittableTransaction과 환경 트랜잭션을 이용해 TransactionScope 클래스를 사용하지 않고 트랜잭션을 수행할 수 있다. 리스트 8을 보자. 이 코드는 CommittableTransaction 클래스의 인스턴스를 생성하고 이것을 Transaction.Current 속성에 할당함으로써 환경 트랜잭션을 설정하고 있다. 그 후 데이터 액세스를 수행하고 이 데이터 액세스는 트랜잭션 하에서 수행된다. 종국에는 Commit 메쏘드를 호출하여 트랜잭션을 커밋 하고 환경 트랜잭션을 복구한다.

string ConnStr = "SERVER=(local);UID=Tester;PWD=test";

string query = "INSERT INTO TestTable VALUES(99, 'TempData'); " +

            "SELECT @@IDENTITY";

SqlConnection conn = new SqlConnection(ConnStr);

SqlCommand cmd = new SqlCommand(query, conn);

 

// 트랜잭션 생성 및 환경 트랜잭션으로 설정

CommittableTransaction tx = new CommittableTransaction();

Transaction oldtx = Transaction.Current;

Transaction.Current = tx;

 

// 데이터 액세스 수행

conn.Open();

decimal id = (decimal)cmd.ExecuteScalar();

Console.WriteLine(“New data id = {0}”, id);

conn.Close();

 

// 트랜잭션 커밋 및 환경 트랜잭션 복구

tx.Commit();

Transaction.Current = oldtx;

tx.Dispose();

리스트 8. CommittableTransaction 클래스를 이용한 수동 트랜잭션

리스트 8은 TransactionScope를 사용할 때 RequiresNew 옵션을 사용하는 것과 동등한 작동을 한다. 즉, 기존 트랜잭션의 유무에 관계없이 항상 새로운 트랜잭션을 생성하고 시작(환경 트랜잭션으로 설정하는 것)하기 때문이다. 물론 CommitableTransaction과 환경 트랜잭션을 이용하여 Required 옵션도 에뮬레이션 할 수 있다. 리스트 9가 TransactionScope 클래스에 Required 옵션을 사용했을 때와 동등한 기능을 하는 코드이다. 코드에 상세한 주석을 달았으므로 더 설명하지는 않겠다.

Transaction oldTx = Transaction.Current;

Transaction curTx;

 

// 환경 트랜잭션이 존재하는가 검사한다.

if (oldTx == null) {

    // 존재하지 않는다면 새로운 트랜잭션을 생성한다.

    // 이 코드가 트랜잭션 루트이므로 Commit이 가능한 CommittableTransaction이 필요하다.

    curTx = new CommittableTransaction();

    Transaction.Current = curTx;

}

else {

    // 환경 트랜잭션이 존재한다면 현재 트랜잭션으로 사용한다.

    // 환경 트랜잭션의 타입은 결코 CommitableTransaction이 아님에 유의한다.

    curTx = oldTx;

}

 

try {

    //

    // 데이터 액세스 코드…… (생략)

    //

 

    // curTx 값을 조사하여 이 코드에서 생성한 CommittableTransaction 객체라면

    // Commit을 수행하고 그렇지 않다면 아무런 작업을 하지 않는다.

    // 트랜잭션의 커밋은 이 코드를 호출한 트랜잭션 루트에서 작업할 것이다.

    if (curTx as CommittableTransaction != null)

        ((CommittableTransaction)curTx).Commit();

}

finally {

    // 환경 트랜잭션을 원상 복구한다.

    if (oldTx != curTx) {

        curTx.Dispose();

        Transaction.Current = oldTx;

    }

}

리스트 9. Required 옵션에 대한 에뮬레이션

Lightweight Transaction Manager

앞서 SQL Server 2005를 사용할 경우, 트랜잭션에서 최초의 데이터 액세스는 로컬 트랜잭션으로서 사용되며 필요에 따라 분산 트랜잭션으로 프로모션 될 수 있음을 보였다. 이러한 트랜잭션 프로모션의 원리에는 LTM(Lightweight Transaction Manager)라 불리는 새로운 트랜잭션 관리자가 관여된다. System.Transactions 네임스페이스에서 관리하는 트랜잭션은 항상 LTM을 통해서 관리된다. LTM은 트랜잭션이 프로모션이 되어야 하는 시점을 검출해 내고, 트랜잭션을 분산 트랜잭션으로 프로모션 한다.

구체적으로 LTM이 관여되는 과정을 살펴보자. 트랜잭션이 생성되면 트랜잭션 관리자로서 무조건 LTM이 선택된다. 즉, Transaction 객체의 내부에 트랜잭션 관리자로 LTM이 선정되는 것이다. ADO.NET 이나 기타 클래스를 이용하여 트랜잭션 자원(데이터베이스, MSMQ 등)을 액세스 하면 자원 관리자(ADO.NET 이나 MSMQ 관련 닷넷 코드들)는 환경 트랜잭션을 검출하고 이 환경 트랜잭션의 Enlist 메쏘드를 통해 트랜잭션에 참여한다. 이제 LTM은 트랜잭션에 참여한 자원이 무엇인가를 알 수 있는 것이다. 이때 LTM은 자원 관리자가 트랜잭션 프로모션을 지원하는지 검사한다(이는 자원 관리자가 IPromotableSinglePhaseNotification 인터페이스를 구현하는가를 검사함으로써 이루어 진다). 만약 자원 관리자가 프로모션을 지원한다면 LTM은 트랜잭션을 계속 진행하고 트랜잭션 내에서 두 번째 자원 관리자가 관여될 때 트랜잭션을 프로모션 할 것이다 (프로모션 작업은 IPromotableSinglePhaseNotification 인터페이스의 Promote() 메쏘드를 호출함으로써 이루어진다). 프로모션 이후에는 트랜잭션 관리자가 LTM에서 Oletx 트랜잭션 관리자로 교채 되며 다시 LTM으로의 교체는 발생하지 않는다. Oletx 트랜잭션 관리자는 DTC를 사용하는 분산 트랜잭션 관리자로서 DTC와 동일한 역할을 수행하는 System.Transactions 네임스페이스의 한 클래스 정도로 인식하면 되겠다.

만약 자원 관리자가 프로모션을 지원하지 않는다면 LTM은 트랜잭션이 프로모션이 지원되지 않는 것으로 판단하고 현재 트랜잭션의 트랜잭션 관리자를 Oletx 트랜잭션 관리자로 즉시 교체한다. SQL Server 2000, Oracle, MSMQ 등의 자원이 트랜잭션에 참여할 때 곧바로 분산 트랜잭션이 시작하는 이유가 바로 여기에 있다. 이들의 자원 관리자는 프로모션을 지원하지 않기 때문에 LTM이 사용되지 않기 때문이다. 보다 정확히 말해서 LTM 이 사용되자 마자 Oletx 트랜잭션 관리자로 대체되기 때문이라 할 수 있겠다.

LTM의 오버헤드는 매우 적다고 알려져 있다. 마이크로소프트에 의해 진행된 SQL Server 2005의 벤치 마크에서 LTM이 사용된 경우와 ADO.NET의 트랜잭션 메쏘드들(BeginTransaciton/Commit/Rollback 메쏘드)을 사용한 경우와의 성능 차이는 거의 없었다고 한다. 필자가 눈으로 확인하지 않은 정보라서 확실하지 않지만 DTC를 사용하지 않는다는 점에서 기존의 COM+ 기반의 분산 트랜잭션보다는 매우 빠를 것임은 분명하다.

실제로 개발자가 LTM이나 Oletx 트랜잭션 관리자와 직접 통신하거나 관련된 코드를 작성할 일은 결코 없을 것이다. 이들에 관련된 클래스들이 System.Transactions 네임스페이스에 정의되어 있지만 죄다 internal 이나 private 로 선언되어 있기 때문에 정상적으로 이들 클래스에 접근할 수 없으며, 또한 접근할 필요도 없다. 다만 이 글에서 LTM 이니 Oletx 니 설명하는 이유는 앞으로 등장할 여러 기술문서에서 이러한 용어들이 등장했을 때 도움이 되길 바라기 때문이다. 아싸리 말하자면 LTM 이니 뭐니 하는 것들을 몰라도 System.Transacitons 네임스페이스를 활용하는데 큰 지장은 없다. ^^

System.Transactions 네임스페이스의 위상

지금까지 개략(?)적으로 System.Transactions 네임스페이스가 제공하는 트랜잭션 기능과 그 안에 감추어진(?) 원리를 살펴보았다. 이 네임스페이스가 앞으로 닷넷 플랫폼에서 어떤 위치를 차지할 것인가를 생각하지 않을 수 없다. System.Transactions 네임스페이스가 가져다 주는 이점이 무엇이고 기존 미들웨어인 COM+의 관계를 한번 짚어 보도록 하자.

System.Transactions 네임스페이스의 존재 가치

System.Transactions 네임스페이스가 가져다 주는 개발환경과 운영환경은 닷넷 플랫폼에서 많은 장점을 제공한다. 첫째로 System.Transactions 네임스페이스가 제공하는 트랜잭션 프로모션은 SQL Sever 2005를 사용해야 한다는 제약 사항이나, SQL Server 2005만을 사용하더라도 트랜잭션 내에서 2개의 연결이 존재하면 분산 트랜잭션이 사용된다는 약점이 있지만 COM+의 분산 트랜잭션에 비해 더 나은 성능을 선사할 것이라는 것은 의심할 바가 없다.

두 번째 장점은 COM+를 사용했을 때와 달리 베이스 클래스로서 ServicedComponent를 사용해야 한다든가 클래스 단위로 트랜잭션 옵션(Required, Supported 등)을 설정해야 한다던가의 제약이 없다. 따라서 한 클래스 내에서 몇몇 메쏘드는 Required 옵션으로 몇몇 메쏘드는 Supported (실제로 Supported 옵션은 System.Transactions 네임스페이스에 존재하지 않음에 유의하자)로 설계가 가능하다. 더 이상 클래스 이름 뒤에 Tx, NonTx 를 붙여 2개의 클래스 세트를 만드는 일도 필요 없을 것이며, 베이스 클래스를 강요하지 않기 때문에 ASP.NET의 Code-Behind 등에서 자유롭게 사용이 가능할 것이다.

세 번째 장점은 개발 및 운영 상의 장점으로서 COM+ 처럼 컴포넌트를 등록하는 등의 과정이 필요 없다. COM+ 카탈로그에 컴포넌트를 등록하는 작업은 어셈블리를 서명해야 하는 작업부터 시작해서 COM+ DLL이 참조하는 DLL들까지 모두 서명을 요구하는 등 제약사항이 많다. 또한 등록하는 과정 자체가 상당히 불편해서 수십, 수백 개의 COM+ DLL이 사용되는 경우, 자동 등록과 더불어 DLL의 버전 관리에도 상당한 애로점이 존재한다. System.Transactions 네임스페이스는 이러한 등록 과정을 전혀 필요로 하지 않는다.

네 번째 장점으로 System.Transactions 네임스페이스는 명시적인 트랜잭션 시작 및 Commit/Rollback 메쏘드 호출을 수행하지 않아도 되기 때문에 설계 및 프로그래밍을 단순화 시킬 수 있다. 복잡하지 않은 베이스 클래스와 AOP를 동원하면 훌륭한 트랜잭션 프레임워크를 작성할 수도 있다. 닷넷 프레임워크 2.0의 ADO.NET 코드와 Enterprise Services 코드들은 System.Transactions 네임스페이스의 기능을 활용하도록 다시 작성되었다고 한다. 실제로 닷넷 프레임워크 2.0 기반의 ServicedComponent 클래스에서 Transactions.Current 속성을 호출하면 환경 트랜잭션(ambient transaction)객체를 얻을 수 있을 정도니까 말이다.

필자주)
System.Transactions 네임스페이스의 TransactionScope는 COM+의 ServicedComponent 클래스를 사용하는 것에 비해 성능적인 면에서나 트랜잭션 선언, DLL 등록 과정 등 제약 사항이 적은 반면 매번 using 문을 통해 TransactionScope 클래스의 인스턴스를 만들어야 한다는 점이 불편하다. 또한 COM+처럼 AutoComplete 특성(attribute)를 통해 선언적으로 트랜잭션을 커밋 하거나 롤백하는 것이 아니라 명시적으로 코드를 통해 Complete를 호출해야 하는 단점이 있다.

마지막으로, System.Transactions 네임스페이스는 차세대 메시징 미들웨어인 코드네임 인디고(Indigo)의 기반이 될 것이라고 한다. 인디고는 롱혼과 Windows 2003의 메시징 미들웨어로 사용될 것이고 이 인디고의 트랜잭션 처리가 System.Transactions를 바탕으로 이루어진다고 하니, 관심을 안 가질래야 안 가질 수 없는 것이 되어 버린 것이다.

필자주)
2006년 8월 현재 인디고(Indigo)라는 코드 네임은 더 이상 사용되지 않고 .NET Framework 3.0의 WCF(Windows Communication Foundation)이란 이름을 사용한다. WCF는 독립적인 제품이 아니라 .NET Framework 3.0의 일부이다.

COM+와 System.Transactions 네임스페이스

지금까지 살펴본 바 대로 System.Transactions 네임스페이스의 여러 클래스들의 인터페이스 방법이나 행동 방식은 COM+의 그것과 매우 흡사함을 알 수 있었을 것이다. 트랜잭션 옵션, 트랜잭션 결과를 결정하기 위한 투표 방식, TransactionScope 의 중첩 등등은 COM+ 와 매우 유사하다. 그렇다면 COM+는 이제 더 이상 존재가치가 없어 지는 것일까? 더욱이 System.Transactions 네임스페이스는 앞으로 등장할 차세대 미들웨어인 코드 네임 인디고(Indigo)가 이 네임스페이스를 기반으로 할 것이라니 이제 COM+는 죽어가고 있는 것인가?

필자의 생각은 ‘아니요’ 이다. 비록 System.Transactions 네임스페이스가 COM+를 대체할 수 있을 만큼 트랜잭션 기능을 제공하지만 COM+는 System.Transactions 네임스페이스에는 없는 기능들을 여전히 갖고 있다. Object Pooling 이나 COM+ Event, Queued Component, DCOM을 이용한 여러 컴퓨터들 사이의 분산 트랜잭션 등은 아직 System.Transactions 네임스페이스가 커버하지 못하는 것들이다. 또한 COM+ 컴포넌트는 managed 코드와 unmanaged 코드가 모두 사용할 수 있는 트랜잭션 기반의 미들웨어이다. 비록 닷넷 덕분에 영향력을 잃어가는 것은 사실이지만 System.Transactions 네임스페이스가 COM+를 완전히 대체하지는 않을 것이 확실하다. 또한 인디고에 관련된 FAQ를 읽어보면 확실히 인디고는 COM+를 대체할 것이 아니라고 못 박고 있다.

필자주)
인디고, 즉 WCF는 닷넷 프레임워크 3.0의 일부이다. 따라서 닷넷 프레임워크를 사용하지 않는 unmanaged 코드들의 미들웨어로서 여전히 COM+를 사용해야만 할 것이다.

필자가 이 글을 작성하면서 참고한 자료에 의하면 닷넷 프레임워크 2.0을 기반으로 하여 COM+ 컴포넌트를 사용하면 자동적으로 LTM이 사용되고 필요에 따라 트랜잭션은 프로모션 된다고 기록되어 있었다. 정말 그렇게 된다면, 단 한 줄의 코드도 변경하지 않고 기존 코드를 2.0 기반에서 돌리기만 하면 LTM와 트랜잭션 프로모션이라는 좋은 기능을 사용할 수 있을 것이다(필자의 테스트 결과 SQL Server 2005를 사용했고 단일 데이터베이스 연결만이 사용되었지만 LTM은 사용되지 않았다. 필자의 테스트 코드가 잘못되었는지 베타 버전이라서 그런 것인지는 시간관계상 아직 확인하지 못했다).

탐험기를 마치며

부족한 자료를 가지고 짧은 시간 동안 글을 쓴다는 것은 고역임에 분명하다. 하지만 닷사마 2.0 에 새로이 추가된 System.Transactions 네임스페이스에 대한 탐험은 고역이라기 보다는 즐거움을 선사하는 재미있는 것이였다. 지면과 시간관계상 Transaction 객체의 serialize 에 관계된 부분과 DependentTransaction 클래스에 대한 내용, COM+와의 연동 부분에 대해 상세히 다루지 못한 점이 아쉽다. 하지만 필자는 앞으로 계속 System.Transactions 에 대한 테스트와 연구(?)를 수행할 것이며 여기서 다루지 못한 내용과 새로이 알게된 기능, 내용은 필자의 블로그(http://www.simpleisbest.net)를 통해 계속 알려 나갈 것임을 약속하는 바이다. 독자들의 많은 참여 있기를 바라며 졸필을 마친다.

참고문헌



Comments (read-only)
#re: 2005년 7월호 닷넷 칼럼 ::  System.Transactions 탐방기 / 한스 / 2006-08-29 오후 6:13:00
역시 대단한 내공이십니다.
절반은 모르는 내용이지만 트랜잭션을 좀 더 알게 되어 감사합니다.
전 그냥 명시적으로만 사용하는데 한번 써봐야 겠습니다.
#re: 2005년 7월호 닷넷 칼럼 ::  System.Transactions 탐방기 / 이경원 / 2006-09-15 오전 11:55:00
좋은 포스트 감사합니다... System.Transactions에 대하여 조금이나마 감을 잡았네요^^

#re: 2005년 7월호 닷넷 칼럼 ::  System.Transactions 탐방기 / whitezone / 2006-09-27 오후 3:39:00
좋은 내용 많이 알고 갑니다.....^^;;
#re: 2005년 7월호 닷넷 칼럼 ::  System.Transactions 탐방기 / 바바 / 2006-10-31 오후 3:28:00
유용한 정보 감사합니다.
그동안 모르고 썻던 클래스였는데 이젠 동작 원리까지 생각해가면서 코딩할 수 있겠네요^^
#re: 2005년 7월호 닷넷 칼럼 ::  System.Transactions 탐방기 / 도우너 / 2006-11-23 오전 10:41:00
좋은글 감사합니다.
사실 작년에도 읽긴 했지만..
지금 COM+ 로 되어있던걸 ( 어차피 object pooling 을 사용을 안했던 거라.. ) System.Transactions 를 이용해서 바꿔야 할까 고민하고 있어서
다시 읽었습니다.
바꿔야할지, 말아야할지.. ^^;;
암튼, 감사합니다. ^^
#re: 2005년 7월호 닷넷 45칼럼 ::  System.Transactions 탐방기 / Ahmet Kaymaz / 2008-01-04 오후 7:02:00
C# SQL Server Oracle örnek ve kodları
Ahmet Kaymaz
http://www.ahmetkaymaz.com
#re: 2005년 7월호 닷넷 칼럼 ::  System.Transactions 탐방기 / yseok2e / 2008-04-30 오후 6:32:00
System.Transactions 을 사용하는 것은 좋으나, .NET 엔터프라이즈 아키텍쳐의 BIZ / DAC 을 구성하는 Tier 별 구성이 모호해 질 수 있지 않나 생각해 봅니다.
COM+ 에서 주로 처리하던 것을 Code Behind 에서 처리 할때의 issue 도 생각해봐야 하지 않나 생각합니다.
#re: 2005년 7월호 닷넷 칼럼 ::  System.Transactions 탐방기 / AquaMacker / 2009-09-09 오후 5:49:00
님의 킹왕짱이3333 !!!
- 그루소 블레이드마스터 AquaMacker