영국에서 엔터프라이즈 마켓은 대부분 은행권 백오피스 시스템이 주를 이룬다. 너무나 복잡한 도메인 지식들 때문에 보다 효율적 유지보수를 위해서 DDD 기반의 아키텍처를 선호해왔다. 도메인 지식과 코드를 최대한 분리시키지 않고자 하는 것이 그 이유이다. 하지만 DDD의 단점은 DAL위에 Repository가 있고 그 위에 다시금 DDD가 있고 심지어 레거시 코드와 많이 융합된 오래된 시스템은 그 DDD위에 다시 BSL이 있는 것을 많이 보게 된다. BSL이 아니더라도 DDD를 ViewModel Factory같은 녀석으로 감싸야 하는 것이 일반적이다. 


이렇게 다중 레이어들로 계층을 이루게 되다보니 비지니스 로직이 하나 변경되었을때 모든 레이어들이 수정되어야 한다는 무시무시한 단점이 있다. 심지어 여러 코드들이 레퍼런스를 이루고 있다면 정말 그 수정이 헬이 된다. 그래서 순수 DDD에서 무시무시한 변종으로 진화하는 것을 종종 볼 수 있다. 간단한 필드가 추가되어도 마찬가지이다. 이런 비효율을 때문에 마틴 파울러가 소개한 CQRS가 환영받을 수 있을지 모른다. 그래서 영국의 뱅킹뿐만 아니라 많은 회사들의 백엔드 아키텍트들이 다시금 DDD에서 CQRS라는 새로운 아키텍트 모델로 전환하는 파도가 일고 있다.


가장 큰 장점은 복잡한 업무로직에 대한 유지보수가 가장 크며 두번째는 어쨌든 커맨드와 조회를 분리하면서 메모리 캐시나 NoSQL과 같은 데이터베이스를 관리하는데 더 큰 장점이 있다. 아직 제대로 적용해본 적이 없기 때문에 지금상황에서는 뚜렷한 장단점을 설명할 수 없다. 하지만 조만간 샘플 프로젝트를 진행하고 DDD와 비교해서 다시금 조금더 경험적인 포스팅을 이어보도록 하겠다.



닷넷에서 CQRS에 적용에 필요한 친절한 가이드들을 모아봤다.



저작자 표시
신고
Posted by 박경훈

2009년 2월호로 월간 마이크로소프트웨어에 기고한 내용입니다. 
지면상 내용을 간추리려고 노력했습니다. 


리버스 엔지니어링은 소프트웨어 공학의 한 분야로 이미 만들어진 시스템을 역으로 추적하여 처음의 문서나 설계기법 등의 자료를 얻어 내는 일을 말한다. 역 어셈블리 공학이라고도 부르는 이 리버스 엔지니어링 기술을 보는 시각이 곱지 못하다. 왜냐하면 대부분 다른 이의 소프트웨어를 저작자의 동의 없이 제3의 프로그램을 만들어 내는 것이 가능하기 때문이다. 대표적인 예로 크랙과 같은 프로그램을 만드는 것이 여기에 해당된다. 하지만 리버스 엔지니어링을 좋은 방향으로 잘 이용한다면 여러 방면으로 유용할 수 있다. 먼저 닷넷의 경우 소스코드가 없는 어셈블리를 수정할 수 있을 뿐만 아니라 수정하고 싶지만 미숙한 닷넷 언어를 이용해서 컴파일 되었을 경우 CIL을 이용해서 코드를 수정할 수 있다. 그럼 닷넷의 리버스 엔지니어링에 대해서 자세히 살펴보도록 하자.


닷넷의 리버스 엔지니어링

닷넷은 CIL(Common Intermediate Language)로 중간 컴파일을 거치기 때문에 닷넷의 어셈블리어를 이해하는 것은 비교적 쉽다. 먼저 닷넷의 실행 과정을 살펴보자. 


[그림1]닷넷의 컴파일 과정


비주얼 스튜디오나 명령 프롬포트에서 컴파일을 하게 되면 그 파일은 MSIL형태로 컴파일 된다. (MSIL과 IL 그리고 CIL은 모두 같은 것을 지칭한다는 것을 알아두자.) 우리는 중간 MSIL형태로 컴파일 되는 코드를 IL 디스어셈블리를 이용해서 확인할 수 있다. 다음 [화면1]은 IL 디스어셈블리를 이용해서 코드를 열어봤을 때의 화면이다.
 


[화면1] IL 디스어셈블리


[화면1]을 보면 굉장히 낯선 코드들이 적혀있는 것을 볼 수 있을 것이다. 이것을 우리는 CIL이라고 부르고 닷넷의 어셈블리어라고 지칭하기도 한다. 이 CIL 언어를 이용해서 우리는 C#인 VB.NET과 같은 코드없이 재 컴파일 하는 것이 가능하다. 즉, 소스를 잃어 버렸다거나 바이너리 파일에서 원하는 기능을 제거하고 싶다거나 할 경우에 이 CIL을 편집하여 다시 제2의 프로그램을 만들어 낼 수 있다는 것이다. 

 

[그림2] 양방향 엔지니어링


물론 CIL을 이해하는 것은 상당히 어렵다. 하지만 CIL을 볼 수 있다면 닷넷이 내부적으로 어떻게 동작되는지 쉽게 이해할 수 있다. 예를 들어 foreach와 같은 구문이 내부적으로 어떻게 생성되고 어떻게 동작되는지 볼 수 있다는 것이다. 그리고 다른 언어의 어셈블리어보다는 CIL이 비교적 쉽다. 여기서는 CIL의 간단한 개념들에 대해서 살펴볼 것이고 또 간단한 프로그램을 만들어 그 프로그램을 수정해 보도록 할 것이다. 


