'DTCTransaction'에 해당되는 글 2건

  1. 2017.06.04 .NET IsolationLevel에 대한 고찰 5
  2. 2010.09.21 COM+ Transaction VS TransactionScope 8

또?! 오랜만입니다 ㅎㅎ 지인이 애드센스 등록을 하셔서 저도 시도를 해보았더니 콘텐츠 부족으로 실패하여 이렇게 새로운 글을 쓰고 있습니다.

어떤 내용을 쓸까 고민을 하다가 블로그를 정리하면서 예전에 썼던 글 중에 그 당시 궁금했던 내용에 대한 답변을 해보고자 합니다.
(벌써 7년이 지났다니 시간이 참 빠릅니다;;)

TransactionScope 그리고 Stored Procedure (2010. 07. 25)
출처: http://resisa.tistory.com/110 [심플하게~]

(중략)...

1. 한 가지 의문점은 SP안에서는 트랜잭션의 Scope 범위(TransactionScopeOption) 지정하는 부분은 없어보이지만 IsolationLevel 대해서는 지정을   있습니다또한 TransactionScope에서도 IsolationLevel 대해서 지정할  있습니다그래서 IsolationLevel 따른 LTM SQLTransaction 관계가 어떻게 될지도 살짝 궁금해집니다. 하지만 실질적으로 이러한 것을 프로젝트에서 정확하게 알고 사용하는 개발자가 얼마나 될지도 의문이며 IsolationLevel 트랜잭션 사이의 잠금(lock) 대한 것이기 때문에 2. 성능보다는 안전성을 중요시하는 우리 나라의 대부분의 프로젝트에서는 ReadCommitted 사용하는 것이 개발자의 정신건강에 이로워 보입니다. ^^

예전에 썼던 글에 은근히 잘못된 내용이 있습니다;;  이유는 이해부족 입니다;;
저는 새로운 기술을 습득하는 과정에서 '정의'를 자신의 방식으로 표현하고 증명하는 과정을 반복합니다. '정의'와 '증명'을 잘못하면 위와 같이 잘못된 글이 나오기도 하지만 경험이 붙으면 증명하는 시간이 짧아지고 단순 실수도 줄어 이해하는 시간이 짧아지고 자신의 표현 방식으로 인하여 오랫동안 기억되는 장점이 있는 방식이라고 생각됩니다. 그럼 잘못된 부분부터 수정해보겠습니다.

=> 1번에서는 개념에 대해서 잘못 설명하고 있습니다. TransactionScopeOption은 TransactionScope클래스의 파라미터로 DB(SP)에는 없는 속성입니다.

=> 2번 대부분의 프로젝트에서 ReadCommitted로 설정할 것이라는 이유는 MSSQL의 기본 IsolationLevel이 ReadCommitted이기 때문입니다. 이 당시에는 IsolationLevel에 대한 이론적인 것만 알고 있었기 때문에 이런 결론을 냈었는데 업그레이드된?! 지금은 대부분의 프로젝트에서는 ReadUncommitted를 사용할 것이라고 생각합니다. 왜냐하면 ReadCommitted로 설정을 하면 DeadLock이 발생하는 구간이 많이 있을 수 있습니다. 그렇기 때문에 직접적으로 ReadUncommitted를 지정하지 않았더라도 이와 동일한 WITH(NOLOCK) 힌트를 많이 사용했을 것이라고 추측됩니다.

그럼 본론으로 들어가서 노란색 부분에 대한 이야기를 풀어보겠습니다.

먼저 트랜잭션을 아래와 같이 2가지 형태로 구분합니다.
1. 로컬 트랜잭션(SQL Transaction)
2. 분산 트랜잭션(DTC Transaction)

1. 로컬 트랜잭션을 세부화 하면 아래와 같이 나눌 수 있습니다.
1.1. TM(Transaction Manager)이 관여하는 SQL Transaction
1.2 TM이 관여하지 않는 SQL Transaction

개념적인 측면에서는 TM의 관여 여부로 나눌 수 있으며 개발적인 측면에서는 프로그램에서 트랜잭션을 관리하는지 DB에서 즉 SP에서 트랜잭션을 관리 여부로 나눌 수 있습니다.

다시 노란색 부분을 보면 SP에서 트랜잭션을 관리하는 부분들이 프로그램에서 트랜잭션을 관리하는 것과 합쳐졌을 경우에 트랜잭션 관리(Connection, IsolationLevel)에 대한 궁금증이였습니다.

