이전에 Mocking Framework로 Rhino Mocks에 대하여 링크로 간단한 소개글을 썼었습니다. 본 글에서는 Mocking Framework는 왜 필요한지와 Rhino Mocks에 대한 Practice에 대하여 설명하고자 합니다.

먼저 아래의 간단한 코드로 시작합니다.
[TestMethod]

public void GenerateMock_Vs_GenerateStub()

{

    // 1. GenerateStub

    var stubRepository = MockRepository.GenerateStub<IRepository>();

 

    stubRepository.Property = 1;

    Assert.AreEqual(stubRepository.Property, 1);

 

    // 2. GenerateMock

    var mockRepository = MockRepository.GenerateMock<IRepository>();

 

    mockRepository.Property = 1;

    Assert.AreEqual(mockRepository.Property, 0);

 

    mockRepository.Stub(t => t.Property).Return(1);

    Assert.AreEqual(mockRepository.Property, 1);

}

=> Rhino Mocks에서는 MockRepository라는 클래스를 사용하여 가짜 개체를 만들어 주는 것을 볼 수 있습니다. 가짜 개체를 만들 때에 대상이 되는 타입은 인터페이스입니다. 특정 타입이 아닌 인터페이스를 통해서 가짜 개체를 만들어주고 있습니다. 바로 여기서 Mocking Framework를 사용하는 한 가지 이유가 보입니다. 화면과 통신을 각각 담당하여 개발을 한다고 하면 화면을 담당하는 개발자는 통신을 담당하는 개발자가 모든 개발을 하기 전까지 화면에 통신 데이타를 보여주지 못할 것입니다. 하지만 이렇게 인터페이스와 Mocking Framework를 사용한다면 통신 개발이 완료되지 않았다고 하더라도 데이터를 보여줄 수 있습니다. 어떻게? (물론 실제 통신 클래스에서 가짜 데이터를 만들어서 넣어줘도 화면에 보여줄 수는 있습니다.) 바로 위의 코드에 답이 있습니다. StubRepository는 바로 Property라는 이름의 프로퍼티에 값을 할당하는 것으로 MockRepository는 Stub() 메서드를 사용하여 값을 할당하는 방법입니다. Stub과 Mock의 사용 방법에 대한 차이는 알겠는데 실질적으로 어떠한 상황에서 Stub을 또는 Mock를 사용해야 하는 걸까요?

[TestMethod]

public void The_Difference_Between_Stubs_And_Mocks()

{

    var repository = MockRepository.GenerateMock<IRepository>(); 

    // MockRepository.GenerateStub<IRepository>();

 

    var user = new User { Name = "Old User" };

 

    repository.Stub(t => t.GetUser("Old User")).Return(user);

 

    repository.Expect(t => t.Save(user));

 

    var controller = new LoginController(repository);

    controller.ChangeUserName("Old User");

 

    repository.VerifyAllExpectations();

}

=> 먼저 Mock(GenerateMock()메서드 사용)은 위의 Expect()메서드와 같이 실행이 예상되는 메서드를 지정하고 마지막 줄의 VerifyAllExpectatation()로 검증 작업을 합니다. 여기서 검증 대상이 되는 실질적인 클래스는 어떤 것일까요? 바로 LoginController입니다. 해당 코드 중에 new키워드를 사용하여 인스턴스화 한 클래스는 User와 LoginController입니다. Rhino Mocks에서는 바로 이렇게 실질적으로 인스턴스화 한 클래스에서 검증을 합니다. 해당 내용은 마지막 부분에서 Event와 관련된 코드에서 다시 한 번 살펴보도록 하겠습니다.
그러면 검증 대상이 된 LoginController는 어떻게 구현이 되어 있는지 살펴보겠습니다.
public class LoginController
{
    IRepository repository;

    public LoginController(IRepository repository)
    {
        this.repository = repository;
    }

    public void ChangeUserName(string userName)
    {
        var user = repository.GetUser(userName);

        user.Name = "New User";
        repository.Save(user);
    }
}
=> 위의 테스트 코드를 실행시키면 Pass가 됩니다. 주석 처리된 부분을 Stub으로 변경하여도 Pass가 됩니다. 그렇다면 차이점이 없는 것일까요? LoginController에서 Save()메서드가 호출되는 부분을 주석처리 하고 Stub 테스트 코드를 실행하면 오류가 발생하지 않습니다. 정리하면 Expect()메서드로 호출될 것이라 예상된 메서드가 실행이 되든 되지 않든 Pass가 된다는 말입니다. Mock의 경우에는 Fail이 발생합니다. 이것으로 둘의 차이점과 언제 Mock을 언제 Stub을 사용해야 할지를 판단할 수 있습니다. Expect()메서드를 사용 유무로 또한 차이점이기는 하지만 해당 Expect()메서드는 Rhino Mocks에서 제공하는 메서드일 뿐입니다.(아래의 참고 링크를 보시면 Stub에서도 호출 여부를 판단할 수 있는 방법도 존재합니다.) Stub으로 만드는 개체의 경우에는 해당 개체의 행위 또는 값이 해당 테스트 코드와의 상관없거나 영향을 미치지 않는 개체일 경우에 사용하면 되는 것입니다. 예를 들면 어떤 기능을 구현하는 코드는 완성이 되었지만 해당 코드에 보안 관련 기능은 아직 구현이 안되어다고 한다면 보안 관련 기능은 Stub 개체로 만들어서 테스트 코드를 작성하면 되는 것입니다.

