메일 발송 서비스를 개발하기 위해서 검토를 진행하였습니다.

메일 발송과 같은 도메인(비지니스) 로직이 없고 복잡한 기술 중심의 기능이 아니면 우선순위가 낮아 일단 안전하게 실행만 된다는 생각으로 별도의 검토 없이 개발을 진행했습니다. 최근에는 마음의 여유가 생겨 우선순위가 낮은 부분에 대해서도 의미를 부여하고 부여된 의미만큼 결과에 만족하는 긍정적인 사이클이 형성되고 있는 것 같습니다.

기존에 해왔던 방식은 System.Net.Mail.SmtpClient을 사용하던 방식입니다. 사용방식이 어려운건 아니라 그냥 사용해도 무방합니다. 다른 방법이나 아이디어를 얻기 위해서 오픈 소스를 검색해보았습니다.


1번째로 살펴볼 FluentEmail의 사용하는 코드는 아래와 같습니다.

 // Using Razor templating package

Email.DefaultRenderer = new RazorRenderer();

var template = "Dear @Model.Name, You are totally @Model.Compliment.";

var email = Email
    .From("bob@hotmail.com")
    .To("somedude@gmail.com")
    .Subject("woo nuget")
    .UsingTemplate(template, new { Name = "Luke", Compliment = "Awesome" });

=> 사용이 편리해 보이지만 String.Format이나 $"{}"을 사용하면 동일하게 템플릿이 작성되는거 같아 잔기술 느낌이 납니다.

=> Fluent라는 이름이 붙으면 Method Chaining(.을 붙여서 연결하는 것)과 기존 클래스들을 랩핑하는 느낌인데 코드의 양이 엄청 줄거나 사용을 쉽게 하는 것외에 다른 핵심적인 내용이 없어 보입니다.


2번째로 살펴볼 오픈 소스는 PreMailer.Net 입니다. stylesheets에 선언된 내용이 inline style attribute로 추가해주는 라이브러리입니다. 일단 제가 원하는 것은 메일 발송이였기 때문에 거리는 있지만 이메일 양식과 관련된 내용이라 좀 더 살펴보았습니다.


[TestMethod]

public void MoveCssInline_RespectExistingStyleElement()

{

string input = "<html><head><style type=\"text/css\">.test { height: 100px; }</style></head><body><div class=\"test\" style=\"width: 100px;\">test</div></body></html>";


var premailedOutput = PreMailer.MoveCssInline(input, false);


Assert.IsTrue(premailedOutput.Html.Contains("<div class=\"test\" style=\"height: 100px;width: 100px"));

}

=> 해당 라이브러리의 기본 기능은 head 사이에 style이 div 태그 사이에 style에 적용되는 것입니다. 그 외에도 HTML태그에 대한 대문자 -> 소문자로 변경해준다던지 유효하지 않은 문법도 확인 할 수 있습니다. 그렇다면 이런 라이브러리는 왜 필요한 것일까요? 앞에 기능을 중심으로 생각해보면 외부 링크로 설정된 스타일이 포함된 경우 메일로 전송을 하게 되면 보안상(메일을 열었을 때 외부 링크에서 스타일을 받아와야 하므로) 좋지 않기 때문으로 추측됩니다.


3번째로 살펴볼 오픈 소스는 MailKit 입니다. 기존에 SmtpClient을 사용한 것이 아니라 RFC(표준)를 준수하며 크로스 플랫폼에서 사용이 가능하도록 구현한 라이브러리입니다. .NET에서 크로스 플랫폼 이야기가 나오면 .NET Core를 떠올리게 되는데 SmtpClient은 .NET Core에 포함되어 있지 않기 때문에 리눅스 환경에서는 MailKit을 사용하면 메일 발송을 할 수가 있습니다. 

MailKit에는 추가적으로 더 의미가 있는 점이 있는데 바로 성능이 우수하다는 것입니다. 성능이 우수한 이유를 PIPELINING 기술을 통해서 설명하고 있습니다.

https://tools.ietf.org/html/rfc2920


아래는 그림은 HTTP PIPELINING에 해당하는 그림으로 해당 용어에 대한 개념을 이해하기가 더 쉽습니다.