프로그램에서 트랜잭션 관리를 하는 방법은 아래와 같습니다.
1. ADO.NET(System.Data.SqlClient.SqlConnection)를 사용하여 직접 BeginTransaction()를 호출하여 사용
2. System.Transactions.TransactionScope를 사용

트랜잭션을 관리하는 1번, 2번 방법의 공통점은 프로그램에서 트랜잭션을 관리하기 때문에 TM이 트랜잭션을  관리한다는 점입니다. 차이점이 바로 이 글의 핵심이 되는 답변으로 IsolationLevel과 관련되어 있습니다. TransactionScope에 IsolationLevel을 지정하여 사용할 때 SP의 IsolationLevel을 변경할 수 없다는 것입니다. IsolationLevel에 대한 위의 네임스페이스를 살펴보면 아래와 같습니다.

1. System.Data.SqlClient.SqlConnection - System.Data.IsolationLevel
2. System.Transactions.TransactionScope - System.Transactions.IsolationLevel

조심스럽게 예측해보면 TransactionScope의 트랜잭션은 DB 트랜잭션이라기보다는 프로그램에서 발생하는 트랜잭션(여기서는 DB 트랜잭션을 포함하는 상위의 트랜잭션의 개념으로 이해)이라고 할 수 있고 IsolationLevel은 트랜잭션 사이의 격리성이기 때문에 프로그램에서 발생하는 트랜잭션에 대한 격리성이지 DB 트랜잭션에 대한 격리성이 아니기 때문에 DB(SP) IsolationLevel을 변경할 수 없는 것입니다.

결론을 맺으면 프로그램에서 DB(SP) 트랜잭션 관리(IsolationLevel)를 하고 싶으면 SqlConnection을 사용하여 직접 트랜잭션을 발생(BeginTransaction())시켜야하며 TransactionScope을 통해서는 DB(SP) IsolationLevel까지는 관리할 수 없다는 것입니다.

Posted by resisa
,
COM+ Transaction과 System.Transaction 네임스페이스 안의 TransactionScope의 트랜잭션 관점에서의 차이점을 어떻게 알고 계시나요? 저는 COM+ Transaction은 무조건 분산 트랜잭션으로 TransactionScope은 ConnectionString에 따라서 로컬 또는 분산 트랜잭션을 사용하는 것으로 알고 있었습니다. 그런데 최근에 COM+ 관련 테스트를 하다가 재미있는 점을 발견하였습니다. COM+ Transaction도 로컬 트랜잭션으로 사용되는 것과 같은 현상(?!)을 보게 되었습니다. 그러면 저에게 이러한 혼란을 준 현상을 SQL Profiler과 Component Services로 트랜잭션을 모니터링 해보도록 하겠습니다.

테스트 환경
 - .NET Framework 4.0
 - Windows 7 64bit Ultimate
 - MS SQL 2008 R1(local에 설치)

먼저 COM+ 관련 예제 코드는 아래와 같습니다.
[Transaction(TransactionOption.Required)]
public class COMTxObject : ServicedComponent
{
    string connStr = "Data Source=(local);Initial Catalog=EFWithSQL;Integrated Security=True;";
    string connStr2 = "Data Source=(local);Initial Catalog=EFWithSQL;Integrated Security=true;";

    [AutoComplete(true)]
    public void SQL_Select()
    {
        string name = Guid.NewGuid().ToString();

        string query = @"
select top 1 [id] from [dbo].[Entity1Set]
";

        using (SqlConnection conn = new SqlConnection(connStr))
        {
            conn.Open();

            SqlCommand cmd = new SqlCommand(query, conn);
            object result = cmd.ExecuteScalar();
            conn.Close();
        }
    }

    [AutoComplete(true)]
    public void SQL_Insert()
    {
        string name = Guid.NewGuid().ToString();

        string query = @"
insert [dbo].[Entity1Set]([Name])
values ('" + name + @"')
select [Id]
from [dbo].[Entity1Set]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
        ";

        using (SqlConnection conn = new SqlConnection(connStr))
        {
            conn.Open();
            SqlCommand cmd = new SqlCommand(query, conn);
            int result = cmd.ExecuteNonQuery();
            conn.Close();
        }

        SQL_Insert1_1();
        
    }