기본편에서 살펴볼 마지막은 Event와 관련된 코드입니다.
[TestMethod]

public void Event_Registration()

{

    var view = MockRepository.GenerateMock<Form>();

 

    var presenter = new Presenter(view);

 

    view.Raise(t => t.Load += nullthisEventArgs.Empty);

 

    Assert.IsTrue(presenter.OnLoadCalled);

}

 

public class Presenter

{

    public bool OnLoadCalled { getset; }

 

    private Form loginForm;

 

    public Presenter(Form loginForm)

    {

        this.loginForm = loginForm;

        this.loginForm.Load += new EventHandler(loginForm_Load);

    }

 

    void loginForm_Load(object sender, EventArgs e)

    {

        OnLoadCalled = true;

    }

}

=> 위의 코드에서는 Mock 개체를 인터페이스가 아닌 Form클래스를 대상으로 만들었습니다. Raise()메서드가 해당 코드의 핵심 코드이며 Load이벤트가 발생 여부를 확인의 대상이 되는 클래스는 Presenter 클래스입니다. 이전과 마찬가지고 실질적으로 new키워드를 사용하여 인스턴스화 된 클래스가 검증 대상 클래스가 되며 이러한 이유는 Mock 개체는 일반적으로 인터페이스를 개체화 하기 때문입니다. 즉, 실질적인 생성된 검증 대상 클래스가 있어야 로직(여기서는 Load이벤트)가 실질적으로 발생했는지를 알 수 있기 때문입니다.

본 글에서는 Rhino Mock를 사용하는 기본적인 방법을 살펴보고 해당 방법을 통하여 Mocking Framework가 필요한 이유와 실질적으로 어떠한 상황에서 사용하는지를 간단히 살펴보았습니다.(물론 저의 생각.. ㅋㅋ)

다음 편을 쓸지는 장담할 수 없지만 DB 대신의 저장소의 역할을 하는 예제와 UI와 관련된 테스트 코드 작성 등등 좀 더 실질적으로 사용할 수 방법을 공유하도록 하겠습니다.

참고 사이트
http://ayende.com/wiki/Rhino+Mocks+3.5.ashx
http://builds.hibernatingrhinos.com/builds/Rhino-Mocks
Posted by resisa
,

이번 포스트에서는 Visual Studio에서 빌드를 하면서 생성되는 폴더와 파일들에 대해서 알아보고자 합니다.
모든 프로젝트에는 아래의 그림과 같이 Configuration과 Platform이 있습니다.


=> Configuration은 Debug, Release를 기본값으로 해서 사용자가 정의할 수도 있습니다. Platform은 x86, x64, Any CPU 등등이 있습니다.

일단 윈폼 프로젝트를 하나 생성하고 빌드를 해본 이후에 Platform을 윈폼 프로젝트의 기본값(x86)이 아닌 다른 값으로 복사를 하고 빌드를 해봅니다. 그렇다면 아래와 같은 구조로 폴더가 생성된다는 것을 확인하실 수 있습니다.

일단 
obj폴더와 bin폴더가 만들어지며 Platform이 Any CPU일 경우에는 obj와 bin폴더 바로 하위에 Debug, Release폴더가 만들어지는 것을 확인할 수 있습니다. 만약 다른 Platform을 지정하신 후 빌드하신 다면 해당 Platform폴더가 먼저 만들어지고 Debug, Release폴더가 만들어지는 것도 확인하실 수 있습니다. 확인만 안해봤을 뿐 이해가 되는 구조로 폴더가 만들어지고 있습니다. ^^

그럼 이제 obj폴더와 bin폴더 하위에 어떤 파일들이 생성되는지를 알아보겠습니다.
먼저 다음과 같은 사전 작업을 합니다.
  1. 클래스 라이브러리 프로젝트를 하나 추가한 이후에 윈폼 프로젝트에서 해당 클래스 라이브러리 프로젝트를 참조
  2. 윈폼 프로젝트의 Form1.cs파일을 클릭하면 디자이너 화면을 로드
  3. 빌드

먼저 obj\Debug 폴더 안에 파일을 분석해보고 bin\Debug을 살펴보도록 하겠습니다.
 =>  리소스 관련 파일 3개와 절대 경로 파일 목록 텍스트 파일이 있습니다. 그리고 빌드 완료된 어셈블리와 pdb파일이 보입니다. 그 외에 3가지 파일에 대해서 살펴보도록 하겠습니다.

