가끔 우리 솔루션에서는 System.Runtime.Serialization.SerializationException이 발생한다. 문제가 발생한 상황이니까 예외가 던져지는 것은 당연하다. 하지만 문제는 실제 에러는 SerializationException이 아니라는 것이었다. 그리고 SerializationException은 다들 알다시피 객체가 Serialization/Deserialization하는 과정에서 나는 문제인데, 이 에러가 날만한 시나리오를 딱히 생각해낼 수는 없었다.
Stacktrace를 보니 이 예외는 솔루션 내부의 로깅 컴포넌트에서 던져지고 있었다. 로깅 컴포넌트는 에러나 문제가 발생했을 때 그 정보를 파일에 기록하는 역할을 하는 COM+ 서버 타입 컴포넌트였다. 문제가 생긴 함수는 다음과 같았다.
Public Function WriteException(ByVal userName as string, ByVal exception as System.Exception) as string
이 함수는 Exception객체를 받아서 그 예외 정보를 포맷한 뒤 파일에 쓰는 함수였다. 여기서 문제가 될만한 부분은 인자로 System.Exception 객체를 받는 부분뿐이었는데, 로깅 컴포넌트가 COM+ 서버 타입이기 때문에 Exception객체가 프로세스간 이동을 하기 때문이었다. 하지만 System.Exception 객체가 Serialization 과정에서 문제가 생긴다는 것은 사실 상상하기가 힘들었는데..
암튼 조사 끝에 이유를 알아내었다. 문제가 생긴 객체는 모두 System.Web.HttpException이었다. 그리고 이 System.Web.HttpException이 무슨 이유인지는 몰라도, .NET Framework 1.1에서는 Serializable로 마킹이 되어 있지 않다는 사실을 알게 되었다. 즉 Logging컴포넌트에서 HttpException을 인자로 받았는데, 이게 COM+ 서버 타입이라 프로세스 이동을 하려면 Serialization과정을 거치다가.. Serializable 마킹이 안 되어 있어서 Serialization작업을 못한 것이다. 그래서 SerializationException도 났던 것이고..
.NET Framework 2.0에서는 Serializable 어트리뷰트가 있지만, .NET Framework 1.1에서는 없는 것을 볼 수 있다. 왜 1.1에서 없는지는 속시원한 설명을 제대로 찾지 못했다. 현재로서는 버그가 아닐까 추측만 될 뿐이다. 그리고 2.0이상부터는 이런 문제가 없는 것 같다.
우리 솔루션은 로깅 컴포넌트의 COM+ 유형을 Server Type에서 Library Type 으로 바꾸어서 해결했다. Library Type이라면 프로세스가 다르지 않을테고 프로세스간 객체 이동도 없을 것이니, 이 문제를 신경쓰지 않아도 되는 것이다.
다른 곳에도 이 케이스처럼 Exception객체가 프로세스를 넘어가는 시나리오가 있다면, 이 문제를 조심해야 할 것이다.
메서드를 작성할 때에, Argument가 다 정상적으로 들어올거라고 가정해서는 절대로 안 된다. 반드시 다음과 같이 Validation 코드를 작성해서, 메서드의 가장 위에 둘 필요가 있다.
1. Argument 가 null인지 검사해서, null이라면 NullArgumentException을 던져야 한다. 아래 코드는 .NET Framework의 System.Windows.Annotations.Annotation 클래스의 WriteXml메서드의 가장 윗부분 코드이다.
public void WriteXml(XmlWriter writer) { if (writer == null) { throw new ArgumentNullException("writer"); } }
2. 각 argument값 자체의 이상 유무를 검사한다. 기본적인 string 이라면, 전에 올렸던 글처럼 IsNullOrEmpty 메서드를 써서 검사하고, 다른 type이라면.. -_-;; 다양한 것을 다 여기서 다루기는 힘드므로.. 일단 적절하게 검사한다. 아래 예제는 위에서도 사용한 System.Windows.Annotations.Annotation 클래스의 WriteXml메서드의 일부분인데, 넘어온 XML의 내부 스트링들을 IsNullOrEmpty메서드로 검사해서, 없을 경우 채워넣는 코드이다.
if (string.IsNullOrEmpty(writer.LookupPrefix("http://schemas.microsoft.com/windows/annotations/2003/11/core"))) { writer.WriteAttributeString("xmlns", "anc", null, "http://schemas.microsoft.com/windows/annotations/2003/11/core"); } if (string.IsNullOrEmpty(writer.LookupPrefix("http://schemas.microsoft.com/windows/annotations/2003/11/base"))) { writer.WriteAttributeString("xmlns", "anb", null, "http://schemas.microsoft.com/windows/annotations/2003/11/base"); } }
System.Diagnostics.StackTrace
objStackTrace = new System.Diagnostics.StackTrace(new
System.Diagnostics.StackFrame(1));
stackTraceString = objStackTrace.ToString();
return stackTraceString;
마지막에 return되는 값은 다음과 같이 생겼다. 이것이 바로 현재 메소드를 호출한 Caller Method이다.
at
StackFrameTest.Class3.class3Method(String param)
* StackFrame 클래스를 생성할 때 주는 파라미터 - Integer - 를 조정해서, 각 호출 스택 상의 레벨을 모두 추적할 수도 있다. * 간단하게 ToString()을 하면 저렇게 나오게 되지만, StackFrame 객체에는 GetFileName, GetFileLineNumber 등의 더 자세한 정보를 알 수 있는 메소드들도 있다.
PublicClass Metadata Private myMetadata As New InsightMetadata
Private Shared _singleton As Metadata Private Shared myInstanceMutex As New Mutex
Private Sub New()
End Sub
Public Shared Function GetInstance() As Metadata
myInstanceMutex.WaitOne()
Try If _singleton Is Nothing Then
_singleton = New Metadata
End If
Finally
myInstanceMutex.ReleaseMutex()
End Try
Return _singleton
End Function
Public ReadOnly Property Appset(ByVal appsetID As String, ByVal app As String, ByVal userName As String, ByVal context As String, ByVal security As String) As Appset
Get If(IsNothing(myMetadata.Appset(appsetID))) Then
load(appsetID, app, userName, context, security)
End If
Return myMetadata.Appset(appsetID)
End Get
End Property
Public Overloads Sub refresh(ByVal appset As String, ByVal app As String, ByVal userName As String, ByVal context As String, ByVal security As String)
load(appset, app, userName, context, security)
End Sub
Private Function load(ByVal appsetID As String, ByVal app As String, ByVal userName As String, ByVal context As String, ByVal security As String) As Appset
... End Function
End Class
이 코드는 클래스 이름으로도 대충 짐작이 가듯이 전체 프로그램의 공용 Meta 데이터들을 저장해 놓는 모듈이다. 이런 메타 데이터들은 많은 모듈에서 사용하면서도 프로그램이 구동 중일 때는 거의 바뀌지 않는다는 속성이 있다. 그래서 이런 메타데이터는 메모리에 하나 올려놓고, 그것을 모두 사용하면 가장 좋을 것이다. 매번 읽어오지도 않고, 각 모듈에서 각각 호출하지도 않게 하는 최선의 방법일테니까 말이다.
싱글턴 패턴
이렇게 메모리에 딱 하나의 인스턴스를 올려놓을 필요가 있을 때 쓰는 것이 바로 "Singleton" 패턴이다. "Design Patterns"에 나오는 정의를 보자면 "ensure a class has only one instance, and provide a global point of access to it" 즉 "한 클래스가 단지 하나의 인스턴스만을 가지게 하고, 그것에 액세스할 수 있는 글로벌한 포인트를 제공하는 것"이라고 되어 있다. 아래 위키 백과 페이지를 참조하면 더 자세하게 알 수 있다.
싱글턴 패턴은 쓰임새가 그렇게 다양하진 않다. 주로 위의 코드와 같은 메타 데이터, 혹은 파일 로깅 등의 공용 모듈에서 사용된다. (물론 위의 조건 - 하나의 인스턴스, 글로벌한 액세스 - 을 충족시킨다면 그 쓰임새 자체는 제한이 없다) 하지만 아래 글에서 볼 수 있듯이 이 Singleton 클래스는 다른 클래스들과 단단하게 결합되는(tightly-coupled) 경향이 있고, 이는 Unit Test를 어렵게 하고 전체적으로 각 모듈의 독립성을 저해하는 요소로 작용할 수도 있다. 아래와 같은 글들을 참고할 만 하다. (물론 주의깊게 읽어야 할 것이다. 단지 주장일 뿐이니)
MSDN의 Article도 괜찮긴 하지만, 2번째 글은 그야말로 총정리다. C#에서 가능한 모든 경우의 Singleton 구현에 대해서 모두 그 장단점을 서술해놓고 있다. 2번째 글을 보면 이 글의 처음에 나온 코드는 (MSDN 글을 봐도 그렇다) 그다지 좋지 못한 예라는 것을 알 수 있다..^^ 그래서 나도 현재 코드를 바꾸고 있는 중이다. 2번째 글의 네번째 버전으로(아래와 같이) 바꿀 예정이다. MSDN Article에 있는 코드와도 거의 같다고 보면 된다.
아직 여기 Tistory는 API를 제공하지 않지만, 대부분의 블로그 서비스는 블로그 API를 제공한다. 이런 API를 써서 꼭 블로그에 접속하지 않고도 블로그의 포스트 리스트를 가져온다거나, 블로그를 작성하거나 할 수 있다. (이를테면 MS Word 2007에서는 Blogger, MSN Space 등의 블로그에 Post할 수 있는 메뉴를 제공한다) 이런 API 중에서 대표적인 것이 Blogger ATOM API와 RFC MetaWebLog API이다.
다음 샘플은 Blogger Atom API를 써서 자신의 Blogger Blog 목록을 가져오는 C# 샘플이다. 설명은 코드의 주석으로...^^;;
//WebClient 클래스는 간단하게 인터넷 익스플로러라고 생각하면 된다.
//즉, 프로그램 내부에서 인터넷 페이지를 열거나 데이터를 post/get 하는 등의 일을 할 수 있는 객체이다.
//Blogger ATOM API는 XML-RPC를 사용하기 때문에 이 객체를 사용해서 데이터를 가져오거나 쓸 수 있다. System.Net.WebClient oClient = new System.Net.WebClient();
//Content-type은 반드시 application-xml로 설정해야 한다. oClient.Headers.Add("Content-type", "application/xml");
//ATOM API는 HTTP SSL 기본 인증을 사용한다.
//기본 인증 token은 아이디:비밀번호를 Base64로 인코딩한 값이 사용된다.
//아래 코드 중 Base64Encode라는 함수는 내 이전 글(http://kkongchi.net/1602055)을 참조한다 oClient.Headers.Add("Authorization", "BASIC " + this.Base64Encode("YourID:YourPassword", System.Text.Encoding.UTF8));
//자신의 블로그 리스트를 읽어와서 TextBox에 넣는다. string s = oClient.DownloadString("http://www.blogger.com/atom/"); this.textBox2.Text = s;