    void SQL_Insert1_1()
    {
        string name = Guid.NewGuid().ToString();

        string query = @"
insert [dbo].[Entity1Set]([Name])
values ('" + name + @"')
select [Id]
from [dbo].[Entity1Set]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
        ";

        using (SqlConnection conn = new SqlConnection(connStr))
        {
            conn.Open();
            SqlCommand cmd = new SqlCommand(query, conn);
            int result = cmd.ExecuteNonQuery();
            conn.Close();
        }
    }

    [AutoComplete(true)]
    public void SQL_Insert2()
    {
        string name = Guid.NewGuid().ToString();

        string query = @"
insert [dbo].[Entity1Set]([Name])
values ('" + name + @"')
select [Id]
from [dbo].[Entity1Set]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
        ";

        using (SqlConnection conn = new SqlConnection(connStr))
        {
            conn.Open();
            SqlCommand cmd = new SqlCommand(query, conn);
            int result = cmd.ExecuteNonQuery();
            conn.Close();
        }

        SQL_Insert2_1();
    }

    void SQL_Insert2_1()
    {
        string name = Guid.NewGuid().ToString();

        string query = @"
insert [dbo].[Entity1Set]([Name])
values ('" + name + @"')
select [Id]
from [dbo].[Entity1Set]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
        ";

        using (SqlConnection conn = new SqlConnection(connStr2))
        {
            conn.Open();
            SqlCommand cmd = new SqlCommand(query, conn);
            int result = cmd.ExecuteNonQuery();
            conn.Close();
        }
    }
}
=> DB는 Entity1Set이라는 테이블에 ID, Name을 컬럼으로 가지고 있으며 ID의 경우에는 int형의 IDENTITY로 지정하여 자동으로 증가하게 되어 있습니다. COM+ 이기 때문에 ServicedComponents를 상속받고 있으며 해당 클래스 어트리뷰트로 TransactionOption이 Required로 지정된 Transaction 어트리뷰트가 지정되어 있습니다. 세 개의 메서드가 존재하는데 하나는 조회를 하는 쿼리이며 두 개는 ConnectionString이 동일한 상태에서의 삽입이고(SQL_Insert()), 다른 하나는 ConnectionString이 다른 상태에서의 삽입(SQL_Insert2())입니다. 각각 메서드 위에는 자동으로 Complete를 지정해주는 AutoComplete라는 어트리뷰트가 선언되어 있습니다. (참고 : COM+에서는 수동으로 Commmit, Rollback를 해주기 위해서 ContextUtil이라는 클래스를 사용하기도 합니다.)
그리고 해당 어셈블리를 강력한 이름으로 서명을 해주고 AssemblyInfo.cs파일에 ComVisible을 true로 변경해 준 후에 빌드를 하고 Regsvcs.exe를 사용해서 COM+ 카달로그에 등록을 해주었습니다.
그러면 조회 메소드(SQL_Select)를 실행하였습니다.
1. SQL Profiler 결과
=> 헛.. 이상합니다. COM+는 분산 트랜잭션을 사용하기 때문에 DTCTransaction이 보여야 하는데 보이지 않습니다.

2. Component Services 결과
=> 헛.. 역시 이상합니다. COM+ Transaction이 발생하였는데 DTCTransaction이 보이지가 않습니다.

이번에는 조회 메소드 안에서 강제로 예외를 발생시켜 보았습니다.
1. SQL Profiler 결과
=> 드디어 DTCTransaction이 보입니다. Rollback이 정상적으로 됩니다.

2. Component Services 결과
=> 역시 Aborted된 트랜잭션의 개수가 보입니다. 헛헛.. 그렇다면 COM+ 트랜잭션이 정상적으로 Commit이 될 경우에는 로컬 트랜잭션을 사용하고 Rollback이 될 경우에는 분산 트랜잭션을 사용한다는 말입니다. 지금까지 COM+ Transaction은 분산 트랜잭션을 사용해서 느리다라고 알 고 있었는데 제가 먼가 새로운 것을 발견한걸까요? ㅎㅎㅎ;;;
여기서 몇 가지 테스트를 더 해보기 하였습니다.
(참고 : Regsvcs로 컴포넌트를 등록해줄 경우에도 DTC를 사용하여 Committed 개수가 증가합니다. 따라서 모든 테스트 전에는 DTC Service를 멈추었다가 다시 시작해주었습니다.)

먼저 로컬 트랜잭션과 분산 트랜잭션 과연 성능의 차이는 얼마나 있는 것일까요? (성능은 환경적인 요인에 따라 다르게 측정되기 때문에 여기서는 단순한 비교(?!)를 통해서 대략 이렇다 정도로만 테스트를 합니다.)
이번에는 조금 더 많이 사용해 본 TransactionScope클래스를 사용해서 테스트를 해보았습니다.
코드는 아래와 같습니다.
Stopwatch watch = new Stopwatch();
            