=> 왼쪽 그림은 요청에 대한 응답이 오기 전까지 다른 요청을 할 수 없는 구조이지만 오른쪽 그림은 요청과 응답에 대한 병렬처리가 가능하다는 것이 핵심입니다. HTTP PIPELINING은 HTTP 1.1부터 지원이 된다고 하며 자세한 내용은 아래 위키를 참고 하시면 됩니다.

https://en.wikipedia.org/wiki/HTTP_pipelining


결론적으로 크로스플랫폼 지원과 성능이 우수한 MailKit 을 사용하려고 하였는데 StackExchange.Exceptional에 예외 로그를 저장하면서 메일을 전송해주는 부분이 함께 있어 해당 부분을 사용하기로 결정하였습니다. 해당 부분은 예외 로그에 대한 메일 전송이고 메일 발송 서비스만을 위해서는 MailKit과 PreMailer.NET까지 고려해서 차후에 개발해볼 예정입니다.

P.S : 7월22일에 처음 썼던 글인데 게으름이 생겨 이제야 마무리를 했네요 ^^;


Posted by resisa
,

이번 글에 대한 제목을 정하기가 어렵네요.

상황 설명을 해보자면 DB테이블에 nvarchar 타입에 json 포맷으로 된 값이 있고 이 값을 API로 전달해주면 됩니다.

리턴되는 데이터는 아래와 같습니다.


 "{ \"Key\": 1 " }"

리턴되는 타입이 string이고 그 안에 키 또는 string 값에 "문자를 필요로 하기 때문에 \문자로 string안에 string을 구분해주고 있는 것입니다.

이렇게 리턴을 하였을 경우에도 JSON.NET에서는 클래스(object)로 역직렬화가 가능합니다. 하지만 다른 언어의 다른 json 직렬화 라이브러리의 경우에는 에러가 발생하기도 하며 API 명세서에 노출하기에도 깔끔하지 못해보입니다.


최초 DB에서 받아오는 json string에 대해서 클래스(object)를 만들면 간단하게 해결됩니다. 하지만 여기서는 json string에 대한 포맷이 주기적으로 변경될 수도 있고 그에 따른 변경사항에 대해서 약결합을 만들어야 하는 상황입니다.

다음으로 생각해볼 수 있는 건 API에서 string이 아닌 타입으로 넘기면 되지 않을까 싶었고 검색을 해보았습니다.


https://stackoverflow.com/questions/22906010/deserialize-dynamic-json-string-using-newtonsoft-json-net


ExpandoObject 라는 System.Dynamic 네임스페이스의 클래스가 있었네요. JSON.NET으로 아래와 같이 역직렬화를 해주고 있습니다.


var converter = new ExpandoObjectConverter();

dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(string, converter);

=> JSON.NET에서 ExpandoObjectConverter를 제공해주고 있습니다. 해결은 아주 간단하게 되었고 ExpandoObject를 살펴보니 IDictionary<string, object>를 상속받고 있습니다. 아하~ json string을 아래처럼 Dictionary로 만들어주고 리턴을 해도 되겠군요.


Dictionary<string object> dic = new Dictionary<string object>();

dic.Add("Key", 1);

=> 그렇다면 Dictionary가 아닌 ExpandoObject 사용하는 것에 대한 장점은 무엇일까요?

1. 복잡한 json string일 경우(특히 계층 관계)

2. ExpandoObject가 INotifyPropertyChanged 인터페이스를 상속받고 있는 걸로 봐서는 프로퍼티 변경에 대한 통지(알림)를 받을 수 있네요

3. 이벤트도 추가할 수 있습니다.


해결은 간단하게 되었고 직렬화에 대한 이해도 살짝 올라갔네요. ㅎㅎ

Posted by resisa
,

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