CIL의 기본개념 이해하기

원래 어셈블리어는 0X58, 0X73와 같은 언어를 이용하기 때문에 사람이 알아보기 굉장히 힘들다. 하지만 CIL의 경우 이러한 어려운 문자들을 쉽게 읽을 수 있게 문자화 시켰다. 예를 들어 x+y 라는 문장이 있다면 여기서 +는 기계어로 0X58로 표현된다. 하지만 CIL에서는 이것을 읽기 쉽게 add로 지칭하여 사용한다는 것이다. 그럼 아래와 같은 아주 간단한 C#코드가 있다고 가정해보자.

static int Add(int x, int y)
{
  return x + y;
}


[코드1] 간단한 C#코드

이 코드를 ildasm.exe를 이용해서 살펴보면 다음과 같은 코드가 생성되는 것을 볼 수 있을 것이다.

  

.method private hidebysig static int32 Add(int32 x,
int32 y) cil managed
{
// 코드 크기 9 (0x9)
.maxstack 2
.locals init ([0] int32 CS$1$0000)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: add
IL_0004: stloc.0
IL_0005: br.s IL_0007
IL_0007: ldloc.0
IL_0008: ret
}


[코드2] CIL 코드

코드를 차례대로 살펴보도록 하겠다. .maxstack 2의 경우 스택의 공간을 할당한 것이다. CIL은 모두 스택 기반으로 동작하게 되는데 이 부분은 지면상 생략하도록 하겠다. 그 다음 IL_0000과 같은 접두사가 코드 앞에 붙어있는 것을 볼 수 있을 것이다. 이것은 for나 foreach와 같은 반복문을 쓸 때 혹은 분기문장을 쓰게 될 경우에 어디로 갈지 지정해주기 위한 번호라고 보면 된다. ldarg의 경우 스택에 파라메터 값을 저장하기 위한 것이다. stloc의 경우 메모리에 그 값을 저장하기 위해서 사용하는 명령어이고 바로 호출되는 ldloc는 저장된 값을 다시 로드하여 반환하게 된다. 

두서없이 코드를 설명해서 머리속이 굉장히 복잡해졌을 것이다. CIL 코드는 CIL 지시자, CIL 애트리뷰트, CIL 명령어(동작코드)이렇게 크게 3가지로 구분된다. 먼저 닷넷 어셈블리 전체 구조를 설명하는데 사용되는 CIL 문법이 있는데 이러한 문법들은 지시자라고 부른다. CIL 지시자는 CIL 컴파일러가 어셈블리 안에서 사용되는 네임스페이스, 클래스, 멤버들을 어떻게 정의되었는지 살펴보기 위해서 사용된다. 그리고 대부분의 경우 CIL 지시자 자체로는 닷넷에 정의된 타입이나 멤버들을 자세하게 표현하기가 힘들다. 때문에 많은 CIL 지시자 들은 다양한 CIL 애트리뷰트들을 선언하여 실제로 어떻게 처리가 되는지 지정하게 된다. public, private와 같은 속성을 지정하기 위한 코드라고 보면 된다. 그리고 마지막으로 메서드 안에서 동작되는 명령어가 바로 CIL 명령어에 해당된다. 물론 CIL 명령어들을 이해하는 것이 가장 중요하다. CIL 명령어들의 종류와 하는 일들에 대해서 알고 싶다면 http://en.csharp-online.net/CIL_Instruction_Set 블로그를 방문해 보도록 하자. 여기에서는 다양한 CIL 명령어들을 정의해놓고 설명해주고 있다.


크래킹의 기초

그럼 간단한 프로그램을 작성하여 크래킹을 시도해보도록 하겠다. 먼저, 간단한 콘솔프로젝트를 생성해보자. 그리고 다음과 같은 간단한 코드를 작성하여 컴파일 해보도록 하자.

Console.WriteLine("Input CD Key Please");
string CdKey = Console.ReadLine();
if (CdKey == "HOONS")
{
    Console.WriteLine("Success");
}
else
{
   Console.WriteLine("Fail");
}



컴파일 된 exe 파일을 IL 디스어셈블리로 열어보자. 그리고 메뉴의 덤프를 이용하면 모든 CIL를 생성하여 il파일로 저장할 수 있다. 
 

[화면2] CIL 코드 내보내기

그럼 다음과 같은 코드들이 출력되는 것을 볼 수 있을 것이다.
 


[화면3] 생성된 CIL 코드

여기서 특정 부분을 수정해서 ilasm.exe을 이용해서 컴파일 해보도록 하겠다. 필자는 모두 Success가 나오게 간단히 문자열만 수정한 후에 저장하였다. 그리고 비주얼 스튜디오 명령 프롬포트를 이용해서 이것을 다시 컴파일 해보도록 하겠다. 

ilasm /exe hoons.il /output=HOONS.exe



이렇게 실행하면 hoons.exe파일이 생성되고 HOONS가 아니어도 무조건 success가 출력되는 것을 볼 수 있을 것이다.

 


[화면5]


저작자 표시
신고
Posted by 박경훈

일반적으로 컴퓨터의 언어에서는 데이터의 부재를 “null”이라는 예약어를 이용해서 표현해 왔다. C#에서 또한 null 타입이 존재하지만 닷넷의 데이터 구조상 null을 표현하기에는 다소 모호한 부분이 있다. 그렇기 때문에 이 null 타입에 대한 이슈는 닷넷 1.0 시절부터 상당히 많이 논의되어왔던 내용이다. 그럼 C#에서 null 타입의 모호한 부분이 무엇이며 null타입의 올바른 사용 방법들에 대해서 살펴보도록 하겠다. 