for (int i = 0; i < 1000; i++)
{
    using (TransactionScope scope = new TransactionScope(System.Transactions.TransactionScopeOption.Required))
    {
        var helper = new SqlHelper();
        watch.Start();
        helper.SQL_Insert();
        //helper.SQL_Insert2();
        scope.Complete();
    }
    watch.Stop();
}
=> SqlHelper클래스는 이전에 사용한 COMTxObject클래스에서 COM+관련 부분만을 제거한 클래스입니다. 주석 처리된 부분을 보면 해당 테스트가 어떻게 될 것인지 아실 수 있습니다. 먼저 동일한 ConnectionString으로 삽입되는 SQL_Insert()메소드를 호출해보고 ConnectionString이 다른 SQL_Insert2()메소드를 호출하여 실행시켜서 로컬, 분산 트랜잭션의 결과를 비교해 보는 것입니다. 실행 시간을 측정하기 위해서 Stopwatch클래스를 사용하였으며 COM+의 객체 생성시간이 더 오래 걸리기 때문에 트랜잭션과 관련이 있는 메소드의 실행시간만을 측정하기 위해서 메소드 실행 전 후로 Start, Stop 메소드를 호출해주고 있습니다.
먼저 로컬 트랜잭션(1000번)으로 처리하였을 경우에는 대략 3~5초 정도의 시간이 걸립니다. 분산 트랜잭션으로 처리하였을 경우에는 대략 6~9초 정도의 시간이 걸렸습니다. SQL Profiler에서는 로컬시에는 TM(Transaction Manger) 분산시에는 DTCTransaction을 사용하는 것으로 보였고 Component Service에서는 로컬 트랜잭션일 경우에는 아무런 반응이 없다가 분산 트랜잭션을 사용할 경우에 아래와 같이 Commited 숫자가 증가하였습니다.
=> 총 5번을 실행(1000*5) 한 결과입니다.

이번에는 COM+ Transaction으로 동일한 테스트를 해보았습니다.
코드는 아래와 같습니다.
Stopwatch watch = new Stopwatch();

for (int i = 0; i < 1000; i++)
{
    var com = new COMPlusLibrary.EFCOMTxObject();
    watch.Start();
    com.SQL_Insert();
    //com.SQL_Insert2();
    watch.Stop();
}
=> SQL_Insert()를 실행시켰을 때 SQL Profiler에서는 TM만 보이며 Component Service에서 역시 아무런 변화가 없습니다. 그런데 로컬 트랜잭션 이였다고 말하기에 한 가지 이상한 점이 있습니다. 실행시간이 조금 더 걸린다는 겁니다. 거의 TransactionScope을 분산 트랜잭션으로 처리하는 것과 비슷한 시간입니다. SQL_Insert2()를 실행시켰을 때 SQL Profilier에서는 DTCTransaction이 보이며 시간은 더 오래 걸립니다. 그래서 해당 부분을 다시 여러번 실행시켜 실행시간을 다시 측정해보았습니다.
   COM+ Transaction TransactionScope 
 로컬 트랜잭션  4.4~6.3  3.7~5.2
 분산 트랜잭션  7.3~10.4  6.1~9.2
=> 1000번씩 실행되는 구문을 20번 실행시켜 보고 최소값과 최대값을 제외하고 실행 시간의 범위를 표시해보았습니다. 여기서는 실행 모드(Ctrl + F5)로 측정한 값입니다. 로컬이든 분산이든 우리가 이미 알고 있듯이 COM+ Transaction보다는 TransactionScope의 성능이 더 뛰어나다고 말할 수 있습니다. (객체의 생성시간을 제외하고 측정한 시간이기 때문에 COM+ Transaction에게 더 유리한(?!) 측정이였습니다.)

2011-02-18 수정내용
성능 관련 측정을 다시 해보니 문제가 될만한 의심되는 부분이 있었습니다. 정확한 결과는 정확한 확인 이후에 다시 올리도록 하겠습니다.


COM+ Transaction 사용시에 로컬 트랜잭션을 사용한다는 것은 DTC를 사용하지 않는다는 말이 될 수도 있다는 생각에 DTC Service를 종료한 이후에 COM+ TransactionOption을 주고 실행해보았습니다. 아래와 같은 결과를 얻을 수 있었습니다.
 TransactionOption DTC 사용 여부 
 Disable  X
 Not Supported  X
 Supported  X
 Required  O
 RequiredNew  O