어떤 내용을 쓸까 고민을 하다가 블로그를 정리하면서 예전에 썼던 글 중에 그 당시 궁금했던 내용에 대한 답변을 해보고자 합니다.
(벌써 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
,

이번 포스트에서는 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
,


드디어 Strored Procedure(이하 SP)를 사용하는 프로젝트에 참여하고 있습니다. 아쉽게(?) 실질적으로 SP를 호출하고 결과를 받아서 UI에 보여주는 작업은 아닙니다.

SP로 실질적인 프로젝트를 하지 않아서 전부터 궁금하던 사실이 있었습니다. 'SP를 사용하면 굳이 서버쪽에 Layer가 필요한 것일까?' 란 의문이였습니다. 프로젝트의 규모가 작고 하나의 SP에서 대부분의 로직을 처리할 수 있다면 굳이 필요하지 않아보입니다만 또 하나의 의문은 SP로 작성된 구문 사이의 트랜잭션의 범위에 대한 의문이였습니다. 일반적으로 Biz Layer에서 트랜잭션을 발생시켜 주고 Dac Layer에서 여러 SP를 호출해줄 경우 Biz에서 발생하는 트랜잭션과 SP내의 트랜재션과의 관계와 그 결과에 대한 의문이였습니다. 

조금 더 이해를 돕기 위해서 Biz Layer에서는 TransactionScope 클래스를 사용하였다고 가정하고 SP내에서는 ADO.NET 트랜잭션 메소드(BeginTransaction/Commit/Rollback)를 사용하였다는 가정입니다. TransactionScope는 System.Transaction 네임스페이스 하위의 클래스이며 System.Transaction 네임스페이스에서 관리하는 트랜잭션은 항상 LTM(Lightweight Transaction Manager)을 통해서 관리된다고 합니다. 따라서 해당 테스트는 LTM과 ADO.NET 트랜잭션 메소드 사이의 관계에 대해서 설명하자고 하는 글입니다.

LTM에 대해서는 아래의 사이트를 참고하세요.

http://www.simpleisbest.net/articles/996.aspx


그럼 이제 테스트를 해보도록 하겠습니다. (참고 : 테스트 DB는 SQL Server 2008(R2는 아님)입니다.)

DB 데이터 베이스는 ID(identity), Name 가지고 있는 테이블입니다.

이제 SP를 DB에 만들어 보겠습니다. SP의 내용은 아래와 같습니다.

CREATE PROCEDURE EFSP

 

@name nvarchar(Max),

@txOutput int OUTPUT

 

AS

 

BEGIN TRAN

 

insert [dbo].[Entity1Set]([Name])

values (@name)

select [Id]

from [dbo].[Entity1Set]

where @@ROWCOUNT > 0 and [Id] = scope_identity()

 

-- 위의 insert 구문 2 추가

 

SET @txOutput = @@TRANCOUNT


COMMIT TRAN

GO 

Entity Framework에서 해당 SP Function으로 Import 있습니다. System.Data.SqlClient 사용하셔도 됩니다. 여기서 @@TRANCOUNT로 발생하는 트랜잭션의 개수를 SP의 output 파라미터에 대입해주는 것을 확인하실 있습니다. ADO.NET 트랜잭션 메소드는 SQL Profiler SQLTransaction으로 표현이 되는데 SQLTransaction 해당 테스트와 상관없이 너무나 자주 발생하여 트랜잭션이 발생하는 개수를 파악하기 위한 방법으로 사용되었습니다.

그러면 이제 단순히 SP 실행해보도록 하겠습니다.


=> 위에 말했던 대로 SP내부에서 사용한 ADO.NET 트랜잭션 메소드는 SQLTransaction으로 표시가 되며 EventSubClass 열로 트랜잭션이 어느 부분부터 시작되며 어느 부분에서 종료가 되는지 아실 있습니다. ObjectName 통해서는 사용자가 발생한 트랜잭션인지 SQL Server내부에서 발생하는 트랜잭션인지를 구분할 있습니다.

 

그러면 이제 TransactionScope 클래스와 함께 SP 실행해보도록 하겠습니다.


=> TransactionScope 클래스를 사용하였기 때문에 TM(LTM) 관여하는 것을 보실 있으며 DBM_INIT이라는 내부 Object 의해서 무엇인지는 정확하게 없지만 SQLTransaction 발생한 것도 있습니다. 여기서 중요한 것은 바로 sp_reset_connection 함수입니다. 함수는 커넥션 풀링이 되었다는 의미입니다. 이상합니다. 하나의 SP만을 실행준 것인데 커넥션 풀링이 필요한 것일까요?

 

TransactionScope 클래스를 사용한 구문을 보도록 하겠습니다.

using (TransactionScope scope = new TransactionScope())

{

    // 하나의 SP실행

 

    scope.Complete();

} 

=> 앞에서 말했듯이 System.Transaction 네임스페이스의 트랜잭션은 TM 관리하였습니다. 단순히 SP만을 실행해줄 경우에는 SQLTransaction에서 commit 이후에 커넥션이 닫혔습니다. 그러나 TransactionScope안에서 SP 실행할 경우에 SQLTransaction commit되기 전에 커넥션이 먼저 닫히게 됩니다. 이것으로 보아 TransactionScope 사용하게 되면서 ADO.NET 트랜잭션 메소드가 TM 관리하는 트랜잭션으로 관리된다는 것을 있습니다. TM Commit 되기 전에 커넥션이 닫혔기 때문에 TM에서는 커넥션 풀링을 통해서 다시 커넥션을 맺고 내부의 SQLTransaction 발생한 이후에 SQLTransaction commit해주며 이후에 TM Commit 되고 마지막으로 커넥션을 닫아주는 것입니다.

가지 테스트를 통해서 TransactonScope 옵션(TransactionScopeOption) 따라서 ADO.NET 트랜잭션 메소드가 참여하게 된다는 사실을 확인할 있었습니다.

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

Posted by resisa
,
솔루션 파일의 스키마에 대해서 알아보도록 하겠습니다.


위와 같이 솔루션을 만들고 Project Dependencies에서 ClassLibrary1 프로젝트에서 ClassLibrary2을 참조해줍니다. 그리고 솔루션 파일을 열어보도록 하겠습니다.

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary1", "ClassLibrary1\ClassLibrary1.csproj", "{F7C941E2-B6DA-4652-957A-923EB41C155F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary2", "ClassLibrary2\ClassLibrary2.csproj", "{3FA19692-3C13-427F-9662-57BBC612915C}"
ProjectSection(ProjectDependencies) = postProject
{F7C941E2-B6DA-4652-957A-923EB41C155F} = {F7C941E2-B6DA-4652-957A-923EB41C155F}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{7FB05BE4-9794-4E4C-92A8-1E4FFC56A7A2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F7C941E2-B6DA-4652-957A-923EB41C155F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F7C941E2-B6DA-4652-957A-923EB41C155F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7C941E2-B6DA-4652-957A-923EB41C155F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7C941E2-B6DA-4652-957A-923EB41C155F}.Release|Any CPU.Build.0 = Release|Any CPU
{3FA19692-3C13-427F-9662-57BBC612915C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3FA19692-3C13-427F-9662-57BBC612915C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FA19692-3C13-427F-9662-57BBC612915C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FA19692-3C13-427F-9662-57BBC612915C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F7C941E2-B6DA-4652-957A-923EB41C155F} = {7FB05BE4-9794-4E4C-92A8-1E4FFC56A7A2}
EndGlobalSection
EndGlobal
노란색에 해당하는 항목에 대해서 정리하면 아래와 같습니다.

1. Project ~ EndProject
  • 솔루션 파일에 추가할 수 있는 프로젝트를 의미합니다.
  • 형식은 '추가하는 항목의 Guid = 프로젝트명, 프로젝트 파일 위치, 프로젝트 Guid' 입니다.
  • 여기서 추가하는 항목의 Guid는 아래와 같습니다.
    • 솔루션 폴더 : "{2150E333-8FDC-42A3-9474-1A3956D46DE8}"
    • 프로젝트 :  "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"
  • 솔루션 파일에 추가할 수 있는 항목은 솔루션 폴더와 프로젝트로 구분 할 수 있습니다.
  • 여기서 프로젝트란 모든 형식(Console, Winform, WPF, WebForm)의 프로젝트를 말합니다.

2. ProjectSection(ProjectDependencies) = postProject ~ EndProjectSection
  • 솔루션 파일이나 프로젝트 파일에서 오른쪽 버튼을 누른 후에 'Project Dependencies'속성에서 수동으로 프로젝트에 대한 의존관계에 대한 내용입니다.
  • 형식은 '참조되는 프로젝트의 Guid = 참조되는 프로젝트의 Guid' 입니다.
  • 프로젝트 참조에 대한 정보는 솔루션 파일에는 존재하지 않습니다.

3. GlobalSection(SolutionConfigurationPlatforms, ProjectConfigurationPlatforms) = preSolution ~ EndGlobalSection
이 부분은 그림으로 설명해 보도록 하겠습니다.
  • SolutionConfigurationPlatforms에 해당하는 내용은 파란색 박스입니다.
  • ProjectConfigurationPlatforms에 해당하는 내용은 빨간색 박스입니다.
  • 솔루션의 Configuration를 변경하면 해당 솔루션에 포함된 모든 프로젝트의 Configuration이 변경되며 특정 프로젝트만 Configuration를 변경하고 싶을 경우에 빨간색 박스의 내용을 변경하면 됩니다.
  • Build의 체크 박스 표시 여부에 따라서 Debug|Any CPU.Build.0 가 포함된 행이 솔루션 파일에 포함되는지가 결정되며 당연히 체크 표시가 없다면 해당 프로젝트의 빌드는 Skip 됩니다.

4. GlobalSection(NestedProjects) = preSolution ~ EnbGlobalSection
  • 이 구역이 바로 솔루션 폴더와 해당 솔루션 폴더에 포함되는 프로젝트에 대한 내용입니다.
  • 형식은 '프로젝트 Guid = 솔루션 폴더 Guid' 입니다.
다음에는(완전 미정 ㅋ) 프로젝트 파일에 대한 스키마에 대해서 알아보도록 하겠습니다. ^^;
Posted by resisa
,

프로젝트 개수가 많아지다 보면 솔루션 파일을 열어 Visual Studio상에서 빌드를 하는 것보다는 MSBuild를 사용하여 빌드를 하는 것이 현명한 선택일 것입니다. MSBuild를 사용할 경우에 단순히 솔루션 파일을 지정해주면 빌드가 정상적으로 완료되지 않습니다. 바로 프로젝트 간에 참조 문제 때문입니다. MSBuild에서는 파라미터로 솔루션 파일을 지정해주면 해당 솔루션 파일에 정의된 프로젝트 순서대로 빌드 순서를 정해버립니다.
아래의 사이트는 이러한 문제점을 해결해줍니다.
http://serialize.wordpress.com/2009/12/16/msbuild-task/

처음 MSBuild를 접하시는 분은 위의 사이트를 눈을 크게 뜨고 보아도 실행이 잘되지 않을 것입니다.(제가 그랬습니다. ㅋ)
<Project DefaultTargets="BuildProjects" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

 

  <Target Name="BuildProjects">

 

    <GetProjecsInOrder Solution="$(MSBuildProjectDirectory)\MySolution.sln">

      <Output ItemName="ProjectFiles" TaskParameter="Output"/>

    </GetProjecsInOrder>

   

    <Message Text="%(ProjectFiles.FullPath)"/>

   

    <MSBuild Projects="%(ProjectFiles.FullPath)"

      Targets="Build"

      Properties="Configuration=Release"/>

  </Target>

 

  <UsingTask AssemblyFile="MSBuildTasks.dll" TaskName="GetProjecsInOrder" />

 

</Project>

=> MSBuildTasks.dll이라는 사용하는 구문을 몰라서였습니다. -_- MSBuildTasks.dll소스를 열어보면 Task라는 클래스(빌드가 되는 단위 ITaskItem이 프로젝트와 매칭되는 빌드의 단위)를 상속받아서 구현하고 있는 것을 볼 수 있습니다. 솔루션 파일를 정규식으로 필터링을 해서 프로젝트의 정보를 얻습니다. 이 프로젝트 정보로 해당 프로젝트 파일를 열어 프로젝트 파일에 있는 프로젝트 참조에 대한 정보를 정규식을 통해서 얻습니다. 이렇게 모든 정보를 얻고 나서 Recusive한 알고리즘(?!)을 통해서 빌드되는 프로젝트의 순서를 정해주게 됩니다. 위의 Message 앨리먼트를 사용하게 되면 이러한 빌드 순서에 대해서 화면에 찍어주게 됩니다.
한 가지 아쉬운 점이라면 바로 프로젝트 참조에 대해서만 빌드 순서를 정해준다는 것입니다. 물론 일반적인 솔루션이라면 당연히 프로젝트 참조 대해서만 빌드 순서를 정해주면 되지만 모든지 예외는 존재하는 것 같습니다. 그닥 어렵지는 않습니다. 이전 포스트에서 소개해드린 솔루션 파일에 어셈블리 참조에 대한 Project Dependencies 정보가 사전에 필요합니다. 그리고 이 정보를 사용하여 어셈블리 참조에 대한 정보를 얻어 위에서 사용한 Recursive한 루틴을 태우면 됩니다. 프로젝트 참조와 어셈블리 참조 모두를 적용하면 어떻게 될지도 궁금하네요 ^^

<2010-05-12>
MSBuild에서 솔루션 파일안에 프로젝트 파일의 정보 순서대로 빌드를 해준다고 생각했었는데 다시 해보니 Project Dependency에 의해서 빌드 순서를 정해서 빌드를 합니다. 위의 사이트의 해결방법이 그렇다면 쓸모없는 방법이라는 결론입니다. 하지만 700정도 되는 프로젝트를 단순히 솔루션 파일에 의해서 MSBuild를 하면 제대로 빌드가 되지 않습니다. 무엇인가 정확하게 알지 못하는 부분이 있는 것 같아 정확하게 원인이 파악되면 다시 글을 수정하도록 하겠습니다.

<2010-05-23>
Project Dependency를 나타내는 방법은 아래의 두 가지입니다.
1. 프로젝트 참조를 한다.
2. Project Dependencies 메뉴에서 수동으로 Dependency를 정의한다.
2번 째 방법은 솔루션 파일에 ProjectSection(ProjectDependencies)에서 찾아볼 수 있지만 1번 째 방법은 그렇지 않습니다.

MSBuild시에 솔루션 파일을 지정하고 빌드할 경우 테스트해 본 결과 1번째 2번째 방법 모두 빌드 오더를 MSBuild 자체적으로 계산하여 정확하게 빌드를 해주는 것을 확인하였습니다. 위의 사이트의 해결방법이 쓸모없는 방법인 것으로 판단됩니다. --;
Posted by resisa
,

현재 프로젝트에서 참조를 어떻게 하고 계십니까? 아마도 대부분 이렇게 구성하고 계실 것입니다.
1. 프로젝트 사이에 참조가 필요한 경우에는 프로젝트 참조를 한다.
2. 3Party DLL 등등은 어셈블리 참조를 한다.
2번에서 3Party DLL의 소스가 있을 경우에 소스의 수정 여부에 따라서 프로젝트 참조를 할 것입니다.
이렇게만 구성을 하여도 충분히 훌륭한 솔루션(.sln)을 구성한 것입니다.

그렇다면 혹시 프로젝트 참조와 어셈블리 참조의 차이점은 알고 계십니까?
참조의 관점에서 본다면 차이점이 없습니다. 어셈블리 참조는 해당 위치(Path)에 있는 어셈블리를 참조하는 것이고 프로젝트 참조는 프로젝트의 출력 경로의 어셈블리를 참조하게 해주는 것 뿐입니다.
빌드의 관점에서 본다면 차이점이 존재합니다. 먼저 어셈블리 참조는 단순히 참조만 합니다. 하지만 프로젝트 참조를 하면 참조된 프로젝트의 출력 경로로 빌드된 어셈블리가 생성되고 참조됩니다.
이 차이점을 가지고 참조된 어셈블리의 Properties에 대해서도 살펴보겠습니다. 차이점이 보이실 겁니다. 바로 Specific Version 프로퍼티입니다. 이 프로퍼티는 어셈블리 참조에는 존재하고 프로젝트 참조에는 존재하지 않습니다. 그렇다면 빌드의 관점 뿐만이 아니라 프로퍼티에 대해서도 뭔가 차이점이 존재하는 것 아니냐고 생각하실 수 있습니다. 하지만 명백하게(?!) 빌드의 관점을 제외하고는 차이점이 없습니다.
Specific Version 프로퍼티는 그 이름대로 GAC(Global Assembly Cache)에 등록되어 있는 어셈블리 중에 특정 버전를 참조할 것인지의 여부를 설정하는 것입니다. 프로젝트 참조에서는 위에서 말했듯이 빌드가 된 어셈블리를 참조하기 때문에 특정 버전의 여부가 의미가 없어지는 것입니다. 참고적으로 어셈블리 참조시에 Local Copy는 GAC에 등록여부에 따라서 해당 값이 결정되기 때문에 닷넷 프레임워크에서 제공해주는 System 등등은 false가 됩니다. 프로젝트 참조에서는 Local Copy는 Default로 True 값을 가집니다.
지금까지 차이점에 대해서 이야기를 해보았습니다. 어셈블리 참조를 프로젝트 참조와 동일하게 만들어보도록 하겠습니다.
참조를 하는 프로젝트에서 오른쪽 버튼을 눌러 아래와 같이 Project Dependencies를 선택합니다.
=> Depends on 메뉴에서 어셈블리 참조를 한 ClassLibrary1에 체크 표시를 하고 'OK'버튼을 클릭합니다. Build Order탭을 살펴보면 해당 빌드 순서도 확인하실 수 있습니다. 이렇게 설정을 하고 참조를 하고 있는 프로젝트에서 '빌드'를 실행하면 어셈블리 참조를 한 어셈블리도 빌드가 되는 것을 확인하실 수 있습니다.(물론 어셈블리 참조를 하였더라도 해당 어셈블리로 출력되는 프로젝트는 솔루션에 로드되어 있어야 합니다.) 이러한 정보를 어디서 가지고 있을까요? 바로 솔루션 파일(.sln)에 있습니다.
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsFormsApplication5", "WindowsFormsApplication5\WindowsFormsApplication5.csproj", "{DA5C04D8-F654-49FF-BBEB-CD6BE17AD952}"

           ProjectSection(ProjectDependencies) = postProject

                     {9526A004-DF09-4C1E-9BB2-A1B31B37A716} = {9526A004-DF09-4C1E-9BB2-A1B31B37A716}

           EndProjectSection

EndProject

=> 노란색으로 표시한 부분 아래에 나와있는 Guid가 바로 ClassLibrary1의 Guid입니다. 프로젝트 참조시에는 솔루션 파일에 이러한 정보는 보이지 않습니다. 하지만 Project Dependencies 속성을 확인해보면 자동으로 체크표시가 되어 있는 것을 확인할 수 있는데 해당 정보는 프로젝트 파일(.csproj)에서 얻을 수 있습니다. 어쨌든 Visual Stuido에서는 솔루션 파일과 프로젝트 파일에 있는 정보를 사용하여 빌드 순서를 정해주게 됩니다.
마지막으로 빌드시에 참조되는 어셈블리를 어떠한 경로를 통해서 찾는지 알아보겠습니다.
(여기서는 VS2010기준입니다.)
단순히 나열을 해보도록 하겠습니다.
  {HintPathFromItem}
  {TargetFrameworkDirectory}
  {Registry:Software\Microsoft\.NETFramework,v4.0,AssemblyFoldersEx}
  {AssemblyFolders}
  {GAC}
  {RawFileName}
  bin\Debug\
위의 항목 중에는 단순히 하나의 경로도 있지만 컴퓨터 환경에 따라서 수십개의 경로를 탐색하기도 합니다. 또한 .exe .dll를 모두 찾습니다. 만약에 위의 항목이 실질적으로 경로를 탐색하는 순서라면 bin\Debug\가 제일 마지막에 있는 것이 의외(?)네요. 이러한 정보는 모두 MsBuild를 통해서 얻을 수 있는데 VS에서도 MsBuild에서와 마찬가지로 위의 경로를 탐색합니다.

<2010-06-23>
결정적인 차이점이 존재하네요. 바로 Go To Defenition(정의로 이동?)입니다. 어셈블리 참조를 하면 해당 코드로 이동할 수가 없지만 프로젝트 참조를 하면 해당 코드로 이동이 가능합니다. +_+;;
Posted by resisa
,
이전 포스트(닷넷 트랜잭션 정리)에서 닷넷 프레임워크 2.0부터 제공해주는 TransactionScope 클래스를 사용하여 Oracle DB에서 트랜잭션을 처리하기 위해서는 Ora MTS가 필요한 것을 알았다. Windows Vista에서 Ora MTS를 사용하여 트랜잭션을 처리하기 위해서는 패치가 필요하다. 다음 표는 운영체제별로 Oracle 10g로 트랜잭션을 처리하기 위해서 필요한 프로그램을 정리해보았다.
                                                                                                                                       (32bit 기준)
 Windows Xp(Server 2000)  Windows Vista(Server 2008)
 Oracle 10g  10.1.0.2  10.2.0.3
 ODAC(Ora MTS)  10.1.0.4.0  10.2.0.2.21
 Patch Set  X  10.2.0.4

Oracle 10g 다운로드 : http://www.oracle.com/technology/software/products/database/index.html
ODAC 다운로드 : http://www.oracle.com/technology/software/tech/windows/odpnet/index.html
Patch Set 다운로드 : https://metalink.oracle.com/CSP/ui/index.html

xp의 경우에는 설치할 때의 그닥 문제가 없다. ODAC만 설치해줘도 TransactionScope를 사용해서 트랜잭션 처리가 가능하다. 하지만 Vista의 경우에는 ODAC 다운로드 링크에서 설치 문서대로 설치를 해줘야 한다.(설치 순서도 지켜 설치하고 ODAC와 Patch Set은 Database가 설치된 Oracle Home디렉토리로 경로를 맞춰서 설치해준다.)

1. Install Oracle Database Client (10.2.0.3.0) for Microsoft Windows Vista and Windows Server 2008.
2. Download ODAC1020221.exe or ODTwithODAC1020221.exe and unzip its contents by running the executable.
3. Run the Oracle Universal Installer (OUI) by launching setup.exe from the "install" directory that was unzipped in Step 2.
4. On the OUI screen named "Specify Home Details" choose the same Oracle Home as in the Step 1 and proceed.
5. Install Oracle Database 10g Release 2 (10.2.0.4.0) Patch Set. This patch can be downloaded from Oracle Metalink.

여기서 ODAC1020221를 설치하려고 하면 javaw.exe 에러가 발생하여 설치 프로그램이 자동으로 종료가 되어버린다. 여러곳에 검색을 해서 해결방법을 찾으려 했지만 모두 실패했다. 네이버에서 MS Office 2007때문에 설치안된다는 것을 처음에 보았는데 말이 안된다는 생각에 해보지 않았다. 다른 Vista가 설치된 PC에 설치해보려고 해도 역시나 같은 에러가 발생하였다. 마지막으로 Office를 지우고 설치를 해보았다. 설치가 되었다 -_-;;
그래서 이제는 되는구나 싶어서 프로그램을 실행시켰더니 Branch 길이가 다르다는 메세지가 떳다. 다른 사람의 말에 귀도 잘기울려야 하고 설치 가이드도 무시하지 말고 꼼꼼하게 잘봐야한다는 생각을 했다 ㅡ.ㅡ;;;
Oracle Metalink는 오라클 온라인 고객지원 사이트라고 생각하면 된다. 이것은 Oracle과 유지보수 계약을 맺으면 CSI라는 넘버를 부여하고 그 아이디를 이용해서 사용자 등록을 한 이후에 관리자가 승인(회사라면 회사내의 특정한 분이 관리자로 임명되어있다.)처리를 해주면 등록한 이메일 주소로 패스워드를 받아서 로그인을 할 수 있다. 여기서 Patch Set를 받아 드디어 설치를 했고 Vista에서도 TransactionScope를 이용해서 트랜잭션을 처리할 수 있었다. 하지만 이 설치과정이 배포할 경우 모든 클라이언트에 해야한다는 면에서 너무나 부담스럽게 느껴졌다.(패치의 용량이 거의 1G다. ㅡ.ㅡ;) 특히나 원래 DB를 MSSQL를 사용하고 있다가 Oracle에 연결하여 어떤 정보를 가져와야 한다거나 하면 모든 PC에 ODAC와 패치를 설치해줘야 한다니 누가 한단 말인가? +_+;
닷넷을 사용하면 웬만하면 MSSQL를 사용하는 것이 좋다는 생각이 들고 당연히 오라클과 같은 다른 DB에 접속을 하여 처리해야 하는 경우에는 웹서비스를 이용해서 서버에만 ODAC와 패치를 설치해주는 것이 어떨까 하는 생각을 해봤다.
Posted by resisa
,