null의 진정한 의미

null에 대한 정의를 내리기 전에 먼저 닷넷의 데이터 타입에 대해서 살펴보도록 하겠다. 닷넷의 CTS(Common Type System)에서는 모든 것이 객체다. 여기에는 우리에게 익숙한 int, char, double 등의 간단한 타입부터 string과 같은 확장된 타입들이 있고 이들은 모두 System.Object라는 클래스에서 파생되었다. 이 타입들은 값 타입(int, float, bool, long, byte 등)과 참조 타입(string, object)으로 나누어 지게 된다. 

 

(Value) 타입

 참조(Reference) 타입

저장되는것

데이터

 데이터가 위치하는 주소

메모리 영역

일반적으로 스택(Stack)

일반적으로 힙(Heap)

대표데이터 형

기본데이터 타입구조체열거형(Enumerator)

클래스배열델리게이트인터페이스

[표] 값 타입과 참조타입의 비교


하지만 여기서 문제가 되는 것은 Value 타입에 null을 대입할 수 없다는 것이다. 그렇다면 왜 값 타입에는 null을 대입할 수 없는 것일까? 이 문제를 쉽게 이해하기 위해서는 먼저 닷넷에서 사용하는 null의 정의를 확실하게 내려야만 한다. 닷넷에서의 null은 “어떤 객체도 참조하지 않음” 이란 의미를 지닌 특별한 값으로 정의를 내릴 수 있다. 그럼 왜 값 타입에는 null을 대입할 수 없는 것일지 이해할 수 있을 것이다. 즉, 참조 타입변수의 값은 참조이고, 값 타입 변수의 값은 실제로 존재하는 어떤 값이기 때문이다.


C#1.0 에서 null을 표현하는 방법

값 타입에 null을 표현한다는 것 자체가 모순이 되는 내용이기도 하지만 값 타입에 null을 이용해야 되는 예외 상황은 존재한다. DB와 데이터 연동을 하다 보면 null을 표현해야만 하는 문제를 만나게 된다. 때문에 C#1.0 에서는 다음과 같은 편법을 이용해서 값 타입에 null을 표현해 왔다. 



먼저 별도의 bool 변수의 선언하는 방법이 있다. 용감 무식한 방법이긴 하지만 모든 값 타입의 변수마다 bool 변수를 만드는 것이다. 이 bool 타입을 만들어 관리하는 방법은 2가지가 있다. 별도의 bool 타입변수를 선언하여 관리하는 간단한 방법과 해당 값 타입과 bool 변수를 클래스로 래핑하여 사용하는 방법이 있을 수 있다.

두 번째 방법은 별도의 참조 타입을 생성해서 필요할 때마다 박싱 언박싱을 해주는 것이다. 즉, 각각의 값 타입은 하나의 인스턴스 변수와 타입 변환 연산자를 가지게 될 것이다. 하지만 여기서 문제는 이 타입은 결국 힙 영역에 생성될 것이고 가비지 컬렉션의 압박을 가하면서 메모리 사용을 늘리는 현상을 초래하게 된다. 

마지막 방법은 임의의 값을 null이라고 지정하는 것이다. 우리는 이 값을 매직값이라고 부른다. 즉, 값의 범위에서 하나의 값을 희생하여 null로 간주하는 방법이다. 예를 들어 DB에 날짜를 넣어야 한다면 AD 1년(1년 1월 1일)의 날짜가 들어있을 경우가 거의 없을 것이다. 그렇기 때문에 우리는 AD 1년의 값을 null로 이용할 수 있다는 것이다. 이렇게 임의의 값을 사용할 경우 메모리의 낭비도 없지만 값을 잘못 선택할 경우 프로그램에서 알 수 없는 버그가 될 수 있는 가능성이 있다. 때문에 최소한 null로 지정한 값을 표현할 상수 정도는 추가로 지정을 해주어야 한다. 


C#2.0 에서 지원하는 Nullable<T>

C#2.0에서는 null을 표현할 수 있는 Nullable<T>라는 지네릭 타입이 추가 되었다. Nullable<T>의 핵심 속성은 HasValue와 Value이다. 먼저 Value는 값이 있다면 실제 값을 반환하고 값이 없다면 InvalidOperation-Exception을 발생시킨다. HasValue는 단순한 bool 값으로 실제 값이 존재하는지의 여부를 반환해준다. 

C#2.0에서는 Nullable<T>구조체를 보다 간결히 사용하기 위해서 “?” 라는 식별자를 지원하고 있다. 즉, 다음 두 선언은 같다고 할 수 있는 것이다.

●    Nullable<int> nullable=5;
●    Int? nullable=5;

 

Nullable 
타입의 박싱/언박싱

박싱과 언박싱에 대한 이야기를 나누어 보기 전에 null 값의 비교에 대해서 살펴보도록 하겠다. Nullable 타입의 비교는 HasValue 속성을 직접 이용할 수도 있고 == 연산자를 이용할 수도 있다. 
 

DateTime? birthday;
If(birthday==null)
If(birthday.HasValue)

 
그리고 Nullable<T> 타입은 다음과 같이 값뿐만 아니라 null을 대입할 수 있다는 것을 볼 수 있다. 
 

DateTime? birthday;
birthday = null;
birthday = new DateTime(1938,03,21);

 
하지만 이 코드를 개념상으로 접근한다면 상당히 혼란스러운 부분일 수 있다. 그래서 내부적인 동작을 IL로 확인해 본 결과 birthday = null 코드는 DateTime? 생성하여 대입하는 것을 볼 수 있었고, birthday = new DateTime(1938,03,21); 코드는 DateTime의 일반 생성자를 호출하고 그 결과를 바로 Nullable<DateTime>으로 다시 감싼 후에 전달하는 것을 볼 수 있었다. 
 