=> 물론 여기서 Supported 옵션은 해당 클래스(COMTxObject)의 메소드가 실행되기 전에 트랜잭션이 발생된 이후에 실행되었다면 이전 트랜잭션에 참여하기 때문에 DTC를 사용할 수도 있습니다. DTC를 사용해야지만 트랜잭션이 발생한다는 것을 알 수 있습니다. 또한 DTC를 사용했다고 해서 꼭 DTCTransaction이 발생하는 것은 아니라는 점도 알 수 있습니다.

결론을 내기 전에 지금까지 사용된 용어들을 정리해보는 차원에서 유수석님의 글을 참조 하겠습니다.
http://www.simpleisbest.net/archive/2005/08/23/208.aspx

 분산 트랜잭션이란 네트워크에 분산되어 있는 자원들에 대해 트랜잭션을 수행하는 것을 말한다. 예를 들어, 어떤 애플리케이션이 SQL 서버와 오라클에 데이터를 트랜잭션 하에서 수행해야 한다면 분산 트랜잭션을 필요로 할 것이다. 대개의 경우 분산 트랜잭션은 턱시도, 티맥스, 엔트라와 같은 TP 모니터나 COM+, EJB 등과 같은 컴포넌트 기반 미들웨어가 그 기능들을 제공한다.

분산 트랜잭션의 반대되는 개념으로서 로컬 트랜잭션은 단일 자원(데이터베이스)에 대한 커밋과 롤백을 수행하며 1-phase 커밋으로서 트랜잭션을 수행한다. 한편 분산 트랜잭션의 핵심은 2-phase 커밋으로 볼 수 있다. 커밋(Commit)과 취소(Abort)(분산 트랜잭션에서는 롤백(rollback)보다는 abort라는 용어를 사용한다)를 수행하는데 두 단계를 거친다는 말이다. 첫 번째 phase는 준비 단계(prepare phase)로서, 데이터베이스는 커밋을 위한 모든 준비를 수행한다. 분산 트랜잭션에 참여한 모든 데이터베이스가 준비 단계를 성공한 후에야 두 번째 phase인 커밋 단계가 수행되는 것이다. 만약 분산 트랜잭션에 참여한 어느 한 데이터베이스라도 준비 단계를 실패한다면 두 번째 phase는 커밋이 아닌 취소를 수행하게 될 것이다.

이렇게 각 데이터베이스에게 준비 혹은 커밋/취소의 두 단계를 수행하도록 트랜잭션을 제어하는 트랜잭션 관리자를 TM (Transaction Manager)이라 부르며 이는 MSDTC(Microsoft Distributed Transaction Coordinator)가 담당한다. 지금까지 단순히 분산 트랜잭션에 참여하는 ‘데이터베이스’라고 참조했지만 데이터베이스 외에 다른 자원들도 분산 트랜잭션에 참여할 수 있다. 대표적인 예로 MSMQ, COM+의 Shared Property가 이에 해당된다. 따라서 분산 트랜잭션에서는 트랜잭션에 참여하는 각 자원에 대한 관리를 맡는 RM(Resource Manager)이 존재한다. RM의 대표적인 예는 오라클, SQL 서버와 같은 데이터베이스를 들 수 있다.

요약해 보면, COM+ 컴포넌트의 분산 트랜잭션의 관리는 MSDTC와 같은 TM을 통해 수행하며 TM은 트랜잭션에 참여한 RM들(SQL 서버, 오라클, MSMQ 등)에게 2-phase 커밋을 수행한다는 점이다.


지금까지의 여러 가지 테스트 결과를 확인해보면 COM+ Transaction에서도 로컬과 분산 트랜잭션이 모두 발생하며 동일한 ConnectionString일 경우에 정상적으로 Commit이 될 경우에는 로컬 트랜잭션으로 Rollbakc이 될 경우에는 분산 트랜잭션을 사용하는 것으로 알 수 있습니다. 하지만 COM+ Transaction은 TransactionScope를 사용하는 경우보다 성능적인 면에서 떨어지며 TransactionScope의 경우에 동일한 ConnectionString일 경우에 Commit, Rollback의 여부에 상관없이 모두 로컬 트랜잭션이 발생하며 MSDTC 또한 사용하지 않습니다.
Posted by resisa
,