DesignTimeResolveAssemblyReferenceInput.cache
 => Visual Studio(IDE)에서도 내부적으로 당연히(!) 리플렉션을 사용합니다. 해당 파일은 테스트 결과 참조를 추가/삭제하던지 *.resx나 *.settings파일을 클릭할 경우에 생성됩니다. 해당 파일을 열어보면 Microsoft.VisualStudio.CommonIDE 어셈블리를 로드하는 것을 확인할 수 있습니다. 따라서 Visual Studio 전반적으로 리플렉션을 사용하는 곳에서 해당 파일이 생성되는 것을 알 수 있습니다.

DesignTimeResolveAssemblyReference.cache
 => 해당 파일은 윈폼 프로젝트에서 Form1.cs파일을 클릭하여 디자인 화면에 폼이 보여질 경우에 생기는 파일입니다.

ResolveAssemblyReference.cache
 => 위의 사전 작업에서 윔폼 프로젝트에서 클래스 라이브러리 프로젝트를 참조 후 빌드하였습니다. 바로 이 파일음 참조된 어셈블리와 관련되어 빌드시에 생성되는 파일입니다.

이전 포스트인 '프로젝트 참조 VS 어셈블리 참조' 에서 참조한 어셈블리를 어떠한 우선 순위 경로로 찾는지에 대해서 잠깐 언급을 했었습니다. 위의 3가지 파일 중에 ResolveAssemblyReference.cache가 바로 이러한 결과 값이라고 할 수 있습니다. 만약 클래스 라이브러리 프로젝트를 참조하지 않았다면 해당 파일은 생기지 않습니다.
그렇다면 Visual Studio는 어디서 이러한 정보를 가져오는 걸까요? 이 정보를 변경할 수는 없을까요?
%windir%\Microsoft.NET\Framework\v4.0.30319\Microsoft.Common.targets 파일에 이러한 정보가 있습니다.
줄 1410
<ProjectDesignTimeAssemblyResolutionSearchPaths 
   
Condition=" '$(ProjectDesignTimeAssemblyResolutionSearchPaths)' == '' "
    {CandidateAssemblyFiles}; 
    $(ReferencePath); 
    {HintPathFromItem};
    {TargetFrameworkDirectory};                
    {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),
  $(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)};
    {RawFileName}; 
    $(TargetDir) 
</ProjectDesignTimeAssemblyResolutionSearchPaths

줄 1461

<
DesignTimeAssemblySearchPaths Condition=" '$(DesignTimeAssemblySearchPaths)' == '' ">
    {CandidateAssemblyFiles};
    $(ReferencePath);
    {HintPathFromItem};
    {TargetFrameworkDirectory};
    {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),
  $(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)};
    {RawFileName};
    $(OutDir)
</DesignTimeAssemblySearchPaths>

줄 418
<AssemblySearchPaths Condition=" '$(AssemblySearchPaths)' == ''">
    {CandidateAssemblyFiles};
    $(ReferencePath);
    {HintPathFromItem};
    {TargetFrameworkDirectory};
    {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),
  $(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)};
    {AssemblyFolders};
    {GAC};
    {RawFileName};
    $(OutDir)
</AssemblySearchPaths>


 => 각각의 경로는 *.cache 파일과 매칭됩니다. (디자인 타임과 관련된 사항은 시간이 지나 헷갈리네요. 순서가 매칭 순서가 맞지 않을 수도 있습니다. ^^;)
이전에 .NET 1.1를 Target Framework로 하고 개발 툴을 VS2010에서 사용하려고 할 때 위의 설정을 변경해준 적이 있습니다. 빌드와 디자인 타임에 해당 폼을 보는 것 까지는 성공을 했었는데 도구 상자(ToolBox)가 최소 .NET 2.0으로 설정이 되는 문제와 기타 잠재된 문제도 있을 것으로 예상되어 포기 했었습니다.
여튼 이제 bin\Debug 폴더에 생기는 파일을 보면 최종 결과물인 어셈블리와 pdb파일 그리고 vshost파일이 있는 것을 알 수 있습니다. 빌드시에 필요한 정보인 리소스, 로그, 툴에서 사용하는 정보 등을 obj 폴더 하위에 만들어 사용하고 최종 결과물은 bin폴더 하위에 만드는 구조입니다. 디버깅시에 사용하는 vshost파일의 경우 obj에 있으면 어떨가 잠시 생각을 해봤지만 obj 폴더 하위에는 해당 프로젝트의 결과물만 가지고 있을 뿐 참조하고 있는 어셈블리가 복사되는 것은 아닙니다. bin 폴더에는 참조하고 있는 어셈블리의 Copy Local속성이 True일 경우에는 bin폴더로 복사가 됩니다.
실제 개발과는 별로 상관없는 내용이였지만 Visual Studio를 사용하면서 만들어지는 폴더와 파일에 대한 내용이 궁금해져 한 번 정리를 해보았답니다. ^^
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
,