C#에서는 박싱/언박싱 작업이 빈번하게 일어나게 된다. 박싱이란 값 타입을 참조 타입에 대입하는 것을 의미한다. 하지만 값 타입을 어떻게 참조 타입에 넣을 수 있을까? 논리적으로 볼 때 불가능한 부분인 것이다. 하지만 C#의 값 타입은 System.Object 객체로 변환할 수 있게 설계되었기 때문에 자신을 참조타입으로 변환하는 것이 가능하다. 참조타입에 값 타입을 대입하면 스택의 값을 객체에 복사한 후 그 객체는 힙에 저장되게 된다. 언박싱은 그 반대의 작업이 일어난다고 볼 수 있다. 


[그림]


그렇다면 nullable 타입은 값 타입일까? 참조 타입일까? C#에서는 nullable 타입을 값 타입으로 구분하고 있다. 때문에 참조 타입에 대입한다면 박싱, 언박싱이 일어난다는 사실을 알아두어야 한다.
다음은 nullable 타입의 박싱과 언박싱에 대한 예를 보여주고 있다.
 

Int? nullable = 5;
 
//값이 있는 null 박싱
object boxed = nullable;
Console.WriteLine(boxed.GetType());
 
//일반 변수로 언박싱
int normal = (int)boxed;
Console.WriteLine(normal);
 
//null 변수로 언박싱
nullable = (int?)boxed;
Console.WriteLine(nullable);
 
//값이 없는 null 박싱
nullable = new int?();
boxed = nullable;
Console.WriteLine(boxed == null);
 
//null 변수로 언박싱
nullable = (int?)boxed;
Console.WriteLine(nullable.HasValue);

[박싱 언박싱 예제]
 
이 코드를 실행하면 다음과 같은 결과를 볼 수 있다. 


 
첫 번째 줄의 boxed의 타입을 보면 박싱된 값의 타입이 System.Nullable<System.Int32>가 아닌 System.Int32가 출력되는 것을 볼 수 있다. 이것은 GetType() 메서드를 호출하면 내부적으로는 Object 타입으로 박싱되기 때문에 Nullable 타입으로 반환되지 못한다는 것을 알아두자. 나머지 예제들은 nullable 변수로 박싱과 언박싱이 가능하다는 것을 보여주고 있다.

저작자 표시
신고
Posted by 박경훈

이번 강좌에서는 엔터프라이즈급 솔루션을 설계하기 위해서 반드시 알고 있어야 하며 닷넷 프로젝트 설계에 있어서 기본적으로 알고 있어야 하는 개념인 분산 트랜잭션의 개념과 COM+의 개념을 집중 해부 해보고자 한다이 글을 읽는 독자는 닷넷과 트랜잭션의 개념은 이미 알고 있다고 가정아래 썰을 풀도록 하겠다.

 

MTS(Microsoft Transaction Server)의 이해

 

분산 트랜잭션에 대한 이야기를 다루기 전에 10년 전 과거로 올라가 보고자 한다마이크로소프트는 엔터프라이즈급 규모로 확장 가능한 서버 애플리케이션 개발 환경을 지원하기 위해 마이크로소프트 트랜잭션 서버(MTS)를 1996년에 만들어 발표하였다. MTS COM DCOM으로 개발된 기업용 어플리케이션 시스템을 위한 실행 환경을 가지고 있고 무엇보다도 분산 컴퓨팅 환경에서 안정적이고 확장이 가능하다무엇보다도 가장 중요한 것은 분산 트랜잭션을 지원하고 있다는 것이다분산 트랜잭션 즉, DTC에 대한 자세한 이야기는 뒤에서 다루도록 하겠다.

 

MTS.. 트랜잭션 서버라는 명칭 때문에 어렵게 받아들일 수도 있지만 한 걸음 물러나서 보면 아주 간단한 시스템으로 생각할 수도 있다. MTS 기반의 모든 애플리케이션은 COM 컴포넌트의 집합으로 이뤄져 있으며이를 사용하는 클라이언트는 DCOM을 사용해 원격지에서 MTS 애플리케이션을 사용한다즉 마이크로소프트의 기반 기술인 COM/DCOM을 보다 효율적이고 안정적으로 사용 가능하게 만들어 주는 통합 환경으로 생각하면 된다.

 

COM/DCOM은 뭐냐?

 

닷넷부터 시작한 개발자라면 COM/DCOM을 직접 구현해보지 않았기 때문에 개념에 다소 혼란이 올 수 있을 것이다.아주 간단하게 정리해 보겠다. 1차적으로 개념으로 DLL->COM->DCOM으로 발전해 왔다고 개념을 잡도록 한다. DLL먼저 살펴 보자면 EXE라는 실행 파일에 의존적이고 유지보수에 있어서 많은 문제를 가지고 있다이런 단점을 보완해주는 것이 COM이라는 개념이고 특정 프로그래밍 언어에 의존적이지 않고 자원을 공유할 수 있도록 해주는 것이다.물론 COM 규격은 정해져 있고 그 규격에 맞아야 사용할 수 있을 것이다그럼 DCOM은 무엇인가? DCOMDistributed + COM 으로 풀 수 있다. COM을 여러 원격지에서 호출할 수 있다는 것이다. DCOM의 목록은 [관리도구]-[구성요소서비스에서 확인할 수 있다.

[DCOM 목록]

 

그럼 MTS를 발표할 그 당시의 반응은 어땠을까발표 당시에 TP-Monitor(Transaction Processing Monitor)라는 기능과ORB(Object Request Broker)의 기능을 동시에 처리하는 시스템이 존재하지 않았지만 바로 MTS가 이러한 두 가지 기능을 처리할 수 있어서 많은 개발자들의 주목을 모았었다그럼 TP-모니터와 ORB는 무엇인가먼저 TP-모니터는 트랜잭션이 이루어지는 과정을 모니터링 하는 기술이라 할 수 있다지금이야 엔터프라이즈급 환경에서 유용하게 사용할 수 있는 여러 미들웨어 제품들이 나와 있지만 그 당시에는 이런 기술들이 존재하지 않았다는 것이다.

 


[
윈도우 서버2003에서 제공하는 트랜잭션 통계기능]

 

그 다음 ORB CORBA라는 분산 기술에서 처음 등장한 개념이다. (그럼 CORBA가 또 무엇이냐라는 질문의 답은 분산 트랜잭션 이야기가 너무 삼천포로 빠지기 때문에 생략하도록 하겠다.)ORB는 객체들 간의 클라이언트/서버 관계를 맺어주는 미들웨어다. ORB를 사용하면 클라이언트는 서버객체에 있는 메서드를 같은 컴퓨터에 있든 또는 네트워크 상에 있든 상관없이 투명하게 호출할 수 있다. ORB는 호출을 가로채어 요구를 처리할 객체를 찾고매개변수를 전달하고메서드를 호출하고또 처리결과를 되돌려주는 일 등을 담당한다.

 

아무튼 이런 개념의 기술들이 도입된 MTS는 그 당시 많은 주목을 받았다는 것이다.

 

 

COM+와 분산 트랜잭션

 

앞에서 COM MTS에 대해서 이해해 보았다이 두 가지 개념이 잘 정립되어 있다면 COM+는 쉽게 이해할 수 있다. COM+ COM MTS의 핵심기능을 지원하고 있기 때문이다.

 



[COM+
의 정의]

 

닷넷 프로젝트를 설계할 때 COM+기반의 아키텍쳐를 도입하는 이유 중 하나는 바로 MTS 분산 트랜잭션을 이용하기 위한 목적도 상당한 비중을 차지할 것이다. COM+에서 제공되는 트랜잭션의 환경은 앞에서 설명한 MTS 실행 환경을 그대로 이어 받고 있다그렇기 때문에 서버 시스템 개발자가 직면한 다양한 문제를 해결하고 애플리케이션 본연의 업무에 충실할 수 있도록 만들어 준다시스템 부하는 약간 늘다 하더라도 개발자의 생산성과 시스템의 안전성은 그만큼 높아진다는 것이다다음은 트랜잭션기능을 자동으로 지원하는 COM+ 객체를 만드는 코드이다다음 코드만 보더라도 아주 간단하게 기능을 구현할 수 있는 것을 볼 수 있을 것이다이것이 바로 닷넷 프로젝트에서 COM+를 자주 도입하는 이유가 되기도 한다.

 

[Transaction(TransactionOption.Required)]

class A : ServicedComponent

{

  [AutoComplete]

  public void foo()

  {

    B obj = new B();

    try

    {

      obj.TransactionTest();

    }

    finally

    {

      obj.Dispose();

    }

  }

}

 

[Transaction(TransactionOption.Supported)]

class B : ServicedComponent

{

  [AutoComplete]

  public void TransactionTest()

  {

    // 데이터베이스 액세스 코드

  }

}

[자동 트랜잭션 처리 코드]

 

 

분산 트랜잭션(Distributed Transaction)

 

분산 트랜잭션의 내부 구조는 단일 트랜잭션에 비해서 많이 복잡하다다음 그림을 살펴보자.

 

 

이 그림을 보면 트랜잭션의 고유한 아이디를 이용하여 각 서버의 일관성을 유지한다.

 

먼저 DTC라는 개념을 정리해 보겠다. DTC(Distributed Transaction Coordinator)는 클라이언트 응용 프로그램이 한 트랜잭션에 여러 다른 데이터 원본을 포함할 수 있도록 해주는 트랜잭션 관리자이다. DTC는 트랜잭션에 포함된 모든 서버에서 분산 트랙잭션 커밋을 조정하고 있기 때문에 DTC에 분산 트랜잭션을 위임할 수 있는 것이다. DTC에선 위의 그림처럼TXID라는 고유한 아이디를 관리하게 된다. DTC는 많은 컴퓨터들의 연결을 처리하기 때문에 호출 객체의 고유한 아이디가 필요한 것이다로컬 트랜잭션은 하나의 커넥션을 가지고 있지만 분산 트랜잭션은 여러 개의 커넥션을 가지고 연결을 하게 되며 리소스 매니저에서 관리하게 된다여기서 반드시 잡아야 할 개념은 DTC는 트랜잭션 관리자라는 것이며 3자의 입장이라는 것이다.

 

단일 서버의 트랜잭션 관리자는 DTC가 아닌 LTM이 이용된다. LTM은 보다 좋은 성능을 가지고 있고 단일 서버에서만 사용될 수 있다성능을 좌우하는 것은 어떤 프로토콜을 사용하느냐에 따라서 좌우된다.

 

트랜잭션의 프로토콜

 

Lightweight 프로토콜

이 프로토콜은 같은 App 도메인 안에서 로컬 컨텍스트 안에서 트랜잭션을 관리하기 위해서만 사용된다.이 프로토콜은 App 도메인 경계 밖으로 전달이 불가능하고 또한 어떤 서비스 경계 밖의 트랜잭션 역시 전달이 불가능하다. Lightweight 프로토콜은 단지 서비스 안에서 혹은 서비스 밖에서만 이용된다. Lightweight 프로토콜은 다른 프로토콜과 비교해서 가장 좋은 성능을 가지고 있다.

 

OleTx 프로토콜

이 프로토콜은 App 도메인프로세스 그리고 머신들의 경계로부터 전달된 트랜잭션을 이용할 수 있고 두 단계의 커밋 프로토콜” 다룰 수 있다이 프로토콜은 RPC 호출을 이용하고 여기서 사용되는 정확한 이진 포맷은 Windows에 의존적이다하지만 RPC와 Windows-specific 포맷 둘 다 이용하기 때문에 윈도우 기반이 아닌 플랫폼과 상호운용이 어렵다그렇다고 특별히 문제될 것은 없다왜냐하면 OleTx를 이용하는 가장 주된 이유는 윈도우 기반의 인트라넷 안에서 트랜잭션을 관리하기 위해서이기 때문이다.

 

WS-Atomic Transaction (WSAT) 프로토콜

이 프로토콜은 App 도메인프로세스 그리고 머신들의 경계로부터 확장된 트랜잭션을 이용할 수 있고 두 단계의 커밋 프로토콜” 다룰 수 있다는 점에서 OleTx 프토토콜과 비슷하다하지만 OleTx 프로토콜과 다른 부분은 WSAT 프로토콜은 산업 표준 기반이라는 것이고 HTTP를 이용해 text 인코딩을 사용하는 경우 방화벽을 통과 할 수 있다. WSAT 프로토콜이 인트라넷에서 이용할 수 있지만 이것을 이용하는 주된 이유는 다중의 트랜잭션 관리자들이 호출되는 인터넷 기반에서 트랜잭션을 이용할 수 있다는 것이다.

 

 

두 단계의 커밋 프로토콜(Two-Parse Commit Protocol)

 

분산 트랜잭션의 복잡함을 극복하기 위해서 두 단계의 커밋 프로토콜을 이용하여 트랜잭션을 처리하게 된다다음 그림을 살펴보자.

 


[
두 단계 커밋 프로토콜]

 

두 단계 커밋 프로토콜은 두 단계로 작업을 진행한다. 먼저 첫 번째 단계에서는 Commit을 해도 되는지에 대한 여부를 수집하게 된다그리고 두 번째 단계에서 커밋을 할지 롤백을 할지를 결정하게 되는데 모든 리소스들이 커밋을 요청하게 되면 트랜잭션 관리자는 변경된 것들을 모두 커밋 시키게 될 것이고 만약 하나의 리소스라도 커밋을 원하지 않게 된다면 두 번째 단계에서 트랜잭션 관리자는 변경된 데이터들을 롤백 시키게 될 것이다그리고 트랜잭션은 중단되고 트랜잭션 이전 상태로 복구하게 된다.

 

 

닷넷 2.0에서는 COM+안 쓴다(?)

 

필자의 주위 닷넷 개발자들중에 닷넷 2.0에서는 COM+ 안 쓰잖아요~” 라는 말을 하는 것을 여러 번 들었다하지만 여기서 그 이유에 대해서 물으면 그냥 그렇게 들었다는 것이다. 이 말이 맞는 말인지 틀린 말인지를 떠나서 여기서 이런 말이 나오게 된 원인이 무엇일지 먼저 분석해 보도록 하겠다이미 알고 있는 독자도 있겠지만 이 말이 나오게 된 것은 닷넷 2.0에서 새롭게 추가된 System.Transaction 네임스페이스 때문이라는 생각을 먼저 해 볼 수 있다. 바로System.Transaction 네임스페이스는 기존 COM+에서 지원되었던 자동 트랜잭션을 지원하고 어느정도 제약이 있긴 하지만 분산 트랜잭션 기능 또한 지원해주고 있기 때문이다. 기존의 자동 트랜잭션 기능만을 위해서 COM+를 선택했다면 닷넷2.0에서는 더이상 COM+를 선택할 이유는 없다는 것이다.

 

그럼 트랜잭션 기능을 뺀다면 COM+ 볼품없는 기술이 되어 버린 것인가그래서 닷넷 2.0에서는 COM+를 쓰지 않는다는 말이 나오고 있는 것일까결론부터 말하자면 “No” 이다물론기존에 닷넷 시스템을 설계하면서 자동 트랜잭션과 분산 트랜잭션을 위해서 COM+를 선택했다면 그럴 수 있다여기서 간과하기 쉬운 COM+ 개념을 다시 정립할 필요가 있다. COM+는 미들웨어라는 것을 반드시 명심해야한다. COM+는 System.Transaction에서 제약을 가지고 있는 분산 트랜잭션뿐만 아니라 객체 풀링객체 생성 문자열메시지 큐 사용 그리고 분산 프로그래밍에서 가장 중요한 모듈 배포 및 관리를 해줄 수 있다는 점에서 COM+는 분명 아직까지 미들웨어의 핵심적인 기술임이 분명하다. 그렇기 때문에 엔터프라이즈 급 환경을 아키텍쳐 할때 반드시 1순위로 오르는 것이 COM+인 것이다. 여기서 System.Transaction의 프로그래밍에 대해서는 자세히 언급하지 않는다필요하다면 MSDN링크를 확인해보면 기능이나 프로그래밍에 대한 정보를 얻을 수 있을 것이다.

 

 

정리

 

닷넷의 기본 신조자체는 성능이 아닌 안전성이다닷넷 뿐만 아니라 최근에 여러 IT기술의 신조 역시 마찬가지일 것이다.하드웨어는 놀랍도록 발전하고 있고 충분히 성능을 끌어 올려 줄 수 환경은 이미 만들어져 있다이런 상황에서 단0.~~X 초에 민간 할 이유가 없다는 것이다그것보다 중요한 것은 엔드유저가 얼마나 더 신뢰를 가지고 사용할 수 있냐는 것이기 때문이다그래서 COM+나 자동 분산 트랜잭션의 기능은 성능의 부하가 있다 하더라도 반드시 큰 의미가 있는 기술이라는 것을 말하고 싶다.

 

또한 닷넷의 뛰어난 장점인 개발 생산성!! 이것은 말로 하지 않아도 충분히 이해할 수 있을 것이다앞에서 언급했듯이 트랜잭션을 개발자가 일일히 코드를 작성하기란 쉽지 않다그것이 분산 트랜잭션이라면 더 그렇다. 좀 더 가슴에 와닿을 만한 예를 들어 보자면 원격 RM(리소스 관리자)에 데이터를 처리하는 로직을 짜는데 조금 복잡한 로직이라고 가정해보자그럼 이 로직은 분명 에러가 발생할 가능성이 높을 것이다그래서 그 로직 사이에는 일일이 복구하는 로직을 개발자가 작성해야 하는 번거러움이 존재한다. 그런데 그 개발자가 작성한 복구 로직에서 에러가 발생하면 어떻게 될 것인가?여기서 더 난감한 것은 그런 복구로직을 테스트하는 작업이 더욱더 난감하다는 것이 문제고 개발자를 2~3번 죽이는 일이될 것이 분명하다. 이렇기 때문에 우리는 닷넷에서 친절하게 제공해 주고 있는 COM+ System.Transaction의 도움을 받아야 할 것이다.

저작자 표시
신고
Posted by 박경훈

흔히 닷넷기반의 환경을 관리되는(Managed) 환경으로 부르곤 한다. 그렇다면 도대체 무엇이 관리가 된다는 것인가? 그 관리의 주체는 바로 메모리이다. 이전 Native 시대에는 메모리를 할당하고 해제하는 부분을 개발자가 직접 처리하였지만 닷넷은 그 부분을 자동으로 관리해주는 것이다. 닷넷이 관리되는 환경의 수행이 가능할 수 있는 것은 바로 “가비지 컬렉터(Garbage Collector)”가 닷넷에 존재하기 때문이다. 관리환경의 장점을 최대한 활용하고 사용하기 위해서는 가비지 컬렉터에 대해서 잘 알아 두어야 하고 동작원리를 파악하고 있어야 한다. 필자는 가비지 컬렉터의 원리에 대한 내용을 2002년도 “Chappell의 .NET 여행”이라는 책에서 처음 접했었고, 이 내용은 닷넷의 메모리 관리를 이해하는데 어느 정도의 기반지식이 될 수 있었다. 

1. 가비지 컬렉터와 가비지 컬렉션

가비지 컬렉터는 앞에서 설명한 것과 같이 메모리를 관리해주는 메카니즘이다. 많이 혼동하는 부분이 바로 가비지 컬렉터와 가비지 컬렉션일 수 있다. 가비티 컬렉터는 Mark&Compact 알고리즘을 이용하여 객체들의 관계를 추적한다. 즉, 인스턴스화 시켰던 DataSet에 null을 할당하면 DataSet은 사용하지 않는 객체로 간주되고 가비지 컬렉션시에 메모리 해제의 대상이 된다. 하지만 DataSet은 DataTable을 가지고 있고, DataRow, DataItem과 같은 여러 객체들을 참조하고 있다. 바로 DataSet이 해제가 되면 그와 상호관련이 있었던 모든 객체들 역시 메모리를 해제해야 할 것이고 바로 가비지 컬렉터는 이러한 복잡한 관계를 Mark&Compact 알고리즘을 이용해서 이해하고 각각 해제될 수 있는 것이다. 

가비지 컬렉션은 바로 메모리를 해제하고 새롭게 재배치 하는 작업을 칭하고 자동으로 수행되지만 수동으로도 수행을 명령할 수도 있다. 가비지 컬렉션이 수행되는 시기는 개발자가 알 수 없지만 분명한 것은 메모리가 부족하면 분명 가비지 컬렉션이 일어난다는 것이다. 

System.GC.Collect();


가비지 컬렉터는 세대별로 나누어서 메모리를 관리한다. 즉, 메모리를 관리하는 그릇이 3개가 존재한다고 보면 된다. 세대는 0,1,2 세대로 나누어지고 최초의 메모리는 무조건 0이라는 공간에서 관리가 된다고 보면 된다. 그리고 가비지를 한번 정리하였지만 해제되지 않은 객체는 바로 다음 세대로 이동하게 된다. 가비지 컬렉션은 세대별로 독립적이라고 보면 된다. 즉, 가비지 컬렉션이 일어날 때 0,1,2 세대 별로 동시에 발생하는 것이 아니라 개별적으로 가비지 컬렉션을 수행한다는 것이다. 최근의 생긴 메모리 수록 즉, 0세대일 수록 컬렉션이 많이 발생한다. 이것은 당연한 동작 원리이다. 보통 한번 사용한 객체는 꾸준히 많이 사용하지만 그렇지 않은 객체는 단기적으로 사용을 많이 하기 때문이다. 다음 [그림1]은 가비지 컬렉터가 가비지 컬렉션을 수행하는 장면을 보여주고 있다.

그림1
[그림1]가비지 컬렉션의 동작

이렇게 수행을 하고 남은 객체는 바로 그 위의 세대로 승격된다. 다음 [그림2]를 살펴보자.


그림2

[그림2]다음 세대로 승격


이 승격은 2세대까지 2번 승격된다. 0세대는 활발하게 가비지 컬렉션이 수행되지만 2세대는 거의 발생되지 않는다고 보면 된다.


2. 메모리의 효율적인 사용

앞에서 살펴본 가비지 컬렉터는 관리되는 환경의 모든 메모리를 책임지고 관리한다. 이전 Native 시대에서는 메모리 누수와 같은 문제가 발생하면 개발자의 책임이었지만 닷넷에서는 그 책임을 대신 주어준다는 것이다. 하지만 관리되어지는 환경이라고 해서 모든 것을 전적으로 가비지 컬렉터에 의존해서는 안 된다. 물론 가비지 컬렉터가 하드웨어가 무척 발전한 지금의 환경에 부응하고 있는 기능임은 틀림없다. 하지만 필자가 말하고자 하는 것은 가비지 컬렉션이 언제 어떤 객체를 수집하고 어떻게 동작하는 것임을 알아두고 그에 맞추어 코드를 작성해야 한다는 것이다. 이런 효츌적인 메모리 관리를 위해서 몇 가지 지침들을 적어보도록 하겠다.

- 코드에서 System.GC.Collect()를 이용해서 직접 가비지 컬렉션의 수행을 호출하는 것은 가급적 피한다. 

닷넷에 연고가 있는 독자라면 가비지 컬렉션을 수동으로 수행하면 안좋다 라는 말을 많이 들어봤을 것이다. 그럼 이제 그 이유를 살펴보자. 이유는 크게 두 가지 이유가 있다. 첫 번째는 가비지 컬렉션이 수행하는 메카니즘이 생각보다 간단하지 않기 때문이므로 성능상 부하가 있을 수 있다. 왜냐하면 현재 객체가 사용 중인지 확인하는 작업이 필요하며 그 작업이 끝난 후에 객체를 파괴한다. 객체를 파괴할 때에는 참조되고 있는 객체 역시 파괴해야 한다는 것이다. 뿐만 아니라 파괴된 객체들의 빈자리를 매꾸기 위해서 가비지 컬렉터는 객체들을 재배치 작업(Compaction 작업)을 수행하게 되므로 메모리가 많을수록 그 부하가 클 수 있기 때문이다. 두 번째 이유는 객체가 승격되기 때문이다. 객체가 0세대에서 한번 승격되면 그 객체가 더 이상 사용하지 않는다 하더라도 자동적으로 그 객체가 정리될 확률이 줄어들기 때문이다. 그렇기 때문에 코드에서 직접 가비지 컬렉션의 수행의 호출을 추천하지 않는 것이다. 

- 사용하지 않는 객체는 가비지 컬렉터가 수집할 수 있는 대상으로 설정한다.

그렇다면 가비지 컬렉터가 수집하는 대상은 어떤 대상인가? 좀 더 쉽게 이해하기 위해서 다음과 같은 클래스를 만들었다고 가정하자.


Class A
{
  string a="HOONS"
}
Main메서드()
{
  A aClass= new A();//클래스 생성
}


그렇다면 여기서 aClass객체를 사용하고 있는지 어떻게 계산할 것인가이다. 가장 쉬운 방법은 aClass에 null을 대입해 주면 되기도 하지만 가장 바람직한 방법은 using을 이용해서 객체를 사용하는 범위를 지정해주는 것이다. using은 IDisposable 인터페이스를 상속받아서 Dispose() 메서드를 구현해야 한다. 그리고 Dispose()에서는 클래스 안에서 사용했던 자원을 해제하는 것이다. finalizer(~생성자())를 구현한다면 가비지 컬렉터는 자원을 해제하기 바로 직전에 수행하게 될 것이다. 이 finalizer에서는 만약 Dispose()를 호출하지 않을 것을 대비해서 Dispose()를 수행하는 용도나 아니면 관리되지 않은 영역 즉, unmanaged 자원이 있다면 이 자원을 호출하면 된다. 


- 전역변수는 가급적 초기화하지 않는다.

다음과 같은 코드가 있다고 가정하자.

Class A
{
  ArrayList a=new ArrayList();
  public A(int Length)
  { 
      a=new ArrayList(Length); 
  }
}


이 코드는 내부적으로 다음과 같이 동작된다. 

Class A
{
  ArrayList a=new ArrayList();
  public A(int Length)
  {
      a=new ArrayList();
      a=new ArrayList(Length); 
  }
}


그렇기 때문에 필요없는 가비지가 생기게 되는 것이다.

- 자주 사용하는 객체는 전역변수로 잡아서 가비지를 최소화한다.

자주 사용하는 객체는 여러 개를 만들어 사용할 없다는 것이다. 이 부분은 이론상 쉽게 이해할 수 있으므로 깊게 언급하지 않겠다.



3. CLR 프로필러

그렇다면 가비지 컬렉터에서 내부적으로 관리되고 있는 메모리를 어떻게 들여다 볼 수 있는 것인가? 이것은 Microsoft에서 제공하고 있는 CLR 프로필러(Profiler)라는 툴을 이용하면 된다. 이툴은 MS 다운로드 사이트에서 다운 받을 수 있으며 다음 [화면1]은 CLR 프로필러를 실행한 화면을 보여주고 있다. 


화면1
[화면1] CLR 프로필러

이 프로필러는 각 세대별로 가비지 컬렉션이 일어난 횟수를 보여주고 있고 현재 각 세대별로 차지하고 있는 힙의 크기 그리고 할당되고 재할당 된 메모리 바이트를 보여주고 있다. 뿐만 아니라 참고 그래프를 제공하고 있기 때문에 메모리 사용을 분석하는데 큰 도움을 줄 수 있다. 이 툴의 자세한 내용은 이전 HOONS 닷넷에서 진행한 세미나에서도 언급한 적도 있고, 이전 마소에서도 다루었던 적이 있으므로 자세한 사용방법은 지면상 생략하도록 한다.

저작자 표시
신고
Posted by 박경훈