Visual Studio 2010 공식 팀 블로그 @vsts2010

Posted by 오태겸(RuAA)
여름의 정점을 지나가고 있는 듯 합니다. 날씨가 무진장 덥네요,,
장마가 끝나면 폭염이라는데,, ㅡ,.ㅡ;;;
식상한 멘트이긴 하겠지만, 이런 날일 수록 정말 건강이 중요한 것 같습니다.
덥다고 에어컨 바람만 쐬면 냉방병 걸리고, 밤에 잘때도 너무 에어컨 틀어놓으면 감기 걸리니깐,,
아프지 않는게 최고죠~ 특히, 저 같이 혼자 자취하는 자취남들은,, ^^
아무쪼록 건강하세요~


이번 아티클의 주제는 Windows Service를 이용한 호스팅입니다.

윈도우즈 서비스에 대해서 모르는 분이 계실까요?
네~ 계실 수 있죠,, 그래서 간략하게 윈도우즈 서비스에 대해 설명을 하고 넘어가도록 하겠습니다.

What is Windows Service?

NT 서비스라고 알려져 있기도 한 윈도우즈 서비스는 자체의 Windows 세션에서 실행되며, 윈도우즈가 구동을 하고 있는 동안 계속 동작을 해야하는 이러한 작업이 필요할 때, 윈도우즈 서비스의 형태로 구현하여 사용할 수 있습니다. 또한, 윈도우즈 서비스는 윈도우즈의 시작과 함께 자동으로 시작할 수 있기 때문에, 서버가 동작함과 동시에 항상 동작해야하는 응용 프로그램이라면 윈도우즈 서비스로 구현하는 것을 고려해 볼 필요가 있을 듯 합니다.

윈도우즈 서비스의 경우는 따로 UI를 가지고 있지 않기 때문에, 윈도우즈 사용자 또는 관리자가 윈도우즈 서비스의 구동 상태를 상세하게 확인할 수는 없습니다. 다만, 윈도우즈에 기본으로 제공되는 서비스 제어 관리자를 통해 서비스의 시작, 중지, 일시정지 등을 컨트롤 할 수 있습니다. (아래 그림은 서비스 제어 관리자의 모습입니다.)


그림을 보니 윈도우즈 서비스가 어떠한 것들을 말하는 것인지 알겠죠? ^^ (역시, 백문이 불여일견,, 엣헴~)

Windows 서비스 응용프로그램에 대한 소개와 개발 방법에 대해 좀 더 자세한 정보를 원하시는 분은 다음 링크를 참고하시기 바랍니다.

"Windows 서비스 응용 프로그램 소개"

이제 본격적으로 이 윈도우즈 서비스를 이용하여 WCF 서비스를 호스팅하는 방법에 대해 적어보겠습니다.
고고씽~

Windows Service를 이용한 WCF 서비스 호스팅

WCF 서비스를 호스팅하는 방법에는 IIS 호스팅, WAS 호스팅, 그리고 셀프 호스팅으로 나뉘어진다고 얘기했었습니다.

그럼, Windows Service를 이용한 호스팅은 어디에 속할까요?
네~!! 당연히 셀프 호스팅에 속합니다. 그리고, 셀프 호스팅에 속한다는 말은 직접 ServiceHost 클래스를 이용하여 호스팅을 구현 해야 한다는 말이기도 합니다.

결국, 여기서 제가 하고 싶은 말은 이것입니다.
 "Windows Service를 이용하여 WCF 서비스를 호스팅하기 위해서는 ServiceHost 클래스를 이용하여 서비스 호스팅하는 부분을 직접 구현해야 한다."
 
WCF 서비스를 호스팅하기 위한 특별한 코드가 필요한 것은 아닙니다. 여타의 다른 윈도우즈 서비스를 개발하는 것과 같이 개발을 하고, WCF 서비스를 호스팅하는 코드만 추가를 해주면 된다는 것입니다~!!
아마, 윈도우즈 서비스를 한번이라도 개발 해 보신 분은 어렵지 않게 개발을 할 수 있을 것 같습니다.

우선, 윈도우즈 서비스 응용프로그램을 만들기 위해 Visual Studio에서 새 프로젝트를 생성할 때, "Windows 서비스" 템플릿을 선택합니다.


프로젝트를 생성하면 Service1.cs 와 Program.cs 파일이 생성되어 있음을 확인할 수 있습니다.
Program.cs 파일은 이 응용 프로그램의 진입점을 가지고 있으며, 실제 서비스가 동작을 할 때의 코드는 Service1.cs 파일에 구현을 하면 됩니다.

그리고, Service1.cs 파일을 확인하면 Service1 클래스가 선언되어 있으며, 이 클래스는 ServiceBase 클래스를 상속 받는 것을 확인할 수 있습니다.
그리고, Service1 클래스에는 기본적으로 OnStart와 OnStop 메소드가 재 정의(override)되어 있는데 메소드의 이름으로 짐작할 수 있겠지만, 각각 서비스가 시작할 때 와 서비스가 멈췄을 때의 동작을 수행하는 메소드입니다.

이 메소드 외에, OnContinue, OnPause, OnShutdown 메소드를 재정의하여 서비스를 일시정지에서 다시 시작했을 때, 서비스를 일시정지 했을 때, 그리고 컴퓨터가 종료될 때의 동작을 구현할 수 있습니다.

이번 아티클의 주제는 Windows 서비스를 이용한 WCF 서비스의 호스팅이니 만큼, Windows 서비스 구현에 대한 자세한 내용은 앞서 걸어놓은 MSDN의 링크로 대신하고 넘어가겠습니다.
(Windows 서비스 응용 프로그램의 구현에 대한 자세한 내용을 모르셔도 아래 내용을 따라 하시면, 아마 무사히 WCF 서비스를 호스팅 할 수 있으실겁니다. )

음,, 일단 우리가 만들 Windows 서비스의 이름을 먼저 변경해보도록 하겠습니다. 이름을 변경하지 않아도 별 상관은 없지만 우리가 만든 서비스란 것을 알아보기 위해서 변경하는게 낫겠죠~ 서비스의 이름을 변경하는 것은 어렵지 않습니다. Service1 클래스의 생성자에서 바꿔줘도 되고, Service1.Designer.cs 파일을 확인하면 Service1 의 partial 클래스가 정의되어 있는데, 이곳에 위치한 InitializeComponent 메소드 내에서 변경해줘도 됩니다. 저는 InitializeComponent 메소드 내에서 다음과 같은 코드로 서비스의 이름을 지정 해주었습니다.

private void InitializeComponent()

{

     components = new System.ComponentModel.Container();

  this.ServiceName = "RuAA WCF Service"; // 서비스의 이름 변경

}


이제 본격적으로 WCF 서비스를 호스팅 해보도록 하겠습니다.
우선, WCF 서비스에서 사용할 ServiceContract와 DataContract 들을 선언해주어야 겠죠. 다음과 같은 인터페이스와 클래스들을 선언해주었습니다.

// ServiceContract 정의
[
ServiceContract]

public interface IProductService

{

    [OperationContract]

    Product GetProductInfo(int id);

}


// DataContract 정의
[
DataContract]

public class Product

{

    [DataMember]

    public int ID;

    [DataMember]

    public string Name;

    [DataMember]

    public string Company;

    [DataMember]

    public int Price;

}

// 서비스 구현

public class ProductService : IProductService

{

    List<Product> productList;

 

    public ProductService()

    {

        productList = new List<Product>();

        productList.Add(new Product {

            ID = 1,

            Name = "ABC Chocolate",

            Company = "RuAA Inc.",

            Price = 5300

        });

    }

 

    public Product GetProductInfo(int id)

    {

        var item = (from p in productList

                    where p.ID == id

                    select p).FirstOrDefault();

 

        return item;

    }

}

항상 그랬지만, 서비스의 역할은 심플합니다. 복잡한 서비스를 만드는게 목표는 아니잖아요~ ㅎ

이제 이렇게 구현된 서비스를 호스팅하는 일 만을 남겨두었습니다.

앞에서도 밝혔듯이 Windows 서비스에서 WCF 서비스를 호스팅 하는 것은 Self Hosting 에 포함되는 것이기 때문에 ServiceHost 클래스를 직접 구현해야 합니다.

근데 이 코드를 어디에 위치해야 할까요? 예상하신 분들이 분명 계실겁니다.
바로 바로 바로~~!! Service1 클래스의 OnStart 메소드입니다. 

또한, 생각해야 할 것이 있습니다.
Windows 서비스가 멈추었을 때, 당연히 WCF 서비스의 호스팅을 멈추어줘야 겠죠. 그래서 OnStop 메소드 내부에 WCF 서비스의 호스팅을 멈추게 하는 코드도 포함이 되어야 할 것입니다.

Service1 클래스를 다음과 같은 코드로 구현해보았습니다.

// ServiceHost 클래스의 인스턴스를 클래스의 멤버로 선언하여 Service1 클래스의 메소드에서 접근이 가능하도록 한다.
private
ServiceHost svcHost;

// 윈도우즈 서비스가 시작할 때의 동작을 구현한다.

protected
override void OnStart(string[] args)

{

    // ServiceHost 인스턴스 생성
    string baseUrl = "http://10.30.101.84:9090/ProductService";

    this.svcHost = new ServiceHost(typeof(ProductService), new Uri(baseUrl));

    // 엔드포인트 추가
   
this.svcHost.AddServiceEndpoint(typeof(IProductService),

        new BasicHttpBinding(),

        "");

           

    // 메타 데이터 엔드포인트를 위한 Behavior 설정
    ServiceMetadataBehavior metaBehavior = new ServiceMetadataBehavior();

    metaBehavior.HttpGetEnabled = true;

    svcHost.Description.Behaviors.Add(metaBehavior);

 

    // 메타 데이터 엔드포인트 추가

    this.svcHost.AddServiceEndpoint(typeof(IMetadataExchange),

        MetadataExchangeBindings.CreateMexHttpBinding(),

        "mex");

 

    svcHost.Open();  // WCF 서비스 오픈

    ServiceEndpoint endpoint = this.svcHost.Description.Endpoints[0];

 

    // 윈도우즈 이벤트 로그에 정보를 남긴다.

    EventLog.WriteEntry(endpoint.Contract.Name + " Started"

        + " listening on " + endpoint.Address

        + " (" + endpoint.Binding.Name + ")",

        System.Diagnostics.EventLogEntryType.Information);

}

 

// 윈도우즈 서비스가 멈췄을 때의 동작을 구현한다.

protected override void OnStop()

{

    this.svcHost.Close();

    EventLog.WriteEntry("RuAA Service Stopping", EventLogEntryType.Information);

}
 
위의 코드에서 그렇게 어려운 점은 보이지 않습니다. Self Hosting을 해보셨던 분이라면 말이죠~
혹시나, Self Hosting을 해보지 못하신 분이 있다면, 첫 WCF 만들기 아티클을 참고해주시기 바랍니다.

각 주요 코드에 주석도 남겨놨으니 따로 긴 설명은 필요없을 듯 합니다. ^^

이제, 서비스의 구현은 모두 끝이 났습니다. 하지만, 우리가 만든 이 Windows 서비스를 컴퓨터에 설치하기 위해서는 설치 관리자가 필요합니다. (설치 관리자에 대한 자세한 설명은 이곳으로~)
Service1.cs 파일의 디자이너 보기에서 마우스 우측 클릭한 후 나타나는 메뉴에서 "설치 관리자 추가"를 선택합니다.


그러면 프로젝트에 ProjectInstaller.cs 라는 파일이 생기는데, 이 클래스의 디자인 뷰를 확인하면,  serviceProcessInstaller1, serviceInstaller1 이라는 이름의 컨트롤들이 포함되어 있는 것을 확인할 수 있습니다.

이 컨트롤들의 속성을 다음 그림과 같이 수정해보겠습니다.



이제서야, 정말 Windows 서비스를 설치할 모든 준비가 끝이 났습니다.
이 Windows 서비스를 설치하기 위하여 Visual Studio 명령 프롬프트를 실행시킵니다. 이때, 관리자 권한으로 실행시켜 주셔야 합니다. 그렇지 않으면 Windows 서비스가 제대로 설치가 되지 않는 경우가 있더라구요~ ㅎ

그리고, 이 프로젝트 폴더의 bin/Debug 폴더로 이동한 후에 다음 그림과 같이, Installutil 이란 명령어를 이용하여 우리가 만든 Windows 서비스를 설치합니다. (Windows 서비스 설치에 대한 자세한 설명은 이곳으로~!!)
참고로, 설치 파일은 프로젝트를 빌드한 후에 생성된 exe 파일입니다.


위의 그림처럼 "트랜잭트 설치가 완료되었습니다." 란 메세지가 떨어졌다면 아무 에러없이 Windows 서비스가 설치되었다는 말입니다. 그럼, 서비스 제어 관리자에서 확인해 보도록 하겠습니다.


네,, Windows 서비스가 올라와있는 것을 볼 수 있네요~ 그럼 시작 버튼을 클릭하여 Windows 서비스를 시작할 수 있습니다. 그리고, Windows 서비스가 시작하면서 우리가 구현한 WCF 서비스가 호스팅되겠죠,, ㅎㅎ

그럼, 이 WCF 서비스가 제대로 호스팅 되고 있는 것인지 확인을 해보아야 할겁니다,,
역시나, 지금까지 그래왔듯이 콘솔 어플리케이션을 이용하여 확인해보도록 하겠습니다.

솔루션에 콘솔 어플리케이션 프로젝트를 추가하고, 이 프로젝트에 서비스 참조를 시켜주었습니다. 이때 너무나도 당연하겠지만, 서비스의 주소는 ServiceHost 인스턴스에 추가 시켜준 엔드 포인트의 주소를 넣어주셔야 합니다.ㅎ


위의 그림처럼, 서비스를 제대로 찾으면~ 모든 것이 OK!!! 입니다. ㅎㅎ

이 서비스를 이용한 콘솔 어플리케이션 코드는 생략해도 되겠죠?? ㅎㅎ (이번 글이 너무 길어진 것 같아,, ^^;;;;)
그래서~~~ 결과 화면만 보여드리겠습니다 ㅎㅎ



후아~ 결과가 잘 나오는 군요,, ^^
이로써, 이번 포스팅도 무사히(?) 끝을 낼 수 있게 되었습니다. ㅎㅎㅎ

이번 포스팅은 Windows 서비스에 대한 설명과 구현에 대한 내용을 함께 적다보니 조금 길어진 것 같습니다. 물론 한번이라도 Windows 서비스 응용 프로그램을 구현해보신 분이라면 아는 내용들이겠지만, 혹시나 모르시는 분도 있을 것 같아 자세한 내용까지 설명드리지 못했지만 최소한의 내용을 포함시켰습니다.

조금 내용이 길어졌지만 끝까지 읽어주신 분들께 심심한 감사의 인사를 드리며, 저는 이만 퇴근(?)하겠습니다 ㅎㅎ
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. Windows NT 서비스를 이용하여 접목할 수 있는 실제 사례가 인상 깊네요. 강좌 잘 읽었습니다. :-)

Posted by 엄준일(땡초)

지난 회 차에 여러 가지 문제로 .NET 스마트클라이언트가 가진 문제점을 살펴 보았습니다. 그 중, 주된 이슈는 이미 로드된 어셈블리는 업데이트/갱신이 불가능하다는 것과, 메모리의 사용률이 지속적으로 증가한다는 문제입니다. 이러한 문제는 사내 정책적인 서버를 도입하여 해결 가능하지만, 대부분의 조직과 기업은 이러한 정책 서버를 도입하지 않는 것으로 알고 있습니다.    

이미 얘기 했다시피, 평소에도 .NET 에서 이러한 문제를 가지고 고민을 했었지만, 최근 이러한 문제가 이슈가 되었을 때 더 이상 필자 또한 방관할 수 없었습니다. 왜냐하면 "안된다!" 라는 것 자체가 .NET 의 많은 매리트를 배제한다는 의미가 될 수 있기 때문입니다. 이러한 문제로 '목숨거는' 고객이라면 차라리 '지금은 곤란하다. 조금만 기다려달라' 라는 답이 훨씬 나아 보입니다. 물론 이런 문제가 가능하다는 전제 조건으로 말입니다.

   

문제 해결 방안

일단, 몇 날 몇 일을 고민하며 생각한 끝에 아래와 같은 아키텍처링을 하게 되었습니다. 물론 최선의 방법도, 최적의 방법도 아니지만, 문제가 된다면 저에게 피드백을 주시기 바랍니다. 저 또한 짧은 지식으로 이러한 고민을 하게 되었으니 저도 많이 답답하네요^^;    

   

위의 아키텍처링은 논리적인 아키텍처입니다. 이 방법을 통해 이전 아티클을 통해 골치 아픈 .NET 어플리케이션의 메모리 릭(Memory Leak) 을 해결할 수 있을 것으로 기대합니다.

   

어플리케이션과 AppDomain 의 분리

.NET 어플리케이션은 기본적으로 하나의 프로세스(Process) 를 차지하게 됩니다. 그것이 독립 프로세스든, IEHost.DLL 또는 IEExec.EXE 든 간에 말이죠. 이 독립 프로세스는 독립적인 하나의 어셈블리에서 관장하게 됩니다. 기본적인 이 부분의 컨셉은 어플리케이션의 재시작을 방지하기 위한 방법이기도 합니다.    

기존의 어플리케이션의 프로세스와 AppDomain 을 분리함으로써 최소한으로 AppDomain 이 안전하게 언로드될 수 있는 환경을 제공하는 것입니다. 그리고 위해 AppDomain Manager 는 이것을 관장하는 최상위 Manager Layer 가 됩니다.

   

MVVM 으로 구현부와의 분리

MVVM(Model View ViewModel) 패턴의 가장 큰 특징은 View 와 ViewModel 을 분리한 것입니다. 이것을 분리함으로써 View 와 ViewModel 의 종속 관계를 완전히 해결하고, ViewModel 은 격리된 AppDomain 으로 제한함으로써 언제든지 AppDomain 이 언로드될 수 있게 합니다.    

이 부분을 구현하기 가장 이상적인 환경은 바로 WPF(Windows Presentation Foundation) 이 되겠네요.

   

Views 의 교체

MVVM 으로 구현부와의 분리를 통해 당연히 Views 는 언제든지 교체가 가능합니다. 서버/로컬에서 Views 가 교체된다면 ViewModels 을 언로드하고 새로운 Isolated AppDomain 을 생성하여 View 와 ViewModel 간에 연결하는 방법입니다.    

특히 이 통신 구간은 View 와 ViewModel 간의 Interface Contract 를 통해 크리티컬한 자원의 관리를 최소화하는 것에 있습니다. 이로써 이미 로드된 사용자 화면과 어셈블리라도 서버/로컬의 갱신이 있다면 언제든지 갈아치울 수 있는 구간입니다. 이 부분이 앞서 얘기한 .NET 스마트클라이언트의 문제를 해결할 수 있는 핵심 구간입니다.

   

업데이트 기능을 재작성

이 아키텍처링의 가장 큰 문제지만, .NET 스마트클라이언트의 NTD(No Touch Deployment) 기능을 그대로 사용할 수 없습니다. .NET 의 NTD 는 이미 실행되는 AppDomain 에 어셈블리를 로드하기 때문에 .NET 의 NTD 를 그대로 사용한다면 이 아키텍처링을 적용할 수 없습니다.

NTD 기능 뿐만 아니라, ClickOnce 의 자동 버전 감지 기능도 사용할 수 없습니다. ClickOnce 는 주기적으로 서버의 Application Manifest 를 확인하는 과정으로 새로운 버전을 감지하고 업데이트하는데, 이 기능을 그대로 사용한다면 위의 아키텍처링은 사실상 무의미하고, 결국 메모리 사용 증가는 해결할 수 없기도 합니다.

   

제한 사항

하지만 필자가 제안한 .NET 스마트클라이언트의 문제를 해결하기 위한 방법은 제한적인 방법으로 수행이 가능합니다. 물론 모든 경우라도 제안이 가능한 방법이라면 좋겠지만, .NET 의 기본 아키텍처가 해결하지 못한 이상, 필자 또한 제한적인 방법으로 .NET 스마트클라이언트의 문제점을 해결할 수 있습니다.

그 제한적인 방법의 한계는 아래와 같습니다.

  • 개발 표준을 완벽하게 MVVM 기반으로 개발되어야 한다.
  • MVVM 패턴으로 완벽하게 분리가 되어야 한다.
    • WPF 를 사용할 경우 MVVM 패턴으로만 작성되어야 한다.
    • 윈도우 폼(Windows Forms) 또는 ActiveX 컨트롤 일 경우, MVVM 로 작성할 수 없다.
    • 이 경우, View와 ViewModels 를 분리하도록 별도 프레임워크 개발이 필요하다.
  • Marshaling 을 통한 통신
    • Marshaling 은 AppDomain 간의 원격 통신을 해야 한다.
    • 원격 통신으로 인한 성능 저하
  • WPF 개발
    • Binding Expression 을 확장한 Binding Marshaling Expression(단지, 예임) 으로 바인딩을 해야 한다.
    • 원격 바인딩으로 성능 저하 예상

   

결론

필자는 .NET 어플리케이션이 업데이트될 경우 왜 반드시 최적의 방법이 어플리케이션 쉘을 재시작하느냐에 시작한 고민으로부터 시작됩니다. .NET 아키텍처를 이해못하는 것은 아니지만, 고객은 언제나 더 향상된 방법을 제안합니다. 그리고 필자는 그런 고민을 극복하고자 제안한 방법입니다.

물론 위의 아키텍처링을 효율적인 면과 성능적인 면을 더 자세히 테스트해 보아야 하겠지만, 분명한 것은 끊임없이 고객의 요청은 진화하지 퇴화하지는 않을 거라고 생각합니다.

예전에 필자는 위와 같은 문제를 문의할 때, ".NET 에서는 안된다" 라고 답했습니다. 맞아요. 안됩니다. 하지만 문득, '안되면 되게 해야지!' 라는 생각이 들더군요. 짧은 소견이지만 잘못된 부분이 있으면 언제든지 피드백 주시기 바랍니다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

Posted by 엄준일(땡초)

개요

.NET 에서 윈도우 어플리케이션을 개발해 본 독자라면 한번 쯤은 .NET 스마트클라이언트라는 용어를 많이 들어보았을 것입니다. 스마트클라이언트는 배포(Deployment), 플랫폼 독립 모델을 제공함으로써 다양한 클라이언트를 지원하는 것이 특징입니다.

예전에 필자가 UX 라는 주제로 쓴 포스트 중 "당신이 생각하는 UX 란?" 에서도 언급하였듯이, .NET 스마트클라이언트는 X-Internet 이라는 트랜드로 기술적인 부분을 초점으로 마케팅한 용어로 발전하였습니다. 이와 반대로 RIA(Rich Internet Application) 는 UX(User eXperience) 초점에서 마케팅한 용어라고 보셔도 좋습니다.

   

사전 지식

하지만 .NET 스마트클라이언트는 사실상 매번 나오는 이슈가 있습니다. 아니, 이것은 .NET 스마트클라이언트의 문제라기 보다는 .NET 자체의 아키텍처와 관련된 문제이기도 합니다.

결혼부터 말하자면, .NET 어플리케이션은 로드된 어셈블리(Loaded Assemblies) 는 언로드(Unload) 가 되지 않습니다. 간단하게 아래와 같이 .NET 어플리케이션의 모델을 보면 알 수 있습니다. .NET 어플리케이션은 하나의 AppDomain(Application Domain) 을 갖는 것을 알 수 있습니다.

   

AppDomain 은 어플리케이션 간의 CAS(Code Access Security) 라는 임계 영역에 존재하게 됩니다. 말 그대로 CAS(Code Access Security) 이 CAS는 어플리케이션간의 엑세스를 제한함으로써 신뢰할 수 없는 코드나 어플리케이션은 사용자의 컴퓨터에서 실행할 수 없도록 한 보안 모델입니다.    

즉, 이메일이나 인터넷, 사용자 그룹 및 권한 등 신원이 확인되지 않은 어플리케이션을 실행했을 때, 악의적인 목적으로 사용자의 로컬 자원을 엑세스할 수 없도록 제한하는 모델이라고 보시면 됩니다.    

이 코드 보안 모델은 .NET 의 어떤 어플리케이션이든 모두 이 보안 정책 안에 있다고 보시면 됩니다. ASP.NET 도 마찬가지로 아래와 같이 AppDomain 의 임계 영역 안에서 어플리케이션이 동작하게 됩니다. AppDomain 이 하나의 웹 어플리케이션을 동작하게 하고, HttpRuntime 에 의해 HttpContext 가 관리됩니다. 그리고 각각의 요청에 의해 HttpContext 는 별도의 스레드(Thread) 로 사용자의 요청을 응답하게 되는 구조라고 보시면 됩니다.

 예를 들어, 아래와 같은 코드 보안을 위한 선언적인 방법을 이용하여 악의적으로 사용될 수 있는 코드 쓰기, 수정 등을 할 수 없도록 합니다. 어셈블리, 클래스, 구조체, 생성자에서 사용할 수 있습니다. 물론 사용자가 이 보안 수준을 변경할 수 도 있지요.

문제 1

여태까지 이것을 말하기 위해 설명을 한 것입니다. 바로 .NET 어플리케이션은 어셈블리를 로드할 수 는 있지만, 언로드할 수 는 없습니다.

그러니까 더 자세하게 얘기하면, 아무리 가비지 컬렉션(Garbage Collection) 을 호출하고 CLR Runtime(Common Language Runtime) 이 이것을 대신 수행해 준다고 해도, 로드된 어셈블리 자체는 이 대상에서 예외라는 것입니다. 결론은 .NET 어플리케이션을 오래 쓰면 쓸 수록 메모리 사용이 증가할 가능성이 있습니다.

플러그인 모델(Plugin Model) 기반의 어플리케이션도 확장 기능이 많아지면 많아질 수록 메모리 점유율이 높아지고, 특히 엔터프라이즈 기업용 어플리케이션은 반드시 피해갈 수 없는 문제이기도 합니다.    

개인적으로 플러그인 모델과 엔터프라이즈 어플리케이션의 중간 영역이라고 생각되는 Visual Studio 를 한 1주일 정도 닫지 않고 써보셨나요? 쓰지 못할 정도는 아니지만, 괜히 버벅되고 느려지는 현상이 나타나게 된답니다.^^; 이런 현상은 Visual Studio 뿐만이 아니라 .NET 으로 작성된 모든 어플리케이션은 모두 영향을 받게 됩니다.

   

그 이유는, .NET 은 로드된 AppDomain 의 어셈블리를 언로드할 수 있는 방법을 제공해 주지 않습니다. AppDomain 이 참조하는 관계는 기본적으로 로컬 자원의 어셈블리를 참조하겠지만, 코드 베이스(Code Base-코드의 출처) 가 인트라넷이나 인터넷이라면 그 코드 베이스로부터 어셈블리를 다운로드 하게 됩니다.    

문제 2

결론부터 말하면, .NET 어플리케이션은 참조 또는 다운로드한 어셈블리는 다운로드 캐시(Download Cache) 에 보관하게 됩니다. 어셈블리를 참조 또는 다운로드하는 판정 조건은 어셈블리의 버전, 토큰 등 복잡한 과정을 거치기 때문에, 제대로 된 정책을 갖고 있지 않는다면, 이미 다운로드된 어셈블리는 다운로드 캐시로부터 어셈블리를 재사용합니다.    

그렇기 때문에, 다운로드된 어셈블리는 File Lock(파일 잠김)이 발생하므로, 동일한 파일 이름의 어셈블리를 다운로드 받는 것은 불가능 합니다. 하지만 해결책이 없는 것은 아닙니다. Assembly.Load 시리즈의 메서드에는 byte[] 로 읽을 수 있는 오버로드된 메서드가 존재하기 때문입니다.    

즉, 아래와 같이 File Lock 을 방지할 수 있습니다. 하지만 어셈블리는 로드할 수 있으나, 기존의 로드된 어셈블리를 갈아치우지는 못합니다.

 

결국, 하나의 어플리케이션을 오래 사용하면 할수록 메모리의 점유율을 증가할 수 있게 될 가능성이 큽니다. 특히 엔터프라이즈 기업용 어플리케이션은 단위 업무별로 적절한 파일 크기, 업무간의 연간 관계 등을 고려하여 어셈블리를 모듈화하는데, 사실상 메모리 사용률 증가의 문제는 여전히 해결할 수 없는 문제입니다. 그 이유는, 앞서 말했듯이 어셈블리를 언로드할 수 있는 방법은 AppDomain 을 언로드하는 것이고, AppDomain 을 언로드하면 메인 어플리케이션을 재시작해야 된다는 문제입니다.

   

문제 3

이 섹션은 문제 2와 연관된 정책적인 문제입니다. 다운로드된 어셈블리는 다시 다운로드 받을 수 없기 때문에 선행적으로 몇 가지 정책적인 강제가 필요할 수 밖에 없습니다.

  • 어플리케이션 쉘(Shell)
    • 어플리케이션 쉘이 업데이트되면 어플리케이션을 재시작 해야 한다.
  • 어플리케이션 실행 중 단위 어셈블리
    • 단위 어셈블리가 한 번 다운로드되면 서버/로컬의 어셈블리가 갱신되도 다운로드 받지 못한다.
    • 단위 어셈블리가 다운로드 되고 서버/로컬 어셈블리가 갱신되어도 알림 받을 수 없다.
    • 이럴 경우, 어플리케이션 쉘을 서버에서 갱신하여 업데이트 알림을 받을 수 있고, 어플리케이션을 재시작 해야한다.

즉, 어떠한 경우라도 갱신된 어플리케이션을 적용하기 위해서는 메인 어플리케이션 쉘을 재시작해야 한다는 결론을 얻을 수 있습니다.

   

문제 4

더욱 문제인 것은 .NET Framework 4.0 기반의 일부 스마트클라이언트는 이 문제와 상관없이 불가능합니다. 그 이유는 이미 닷넷엑스퍼트의 안재우 수석님의 블로그 중 "[.NET 4.0] IE Embedded WinForm(Smart Client) 지원 중단" 를 참고하세요.

이유의 요지는, IEHost.DLL 과 IEExec.EXE 파일이 .NET Framework 2.0 으로 강력한 이름의 서명이 되었다는 것입니다. 이것은 즉, IEHost.DLL 과 IEExec.EXE 를 통하는 .NET 스마트클라이언트의 경우 GAC(Global Assembly Cache) 를 통해 활성화가 되는데, .NET Framework 4.0 의 스마트클라이언트 어플리케이션은 어셈블리 리디렉트(Assembly Redirect)를 통하지 않고서는 이것을 활성화할 수 있는 방법이 없습니다. 어셈블리 리디렉트를 통한다고 하더라도 Dependency Assemblies 는 .NET Framework 2.0 을 바라보기 때문에 .NET Framework 4.0 의 기능을 사용한다면 절대 불가능하기도 합니다.

하지만 .NET 어셈블리의 바이트 코드 조작을 통해서 가능하긴 합니다.

  • IEHost.DLL, IEExec.exe 의 바이트 코드를 수정하여 강력한 서명을 지운다
  • IEHost.DLL, IEExec.exe 의 바이트 코드를 수정하여 .NET 4.0 으로 저장한다
  • GAC(Global Assembly Cache) 에서 IEHost.DLL 과 IEExec.EXE 를 제거한다.

어셈블리의 바이트 코드 조작은 Mono 프레임워크를 통해서 아주 쉽게 할 수 있습니다. 하지만 IEHost.DLL 과 IEExec.EXE 를 사용하는 모든 사용자 클라이언트를 해킹하는 무자비한 방법입니다. 하지만 된다는 것만으로도 만족한다면 이 방법이 최선의 방법이 될 것 같네요.

   

.NET 스마트클라이언트의 고찰

.NET 스마트클라이언트는 .NET 엔터프라이즈 어플리케이션에 많은 기여를 하였습니다. 그리고 .NET 스마트클라이언트를 사용하는 기업 또는 인트라넷 환경은 매우 많기도 합니다.    

필자 또한 얼마 전에 이러한 고민으로 Microsoft 의 의뢰를 받은 적이 있습니다. 그리고 개인적으로 아주 많이 고민했습니다.    

왜냐하면 자바의 클래스 로드(Class Loader) 는 .NET 의 스마트클라이언트와 유사한 점이 굉장히 많습니다. 하지만 다른 점이 하나 있다면, 자바의 클래스 로더는 GC(Garbage Collection) 의 대상이 된다는 것이죠. 다시 말하면, 어플리케이션의 재시작 없이 마음만 먹으면 메모리 사용률이 증가하지 않도록 아키텍처링이 가능하다는 것입니다.    

필자가 결론적으로, .NET 의 AppDomain 과 자바의 클래스 로더는 각기 특색은 있지만, 어느 것이 정답인지는 모르겠습니다. 다만, 고객이 어플리케이션의 재시작 없이 어플리케이션 업데이트/갱신이 가능해야 한다는 전제 조건이라면 자바의 클래스 로더가 장점이긴 합니다.    

하지만, 필자는 이 문제로 몇 일 동안 고민했습니다. 왜냐하면 세상에는 불가능한 것이 없다라는 것이 필자의 신념이기도 하며, 어떤 문제든 최선의 방법이라는 것이 존재한다고 믿습니다. 그리고 결국 "빙고" 를 찾았습니다. ^^

다음 회 차에서는 .NET 스마트클라이언트의 이러한 문제를 개선할 수 있는 방법을 알아보도록 하겠습니다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 오타가 있네요^^; '결론부터 말하자면' 이 부분이 결혼으로 되어있네요 ㅋ;

Posted by 오태겸(RuAA)

바야흐로, 여름입니다.
아ㅡ 정말 이놈의 귀차니즘 덕분에 너무 띄엄띄엄 포스팅이 되는 것에 대해 죄송하다는 말씀 먼저 드려야할 것 같습니다.
여름이라 더워서 그렇다고 핑계대지 않을게요, 휴가 시즌이라 놀고 싶어서 그렇다고 핑계대지 않을게요~ ;;;
잡담은 여기서 줄이고, 힘을 내어, 이번 포스팅을 시작해 보겠습니다. 레츠 고우~
 
지난 포스트의 주제는 WAS 호스팅이었습니다. 이번 주제는 조금 다르긴 하지만 지난 포스트에 이어서 Hosting과 관련된 내용을 적어볼까 합니다.

WCF 서비스를 호스팅하기 위해 가장 쉬운 방법이 무엇인지 다들 아시죠?
제 개인적인 생각인지는 모르겠지만, Visual Studio를 사용하여 WCF 서비스를 만든다면, 아마도~ 가장 쉬운 호스팅 방법은 IIS 호스팅일 것입니다. 솔루션 만들고 별 수정없이 바로 호스팅이 가능하니깐요,,

갑자기 왜 IIS 호스팅에 대한 얘기를 꺼내냐구요?
음,, 오늘 제가 꺼낼 이야기가 IIS 호스팅일 때 WCF 서비스에 ASP.NET의 몇 가지 특성을 적용할 수 있는 방법에 대한 내용을 적으려다 보니... 네!! 결국, 제가 하고 싶은 얘기는 이번 포스팅에서 나오는 방법들이 IIS 호스팅을 바탕으로 한다는 것을 명심(?)해 달라는 것입니다. ㅎㅎ

닷넷 웹 서비스 와 WCF 서비스

WCF 서비스 얘기를 할 때, 가장 비교를 많이 하는 것이 아마 .NET 웹 서비스 일 듯 합니다. (지금도 이 닷넷 웹 서비스에 대해 얘기를 하려 하구요~ ㅎ)

닷넷 웹 서비스와 WCF 서비스의 가장 큰 차이는 무엇일까요?
구현하는 방법에 대한 차이도 있겠지만, 그것보단 WCF 서비스가 HTTP 프로토콜 이외의 프로토콜(net.tcp, net.pipe, MSMQ)을 이용하여 접근이 가능하게 호스팅할 수 있다는 점일 것입니다. (WAS를 이용한 호스팅 참조)

닷넷 웹 서비스는 WCF 서비스와는 다르게 HTTP 프로토콜만 지원합니다. 그리고 이는, ASP.NET HTTP 파이프라인을 따르고 있습니다.

"아ㅡ ASP.NET 파이프 라인은 또 뭔가요?" 라고 원망 섞인 소리가 여기까지 들리는 것 같습니다. 저도 아직 실력이 미천한 개발자라 자세히 설명드릴 수는 없습니다.
간단하게 설명 드리자면, ASP.NET 에서 Http 프로토콜을 이용하여 들어오는 요청(request)과 응답(response)을 처리하기 위한 파이프라인입니다. 즉, 어떤 요청에 대해서 어떻게 필터링을 수행하고, 어떤 어플리케이션을 호출할 것인지를 처리하며, 파이프라인을 통해서 그에 대한 응답을 전송하는 것입니다.

자세한 내용을 알고 싶은 분은 다음을 참고 하시면 될 것 같습니다.
Securely Implement Request Processing, Filtering, and Content Redirection with HTTP Pipelines in ASP.NET

닷넷 웹 서비스는 이렇게 ASP.NET HTTP 파이프라인을 사용하기 때문에 많은 ASP.NET 의 특징을 함께 사용할 수 있다는 장점을 가지고 있습니다. 이러한 장점에는 다음과 같은 것들이 포함 됩니다. 

  • Authentication
  • Url/File authorization
  • Impersonation
  • Session state
  • Request cache
  • Globaliztion
  • Etc

목록을 보니 인증과 권한에 대한 것, 그리고 세션과 관련한 것들이 있네요~

이제 WCF 서비스 얘기를 해볼까요?
WCF 서비스는 닷넷 웹 서비스와는 다르게 non-HTTP 프로토콜들을 지원해줍니다. 그리고, 이러한 장점을 위하여 프로토콜에 독립적인 디자인을 사용하게 된 것입니다.

조금 둘러서 얘기를 했지만, 제가 하고 싶은 말은 이것입니다. 
"닷넷 웹 서비스와는 다르게 WCF 서비스는 ASP.NET HTTP 파이프 라인을 따르지 않는다!!"


음,, 그렇군요. WCF 서비스는 ASP.NET HTTP 파이프 라인을 따르지 않는군요... 앗~ 그렇다면 닷넷 웹 서비스의 경우 ASP.NET HTTP 파이프 라인을 따랐기에 여러가지 ASP.NET의 특성을 사용할 수 있었는데,, 그럼, WCF 서비스에서는 ASP.NET의 특성들을 사용할 수 없는걸까요??

네~!! 기본적으로는 그렇습니다.
하!지!만!!! ASP.NET 에는 여러 유용한 특성들이 존재했기에 이를 완전히 버리기는 아까웠을겁니다. WCF 서비스를 위해서 같은 특성들을 다시 만들기 보다는 기존에 있던 것들을 가져다 쓰는 방향으로 개발하고 싶었겠지요~(제 개인적인 생각입니다. 아니면 말구요~ ㅎ) 

어떤 이유인지는 확실치 않지만, 어찌됐든 중요한 것은, WCF 서비스에서도 기존의 ASP.NET 특성들을 (전부는 아니고 일부분의 특성들을) 사용할 수 있는 방법을 제공해주고 있다는 것입니다. 단, HTTP 프로토콜을 사용하는 WCF 서비스에서만요~

이번에도 역시 둘러둘러~ 이제서야 본론으로 들어온 것 같습니다 ^^
그럼, 이제 본격적으로 WCF 서비스에서 ASP.NET의 유용한 기능들을 사용하기 위한 방법에 대해서 얘기해보도록 하겠습니다.


WCF 서비스에서 Session 사용하기

이번 포스팅에서는 ASP.NET의 특징들 중에서 간단하게 Session을 사용하는 예제를 구현해보려 합니다. (차후에 WCF의 보안에 대해 포스팅을 할 때에는 Impersonation 과 관련한 예제도 보여드릴 수 있을 것 같습니다.)
여기서 Session은 WCF 의 인스턴스를 생성할 때의 모드인 InstanceContextMode.Session 과는 전혀 무관합니다. 다들 알고 계시리라 생각하지만 혹시나 싶어서요~ ㅎ

우선, ASP.NET의 특징들을 사용하기 위해서는 크게 두 가지의 설정이 필요합니다.

첫 번째, Application Level 에서의 설정이 필요한데, 이는 WCF 서비스 프로젝트의 web.config에서 <system.serviceModel> 의 자식 요소인 <serviceHostingEnvironment> 요소의 속성 aspNetCompatibilityEnabled 의 값을 true로 명시하여 설정할 수 있습니다. 

두 번째로, Service Level 에서의 설정이 필요합니다. 이는 WCF 서비스를 구현하는 클래스에 AspNetCompatibilityRequirements 특성을 통해 설정을 할 수 있습니다.

코드를 보면서 하나씩 해보도록 하죠~

WCF 솔루션을 하나 만듭니다. 좀 편하게 작업을 하기 위해서 셀프 호스팅보다는 Visual Studio 에서 제공해주는 "WCF 서비스 응용 프로그램" 템플릿을 사용하도록 하겠습니다.

그리고, 다음과 같이 서비스 계약을 위한 interface와 WCF 서비스에서 사용할 개체인 Product 클래스를 정의했습니다.

[ServiceContract]

public interface IProductService

{

    [OperationContract]

    Product GetProduct(string ticker);

}

 

[DataContract]

public class Product

{

    [DataMember]

    public string Name;

    [DataMember]

    public int calls;

    [DataMember]

    public double price;

    [DataMember]

    public string RequestedBy;

}


이 다음에 할 일은 당연히 서비스를 구현하는 것이겠죠.
이 서비스에서는 session을 사용하여 현재 메서드가 호출되는 횟수를 기록, 유지하도록 했습니다.

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]

public class ProductService : IProductService

{

    public Product GetProduct(string ticker)

    {

        Product p = new Product();

        int nCalls = 0;

        // I .

        if (HttpContext.Current.Session["cnt"] != null)

            nCalls = (int)HttpContext.Current.Session["cnt"];

        HttpContext.Current.Session["cnt"] = ++nCalls;

 

        p.Name = "Caramel Latte";

        p.calls = nCalls;

        p.price = 2500;

        p.RequestedBy = "RuAA";

          

        return p;

    }

}


이 코드에서 다시 한번 유의해서 보아야 할 부분은 역시 ProductService 클래스 위에 선언 된 AspNetCompatibilityRequirements 특성입니다. 그 값을 Required 로 주었네요~
이 부분은 앞에서 ASP.NET 특성들을 사용하기 위한 설정 중 Service Level에서의 설정에 해당하는 것이었습니다.

그럼, 이제 Application Level에서의 설정을 해주어야 겠군요. 
web.config 파일을 다음과 같이 수정합니다.

<system.serviceModel>

  <behaviors>

    <serviceBehaviors>

      <behavior>

        <serviceDebug includeExceptionDetailInFaults="true"/>

      </behavior>

    </serviceBehaviors>

  </behaviors>

  <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />

</system.serviceModel>


밑줄 쳐 있는 부분을 주목하시면 되겠습니다~ ㅎ
이 부분을 추가함으로써 첫 번째 Application Level 에서의 설정까지 완료한 것입니다.
설정이라고 하기엔 너무 간단한가요? ㅎ

이제, 이 서비스를 검증해보아야 겠죠,, 세션을 잘 유지하는지,,
매번 그래왔듯이 콘솔 어플리케이션을 이용하여 클라이언트를 간단히 만들어 보겠습니다.

서비스 참조를 하고, 다음과 같은 코드를 작성하였습니다. 

static void Main(string[] args)

{

    ProductServiceClient client = new ProductServiceClient();

    Product p = null;

    for (int i = 0; i < 5; i++)

    {

        p = client.GetProduct(i.ToString());

        Console.WriteLine(" : {0}", p.calls.ToString());

        Console.WriteLine(" : {0}", p.Name);

        Console.WriteLine(" : {0} ", p.price.ToString());

        Console.WriteLine(" : {0}", p.RequestedBy);

        Console.WriteLine();

    }

}


이 코드에선 그렇게 중요한 부분이 없습니다. 단순히 서비스의 GetProduct 메서드를 연속해서 5번 호출을 해주는 것 밖엔,, 복잡한건 싫으니깐 이렇게 간단히~ ㅎ

그리고 실행을 해보도록 하죠~
 

앗~ 뭔가 이상합니다. 우리가 예상했던 그런 결과가 나오지 않는군요. 세션이 유지가 되었다면 호출 횟수의 값이 1씩 증가하여 1~5의 값을 보여주어야 할텐데 말이죠.......

이런 결과가 나오는 이유는 바로,, Session을 사용하기 위해서는 클라이언트에서 쿠키를 허용해주어야 하는데,  기본 HTTP 바인딩인 basicHttpBinding 과 wsHttpBinding이 기본적으로 쿠키를 허용하지 않기 때문입니다.
HTTP 바인딩에서 쿠키를 허용해주기 위해선 config 파일에서 binding 태그의 allowCookies 속성의 값을 true 로 바꿔주시면 됩니다. 다음과 같이 말이죠~

<basicHttpBinding>

    <binding name="BasicHttpBinding_IProductService" closeTimeout="00:01:00"

        openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"

        allowCookies="true" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"

        maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"

        messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"

        useDefaultWebProxy="true">

    ……

</ basicHttpBinding>


이제는 모든 설정이 완벽하게 끝이 난 것 같군요.
다시 실행을 해 보겠습니다.


아~ 이제 예상했던대로 결과 값이 나오는 것 같군요.. ㅎ

문득, 서비스의 InstanceContextMode의 값이 바뀌면 어떻게 될지 궁금해지지 않으신가요? ㅎ
한번 직접 해보시면 알겠지만 이 세션은 InstanceContextMode의 값(PerSession, PerCall, Single)이 무엇이 되든지 간에 유지됩니다. (다들 한번씩 해보시길~ ^^)

이번 포스팅은 여기까지하고 줄이도록 하겠습니다.

사실 이 포스팅은 제가 월드컵이 시작할 때 같이 시작했었는데, 결국 월드컵이 끝날 때 같이 끝나게 되었네요,,
이 놈의 귀차니즘 덕분에 포스팅이 항상 늦게 올려져서,, 정말 죄송한 마음밖엔 없는 것 같습니다.
일을 하면서 포스팅을 한다는 것 자체가 쉬운 일이 아님을 깨달았습니다.(MVP 분들이 존경스러워 지는군요,,ㅎ) 그래도 포기하면 안되겠죠,, ㅎ

다음 포스팅 때 뵙겠습니다. 꾸벅~
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. asp.net compatibility mode를 사용하게 되면 System.Web.HttpApplication , System.Web.HttpContext 를 이용해서 행할수 있는 대부분의 작업이 가능해집니다. 이 부분에 대한 설명이 없는 부분이 다소 아쉽네요 ^^;

    • 오태겸 2010/07/12 17:40

      아쉬운 부분을 보완해서 loveciel 님이 한번 포스팅 해주시지요~ ㅎ

  2. 비밀댓글 입니다

    • 오태겸 2010/07/13 17:28

      비밀댓글이라 저한테 보이지 않네요,, 무슨 내용이길래 비밀로 댓글을 다셨는지,, 너무 궁금하게 만드시는데요~ ^^;;

  3. 항상 좋은 글 감사합니다. 질문이 있는데요.. WCF로 채팅 프로그램 만들고 있습니다. 클라이언트간의 파일 전송을 하려고 하는데..(네이트온처럼..)

    채팅이나 쪽지 같은경우는 서비스를 거쳐서 통신을 하게 했는데.. 파일전송도 서비스를 거치면 부하가 많이 걸릴듯하고.. 클라이언트간에 직접 파일전송을 하려고합니다.. 혹시 어떤 방식으로 하면 되는지 힌트 좀 얻을 수 있을까요?

    혹은 참고할 주소나 예제가 있으면 좀 부탁드립니다. on2god@naver.com

    감사~~~

    • 오태겸 2010/07/19 09:51

      보통 클라이언트간에 직접 파일 전송을 위해서 P2P 프로토콜을 사용합니다. 저도 이 프로토콜들에 대한 자세한 정보를 알지는 못하지만 여러 프로토콜이 있는 것으로 알고 있습니다. P2P 프로토콜에 대한 자료를 찾고, 공부해서 적용해보면 좋은 경험이 될 수 있을 것 같네요~ ^^

Posted by Mr.Fear
필자를 포함해서 여러명의 개발자들은 WCF 서비스를 이용할때 당췌 알수 없는 이유의 오류때문에 무한 야근을 한 경험이 있을거라고 확신한다. 이번엔 그중에 하나이면서 , 지금까지 명확하게 설명되지 않았던 오류 하나를 살펴보고자 한다.

<exception>
This collection already contains an address with scheme http. There can be at most one address per scheme in this collection.
Parameter name: item


한글로는?

이 컬렉션에는 스키마 http을(를) 가진 주소가 이미 있습니다. 이 컬렉션에서 스키마당 주소는 하나만 존재할 수 있습니다.
매개 변수 이름: item
설명: 현재 웹 요청을 실행하는 동안 처리되지 않은 예외가 발생했습니다. 스택 추적을 검토하여 발생한 오류 및 코드에서 오류가 발생한 위치에 대한 자세한 정보를 확인하십시오.

예외 정보: System.ArgumentException: 이 컬렉션에는 스키마 http을(를) 가진 주소가 이미 있습니다. 이 컬렉션에서 스키마당 주소는 하나만 존재할 수 있습니다.
매개 변수 이름: item



분명

로컬 호스트에서는 넘흐넘흐 잘 돌아갔던 우리 프로그램. 서버에 올려봤더니 요지부동. 절대 동작할 생각을 안하는 우리 WCF 서비스. 당신은 과연 이 메시지를 본적이 있는가?

이 문제는 하나의 WCF 서비스는 오로지 1개의 호스트로만 서비스되야 하는 규칙이 존재하기 때문이다.

Microsoft 는 이 문제에 대해 처음 해결책을 제시하였다.(이글은 IE에서 클릭했을때만 보입니다.)
http://blogs.msdn.com/rampo/archive/2008/02/11/how-can-wcf-support-multiple-iis-binding-specified-per-site.aspx

그러나 이글은 www.loveciel.com 이란 바인딩으로 접근했을때는 A 라는 메서드를 호출하게 할수 있는 시나리오로 , 사용자가 원하던 방식인 멀티 도메인으로 호출했을때 동일한 호출을 기대하는것은 아니었다.

그리고 그에 따른 방식을 사용자가 제안하였을때 Microsoft 의 대답은 다음과 같았다.

Lets say ServiceA is bound to Services.xxx.com and ServiceB is bound to xxx.com.

I am trying to understand why ServiceB cannot call directly call services.xxx.com. The baseAddressPrefixfilter workaround should further help to host these two services under the same site.

If you did want two base address today the only workaround is to have two copies of the service.


뭐 간단히 말하면 , 여러개의 호스트로 WCF서비스를 호출하는건 안되고 , 정 그렇게 하고 싶으면 두개의 카피를 만들라는 이야기였다. 그리고 이 Microsoft 측은 이 이슈를 종료시켜 버렸다.

그리고 프로그래머는 분노했다 -_-;;

WTF? "By design"? This is a blocking issue for us, and we are now turning back to ASMX handlers, thanks WCF Team! You did a great job by putting in fundamental "design" flaws in WCF.

But oh well, MS connect is useless anyway. They don't take this feedback seriously... THE workaround is to not use WCF and go back to ASMX handlers. Period.


WTF 는 What the fuck? 의 줄임말이다. 처음 이 댓글을 봤을때 얼마나 웃었는지 ㅎㅎ


결과적으로 이 이슈는 3.5에서는 해결될수 없는 이슈이다. 본인의 경우는 ssl을 통하는 사이트가 있었는데 , 이번에 아이폰 인증 모듈이 들어가면서 이 ssl증서를 사용해야할 일이 생겼는데 , 기존사이트는 이 ssl 을 미러링으로 사용하였기 때문에 상당히 많은 host 주소가 하나의 IIS에 등록이 되어 있었다. 그러한 이유로 , baseaddress를 지정하는 방법으로 해당 이슈를 해결할수 있었다. 그러나 crossdomain 에 민감한 ajax 나 flash 등에서는 여러개의 도메인을 하나의 머신에서 쓰는게 상당히 중요한 이슈이기 때문에 ajax 와 실버라이트에서 가장 유용하게 쓰이는 WCF 는 이 이슈를 해결해야 할 필요가 있었다.

결론적으로? 이 이슈는 WCF 4.0 에서 해결이 되었다 ^^

WCF 4.0 에서는 해당 Attribute 를 지정하면 간단하게 멀티 도메인을 허용할수 있게 된다.
웹기반의 WCF 서비스에서만 동작한다

<serviceHostingEnvironment multipleSiteBindingsEnabled="true" /> 


이 일련의 에피소드는 Microsoft 라는 조직내에서 개발자의 니즈를 처리하는 방식을 보여준다. 물론 그 과정에서 다소간의 오류가 의견충돌이 일어날수도 있지만 , 사용자에 입장에서 얼마나 생각하는지에 따라 사용자가 그 기업을 바라보는 시각이 달라짐을 보여준다고 할수 있다. 비록 WCF 3.5에서 해당 오류를 해결하진 못했지만 , 긴 시간동안 사용자의 불만에 대해 고민하고, 결국 수용하는 Microsoft 의 모습을 나는 칭찬해주고 싶다.

참고자료 : https://connect.microsoft.com/wcf/feedback/details/322896/cant-host-wcf-service-in-a-website-with-multiple-identities?wa=wsignin1.0
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 재미있는 일화네요. 강좌 잘 읽었습니다. :-)

Posted by 오태겸(RuAA)

이제 포스팅 할 때마다 오랜만에 글 쓴다는 말하기도 미안해지네요,, 한 달만에 WCF에 관한 포스팅을 하게 되었습니다. 꾸준히 하겠다는 말을 하기도 민망하지만,, 어찌됐든, 이런 민망함을 뒤로하고 본론으로 들어가 보도록 하겠습니다. 

이번 포스팅의 주제는 Hosting 입니다.
호스팅은 WCF 서비스를 클라이언트에서 사용할 수 있게끔 해주는 중요한 작업이죠,, 이정도는 다들 알고 계실거라 생각합니다.

WCF는 여러 가지의 방법으로 호스팅을 할 수 있는 장점이 있습니다. 이는 제가 처음 WCF를 소개할 때도 언급했던 내용이었구요. 그래서, 지금부터는 WCF 서비스를 호스팅할 수 있는 방법과 호스팅할 때 사용할 수 있는 기능에 대해서 알아볼까 합니다.

호스팅에 대한 내용을 하나의 포스팅에 담기에는 내용이 조금 많은 것 같아서 두, 세번으로 나뉘어 포스팅 하도록 하겠습니다. (이렇게 말해놓고 다음 포스팅이 또 한달을 넘긴다면,, 다들 욕(?)하시겠죠, ^^;;)

지금까지 8번의 포스팅을 진행하면서 제가 예제로 사용했던 WCF 서비스들은 모두 Self Hosting이었습니다.

"응?? Self Hosting 이라뇨? 그건 뭔가요? 먹는건가요?"

라고,, 궁금해 하실 수도 있을 것 같습니다.
Selft Hosting이란,, 특별한 무언가가 있는 것은 아니구요, System.ServiceModel 네임스페이스에 정의 되어있는 ServiceHost 클래스를 사용하여 직접 서비스를 호스팅한 것을 의미합니다.

아시겠죠? 지금까지 제가 사용했던 예제들은 모두 콘솔 어플리케이션 내에서 직접 ServiceHost 클래스를 이용하여 서비스를 호스팅했으니 모두 Self Hosting이 되는 것입니다.
물론, 윈도우 폼 프로그램, WPF 프로그램 그리고, 윈도우 서비스를 이용하여 WCF 서비스를 호스팅할 수 있는데, 이것들 역시 셀프 호스팅(Self Hosting) 인 것입니다.

그럼, 셀프 호스팅을 제외하고 어떤 호스팅 방법이 있을까요?
WCF 서비스를 한번이라도 개발하신 분이라면 아마 사용했을 법한 IIS 호스팅이 있습니다. 그리고 IIS7에서 사용할 수 있는 WAS 호스팅이 있습니다.

IIS 호스팅은 웹 서비스 처럼 IIS를 이용한 호스팅이라 특별한 것도 없고, WCF 서비스를 호스팅하기 위한 가장 쉬운 방법이기도 하구요, 그래서 따로 포스팅을 하지 않아도 될 것 같습니다.

그래서, 이번 포스팅은 WAS 호스팅에 대해서 좀 더 자세히 알아보려 합니다.

WAS(Windows Process Activation Service) 를 이용한 호스팅

WAS는 Windows Process Activation Service의 약자로 IIS 7.0의 기본 구성 요소로서 HTTP 이외의 프로토콜(TCP, MSMQ, Named Pipes)을 사용한 서비스를 호스팅할 수 있게 해주는 역할을 수행합니다.
WAS에 대한 자세한 내용을 알고 싶다면 다음 링크를 참조 하시기 바랍니다. (WAS로 HTTP를 초월한 WCF 서비스 확장)

그럼, 간단하게 WAS가 어떻게 HTTP 이외의 프로토콜을 사용할 수 있게 하는지 알아볼까요?
다음은 WAS 의 아키텍처를 간략하게 표현한 그림입니다.


서버는 서버로 어떤 요청이 들어왔을 때, 그 요청을 처리할 수 있는 수신기를 가지고 있으며, 이 수신기(Listener)는 각 요청의 프로토콜에 맞는 수신기 어댑터(TCP 수신기 어댑터, MSMQ 수신기 어댑터, Named Pipe 수신기 어댑터)로 요청을 보냅니다. 수신기와 WAS 사이에는 수신기 어댑터 인터페이스가 존재하는데, 이를 이용해 각 프로토콜의 수신기 어댑터는 전달받은 요청을 WAS로 보낼 수 있게 되는 거죠. 그리고 WAS 에서는 각 요청에 맞는 응용 프로그램 인스턴스를 생성하기 위해 작업자 프로세스로 요청을 전달하게 되는 것입니다.

이러한 일련의 작업들을 통해 WAS는 HTTP 이외의 프로토콜을 이용한 서비스를 가능하게 하는 것입니다.
설명이 조금 복잡한 것 같지만, 대충 어떻게 돌아가는지는 아시겠죠? ^^;;

그럼, WAS를 이용하여 호스팅을 해보도록 하겠습니다.

WAS는 Vista 이상의 Windows 에서는 IIS를 설치할 때 기본으로 함께 설치가 됩니다. 이 때는 HTTP 수신기 어댑터가 설치 되구요, .NET 3.5 이상의 버전이 설치될 때 TCP, MSMQ, named pipe 수신기 어댑터가 설치 됩니다.

WAS를 이용한 서비스를 만들기 위해 가장 먼저 해야할 것은 IIS를 이용한 호스팅을 하는 WCF 서비스 솔루션을 만드는 것입니다. WAS를 이용한 서비스를 만드는 특별한 방법이 있는 것은 아니며, 기본적으로 WAS는 IIS 7.0의 요소이기 때문에 우선 IIS를 이용한 서비스를 만드는 것이 가장 먼저 해야할 일인 것입니다.

Visual Studio 2010에서 다음 그림처럼 "WCF 서비스 응용 프로그램" 솔루션을 선택합니다.


이렇게 새로운 솔루션을 생성하면, IIS를 이용한 WCF 서비스를 만들 수 있습니다. 기본적으로 Service1.svc 라는 파일이 만들어지는데 이 파일이 WCF 서비스의 로직이 담기는 핵심 파일입니다.(솔루션의 구조는 다음 그림과 같은 모습을 하고 있습니다.)


여기서 서비스의 기능을 바꾸려면 어떤 파일을 수정해야 할까요? 바로 찾을 수 있으시겠죠? IService1.cs 와 Service1.svc.cs 파일을 바꾸어야 합니다. 물론, 이번 포스팅에서의 주제는 WAS를 이용한 호스팅이기 때문에 자동으로 만들어지는 서비스의 로직을 굳이 바꿀 필요는 없을 것 같습니다.

그래~서~!! 저는 따로 서비스의 기능을 바꾸지 않고 자동으로 만들어진 서비스를 그대로 이용해 보겠습니다. 절대 귀찮아서 그러는 것은 아닙니다!! (응? ;;)

솔루션을 만든 후 바로 Ctrl+F5 키를 눌러서 실행을 시켜보도록 하죠~ ㅎ

ASP.NET 솔루션을 실행시킨 것 처럼 ASP.NET Development Server가 시작되고, 웹 브라우저를 이용해 svc 파일을 탐색해 보면 다음과 같이 WCF 서비스 안내 페이지를 만날 수 있을 것입니다.


IIS를 이용한 호스팅은,,. 정말 너무나도 쉽습니다. ServiceHost 클래스를 사용하지도 않고, 자동으로 만들어진 파일 그대로 실행을 해도 무리없이 실행이 되는 것을 알 수 있습니다.

오늘 포스팅의 목적인 WAS를 이용한 호스팅을 구현하기 위해 몇가지 설정을 해보도록 하겠습니다.
바로 이전에 실행했던 서비스는 당연한 이야기겠지만, IIS를 이용한 호스팅이었고, 이는 HTTP 프로토콜을 사용하고 있습니다. 

WAS 호스팅을 실행하기 위해서 우선 서비스 솔루션을 ASP.NET Development Server 가 아닌 IIS에서 실행이 되도록 바꾸어야 합니다. 그래서 솔루션 속성창 "웹" 메뉴에서 "IIS 웹 서버 사용"으로 설정해줍니다. (아래 그림 참조)


IIS 관리자를 실행시키고, 이 사이트가 돌아갈 수 있도록 새로운 사이트를 만들어 주는 것도 잊으시면 안됩니다~ 이 내용은 다들 알고 계시리라 생각하고 생략하겠습니다 ^^

WAS를 이용한 호스팅의 목적은 HTTP 프로토콜 이외의 다른 프로토콜을 사용하기 위함입니다. 그래서 저는 이번 예제에서 TCP 프로토콜을 사용할 수 있는 서비스를 만들어보려 합니다. TCP 프로토콜을 사용할 수 있도록 하기 위해선 IIS 관리자에서 설정을 변경해주어야 할 것이 몇 개 있습니다.

IIS 관리자에서 해당하는 사이트를 선택하고 오른쪽에 위치한 "고급설정"을 클릭하여 설정 창을 띄웁니다. 고급설정 창에서 "사용할 수 있는 프로토콜"에 아래 그림과 같이 "net.tcp"를 추가하여 줍니다.



그리고, 한 가지 더 설정해주어야 할 것이 있는데, 해당 사이트와 특정 프로토콜에 대한 바인딩입니다. 사이트 작업 메뉴에서 "바인딩"을 클릭하여 사이트 바인딩 창을 띄웁니다. 그리고, 추가 버튼을 클릭하여 다음 그림과 같이 net.tcp 프로토콜에 대한 바인딩 정보를 입력하여 줍니다.


여기까지 설정이 완료되면, WAS를 이용하여 호스팅을 하기 위한 IIS 사이트 설정이 모두 끝이 납니다. 어렵지 않죠? ^^

이제, 마지막으로 앞에서 만든 WCF 서비스에 TCP 프로토콜을 사용할 수 있도록 netTcpBinding을 사용하는 엔드포인트를 추가시켜주어야 합니다. WCF 서비스 솔루션에 있는 web.config 파일을 열어보죠.

Visual Studio 2010을 이용해 서비스를 만드셨다면, web.config의 내용이 조금 달라졌다고 느끼실 것 같습니다. 사실, 내용이 많이 달라진 것은 아니고, 그 전 버전에 비해 web.config의 내용이 아주 많이 간략화되었습니다. 매우 매우 심플해졌죠. 이 내용에 대해서 나중에 따로 포스팅을 할 기회가 있을 것이라 생각됩니다.

엔드 포인트를 다음과 같이 포함시켜 보겠습니다.

<system.serviceModel>

    <services>

      <service name="WAS_Hosting.Service1">

        <endpoint address="mex" binding="mexTcpBinding" contract="IMetadataExchange" />

        <endpoint binding="netTcpBinding" contract="WAS_Hosting.IService1" />

      </service>

    </services>
    ...
</system.serviceModel>


service 태그의 name 속성과 endpoint 태그의 contract 속성은 WCF 서비스의 네임스페이스와 클래스 이름을 잘 확인하여 설정해주셔야 합니다.

IMetadataExchange를 컨트랙트로 사용하는 엔드포인트도 같이 설정을 해주는 걸 볼 수 있습니다. 이는 서비스의 메타 데이터에 접근할 수 있는 엔드 포인트란 것을 알 수 있으실 겁니다. 이에 대한 내용은 다음 포스팅을 참조해 주시면 될 것 같습니다. (첫 WCF 서비스 만들기 2)

자~ 이제 모든 설정이 끝났습니다. 여기까지 따라오시느라 수고하셨습니다~ ^^

이 서비스가 제대로 서비스가 되는지 확인해보기 위해 콘솔 프로젝트를 새로 만들고 서비스 참조를 통해 우리가 만들었던 WCF 서비스를 참조해보도록 하겠습니다.

IIS 관리자에서 저는 net.tcp에 대한 바이딩을 할 때 포트번호를 8081로 설정했었습니다. 따라서 그림에서 볼 수 있듯이 "net.tcp://localhost:8081/Service1.svc" 주소로 WCF 서비스에 접근할 수 있습니다.


서비스를 참조한 후에 서비스를 사용하는 방법은 특별한 것이 없습니다. 예전에 셀프 호스팅 서비스를 사용했던 것과 같은 방법으로 사용하면 되기에 클라이언트 쪽 코드는 생략하고, 결과 화면만 첨부하겠습니다.


WCF 서비스가 아주 잘 동작하는 것을 확인할 수 있습니다. 유후~ ^^

이번 포스팅의 내용은 여기까지 입니다.
쓸데없이 내용이 길어진 것 같다는 생각도 들지만, 어쨌든 여기까지 읽어 주셔서 너무 감사드리구요~
다음번 포스팅때 다시 뵙도록 하겠습니다. ^^
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. Hi,
    Thank you for your posting.
    It's very brief and right on to the point.

Posted by 오태겸(RuAA)
안녕하세요~ ^^
오늘 너무 오랜만에 WCF 포스팅을 하게 되었습니다. 자주자주 인사를 드려야 하는데 그렇게 하지 못해 너무 죄송합니다.
오늘 감기 기운 덕분에 약을 먹어놨더니 정신이 헤롱헤롱 하네요 ^^;;
글이 조금 이상하더라도 하해(?)와 같은 마음으로 이해해주시길 바랄께요~

이번 글에서는 serialization(직렬화)에 대해 얘기를 해볼까 합니다.

우선, serialization이 무엇인지 알아야 하겠죠?
컴퓨터 용어에서 serialization은 종종 하나의 오브젝트를 바이트의 배열로 전환(converting)시키는 것을 의미합니다. 하지만 WCF 에서의 serialization은 이러한 의미로 사용되지는 않구요, 하나의 오브젝트(or 닷넷 클래스)를 XML Information Set(XML Infoset) 으로 전환시킨다는 의미로 사용됩니다.

아직은 조금 이해하기가 어렵나요? 음,, 아마 이번 포스팅을 끝까지 읽으면 이해가 되실 겁니다. ^^
이제부터 WCF에서의 Serialization에 대해 조금 더 자세하게 얘기해보겠습니다.

WCF에서는 아래와 같은 네 가지의 serialization을 위한 클래스를 제공합니다.

  • DataContractSerializer
    : WCF 에서 사용하는 기본 serializer 를 제공한다.
  • NetDataContractSerializer
    : 타입에 대한 추가적인 정보를 제공하는 serializer
  • XmlSerializer
    : .NET 2.0에서 제공되었던 serialization을 수행한다.
  • DataContractJsonSerializer
    : serialization 포맷으로 JSON을 제공한다.

이번 포스팅에서는 이러한 클래스들이 직렬화를 했을 때 어떤 결과물을 보여주는지에 대해 써볼까 합니다. 이러한 내용들이 WCF 서비스를 만들때 직접적인 도움이 되진 않겠지만, WCF 서비스를 이해하고, 목적에 맞는 서비스를 만들기 위한 커스터마이징을 하기 위해선 도움이 될 수 있을 것입니다.

DataContractSerializer

DataContractSerializer는 WCF에서 사용하는 기본 serialization을 수행합니다. (이후 부터는 serialization 대신 "직렬화"를 사용하겠습니다.) 이 serializer를 사용하기 위해서는 직렬화가 되는 클래스에 [DataContract] 특성을 지정하여 주면 됩니다. 이 방법은 WCF 서비스를 만들때 많이 사용했던 방법이기에 다들 익숙하리라 생각됩니다.

DataContractSerializer 클래스가 어떤 형태로 직렬화를 수행하는지 알아보도록 하겠습니다. 우선, 다음과 같은 클래스를 생성합니다.

[DataContract]

public class Employee

{
     public Employee(int employeeID, string firstName, string lastName
     {

         this.EmployeeID = employeeID;

         this.FirstName = firstName;

         this.LastName = lastName;

     }

 

    [DataMember]

    public int EmployeeID { get; set; }

 

    [DataMember]

    public string FirstName { get; set; }

 

    [DataMember]

    public string LastName { get; set; }

}


그리고, 콘솔 어플리케이션에서 다음과 같은 코드를 작성합니다.

using System.Runtime.Serialization;

using System.IO;

 

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            Employee e = new Employee(101, "Tae kyeom", "Oh");

            FileStream writer = new FileStream("sample.xml", FileMode.Create);

 

            DataContractSerializer ser = new DataContractSerializer(typeof(Employee));

            ser.WriteObject(writer, e);

            writer.Close();

        }

    }

}


이 코드를 실행을 시켜보면~ 프로젝트의 bin\Debug 폴더안에 sample.xml 파일이 생성됩니다.
코드는 간단하기에 이해하는데 큰 어려움은 없을 것 같지만, 간단히 설명하자면,,, Employee 클래스 인스턴스를 생성하고, 이 오브젝트를 XML 형태로 직렬화를 시킵니다. 그리고 이에 대한 내용을 sample.xml 파일에 썼구요. 그리고, 직렬화 할 때는, DataContractSerializer 클래스를 사용했습니다

그럼, sample.xml 파일의 내용을 확인 해보겠습니다.

네~ 다음과 같은 내용을 보여주고 있네요, 딱 봐도 Employee 인스턴스가 가지고 있는 데이터를 XML로 표현해주고 있다는 것을 알 수 있습니다.

여기서 다시 한번 더 되새겨봐야 할 것은 WCF에서 [DataContract] 특성이 적용된 클래스의 경우 서비스에서 클라이언트로 전달될 때, 위 모습과 같은 형태로 전달된다는 것입니다.


XmlSerializer

XmlSerializer는 이 전 닷넷 버전에서도 지원해주었던 클래스이며, 이는 ASP.NET 웹 서비스에서 사용하던 직렬화 방법을 제공해줍니다.
이러한 직렬화 방법을 WCF 에서도 사용할 수 있으며, 이는 ASP.NET 웹 서비스와 호환이 가능하다는 장점이 있죠. 또한, 이것은 웹 서비스를 WCF로의 전환하는 것이 그리 어려운 일이 아니라는 것을 의미하기도 합니다.^^

XmlSerializer는 public 접근자의 기본 생성자, 필드, 그리고 프로퍼티를 직렬화 시켜줍니다.

Employee 클래스를 다음과 같이 조금 수정해보았습니다.
public 기본 생성자가 필요하기에 추가시켜주었구요, [DataContract]와 [DataMember] 특성을 뺐습니다.

public class Employee

{
    public int EmployeeID { get; set; }

 

    public string FirstName { get; set; }

 

    public string LastName { get; set; }

 

    public Employee()

    {

    }

 

public Employee(int employeeID, string firstName, string lastName)

{
           
this.EmployeeID = employeeID;

            this.FirstName = firstName;

            this.LastName = lastName;

}

}


그리고, 이 클래스를 XmlSerializer 를 이용하여 직렬화 시켜보겠습니다.


using System.IO;

using System.Xml.Serialization;

 

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            Employee e = new Employee(101, "Tae kyeom", "Oh");

            FileStream writer = new FileStream("sample.xml", FileMode.Create);

 

            XmlSerializer ser = new XmlSerializer(typeof(Employee));

            ser.Serialize(writer, e);

            writer.Close();

        }

    }

}


코드는 아주 간단하죠. DataContractSerializer 클래스를 이용했던 코드와 별반 다를건 없구요, 단지 직렬화 할때 XmlSerializer 클래스를 사용했습니다. 그리고 이 클래스엔 Serialize 메소드를 이용해 직렬화를 수행하죠.

sample.xml 파일을 확인해보면 아래와 같은 모습을 하고 있는 것을 알 수 있을 것입니다.


결과로 생성 된 xml 파일 역시 앞 예제에서의 결과 파일과 많이 다르진 않지만, 네임스페이스가 조금 바뀐 것을 확인할 수 있습니다.

아,, 참고로~ WCF에서 XmlSerializer를 사용할 수 있게끔 하기 위해선 서비스 계약(Service Contract)에서 [XmlSerializerFormat] 특성을 적용해주면 됩니다.


DataContractJsonSerializer

DataContractJsonSerializer는 직렬화의 결과로 JSON 형태를 제공합니다.
JSON 형태의 경우 XML 보다 데이터의 양이 적은 장점이 있고, 이 직렬화를 사용하면, 자바 스크립트를 이용하여 서비스를 호출할 수 있어, 웹 어플리케이션에서 서비스를 쉽게 활용할 수 있다는 장점이 있습니다.

WCF 에서는 REST 서비스를 구현하는 경우 이러한 DataContractJsonSerializer를 사용합니다.

앞 예제에서 사용한 Employee 클래스를 DataContractJsonSerializer를 이용하여 직렬화 시켜보도록 하겠습니다.

using System.IO;

using System.Runtime.Serialization.Json;

 

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            Employee e = new Employee(101, "Tae kyeom", "Oh");

            FileStream writer = new FileStream("sample.txt", FileMode.Create);

 

            DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(Employee));

            ser.WriteObject(writer, e);

            writer.Close();

        }

    }

}


앞 예제와는 다르게 직렬화 결과를 xml 파일이 아닌 텍스트 파일로 썼습니다. 그리고 DataContractJsonSerializer 클래스를 사용하여 직렬화를 수행했습니다.

다음은 그 결과입니다.


멋지게 JSON 형태로 그 결과를 보여주고 있네요. ㅎ


오늘의 포스팅은 여기까지 하려 합니다. (몸이 좋지 않다는 핑계로 성급히 마무리 하려는,, ㅡ,.ㅡ;;;)

오늘 포스팅의 내용은 WCF 서비스를 만드는 팁도 아니고, 서비스를 만드는데 직접적인 영향을 미치는 것은 아니었던것 같습니다. 하지만, 이 부분은 앞에서도 언급했지만, WCF 서비스에 대한 이해를 위해서 알아두어야 하는 부분이라 생각하기에 포스팅을 감행(?)했습니다. 비록, 실무적으로 많은 도움이 되진 못하겠지만 이러한 기본적인 지식도 중요한 부분이니깐요~ ^^

다음 포스팅에서는 WCF 에서 제공하는 기본 직렬화 방법이 적절하지 않을때 커스터마이징을 수행하는 방법에 대해 알아보도록 하겠습니다.

감사합니다.
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 질문이 있는데요. ^^

    본문에 보면,

    "
    하지만 WCF 에서의 serialization은 이러한 의미로 사용되지는 않구요, 하나의 오브젝트(or 닷넷 클래스)를 XML Information Set(XML Infoset) 으로 전환시킨다는 의미로 사용됩니다.
    "

    라고 언급이 되어 있는데, ... 그렇다면 DataContractJsonSerializer 의 출력 결과물이 XML Infoset이라고 볼 수 있다는 것인가요?

    • ㅎㅎ날카로운 지적이십니다. 당연히 DataContractJsonSerializer 의 결과물은 XML이 아닙니다. 따라서 XML Infoset이라고 볼 수 없겠죠,, 이 클래스만은 예외라고 보시면 될 듯 합니다. ^^;;

    • 엔돌핀 2010/07/29 13:27

      좋은글 담아갑니다~

  2. wcf는 처음하는데, 개념잡는데 많은 도움이 됩니다.
    앞으로의 유익한 포스팅도 많이 기대하고 있겠습니다.
    감사합니다.

  3. 정말 많은 도움이 됐습니다. 감사합니다!

Posted by 엄준일(땡초)

아마 .NET Framework 4.0 을 출시로 향상된 프레임워크의 API 를 사용하기 위해 .NET Framework 4.0 으로 개발하거나 마이그레이션의 계획을 할 예정이라면 반드시 아래의 문서를 보시기 바랍니다.

 

.NET Framework 4.0 으로 마이그레이션 이슈

.NET Framework 4.0 은 구조적으로 전혀 새로워지고 향상된 프레임워크입니다. 그로 인하여 .NET Framework 4.0 은 기존의 구조 또는 API 들이 호환되지 않는 경우가 있습니다. 어플리케이션 레벨과 코어 레벨에서 변경된 사항들로 인한 이슈와 변경 방법을 참고 하십시오.

.NET Framework 4 Migration Issues

   

.NET Franework 4.0 호환성

특히 .NET Framework 4.0 부터는 기존의 .NET Framework 2.0 부터 .NET Framework 3.5 SP1 까지 사용된 CAS(Code Access Security) 와 관련한 변경 사항으로 .NET Framework 의 전반적인 보안 관련 정책이 변경이 되었습니다.

Code Access Security Policy Compatibility and Migration

그 외에도 .NET Framework 4.0 환경에서 기존의 어플리케이션이나 콤포넌트를 정상적으로 동작시키기 위하여 아래의 문서를 참고하시기 바랍니다.

Version Compatibility in the .NET Framework
.NET Framework 4 Application Compatibility Walkthrough

   

ObsoleteAttribute 특성을 피할 것

또한, 상당히 많은 양의 클래스나 구조체들이 ObsoleteAttribute 특성이 적용되었습니다.

장기적으로 지속 가능한 어플리케이션을 위하여 ObsoleteAttribute 특성이 적용된 API 는 절대 사용하지 않는 것을 권장하며, 아래의 문서를 참고하십시오.

Obsolete in the .NET Framework Version 3.5
.NET Framework V2.0 Obsolete Type/Member List (By Namespace)

Obsolete Types in the .NET Framework 4

   

그래도 문제가 발생한다면…?

혹시 그래로 문제가 발생하시나요? 그럼 Microsoft Connect 사이트에서 당신의 문제를 보고하시기 바랍니다. 또는 netfxcf@microsoft.com 으로 버그 번호와 함께 이메일을 보내시기 바랍니다.

(MSDN 문서에서 이곳으로 오류를 보고하라고 하네요 ^^)

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

Posted by 오태겸(RuAA)
지난 포스팅에 이어서 WCF 서비스의 동시성에 대해 이야기 해보겠습니다.
다른 설명 하지 않고, 지난 포스팅에서 했던 것 처럼 예제를 우선 보고 얘기를 진행해볼까 합니다.

Implementing a Singleton

단 하나의 서비스 인스턴스만이 생성되며, 인스턴스에서 동작하는 스레드 역시, 단 하나만 생성되게 하는 경우에 대해 먼저 살펴보겠습니다.
저번 포스팅에서 사용했던 서비스 클래스의 코드를 다음과 같이 굵은 글씨체 부분만을 수정하여 서비스를 실행해 보시기 바랍니다.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,

            ConcurrencyMode = ConcurrencyMode.Single)]

class ProductService : IProductService

{

    ProductService()

    {

        Console.WriteLine("{0}: !!", DateTime.Now);

    }
          ... 생략 ...
}

이 코드를 실행하면 다음과 같은 결과를 확인할 수 있을 것입니다.

[서버]


[클라이언트]


결과가 여러분이 예상했던 것과 일치하나요?? ㅎ

이 경우 역시 서비스의 인스턴스는 서버 측 결과 화면을 통해 클라이언트의 요청의 수와는 관계없이 하나 만이 생성됨을 알 수 있습니다.
그리고, 클라이언트에서 서비스를 호출하는 방식으로 비동기 방식을 사용했던 것 기억하실겁니다. 결과 그림만을 봐서는 잘 모를 수도 있지만, 이 때문에 클라이언트 측 결과 화면을 보면, 지연시간 없이(각 호출에 대한 결과를 받지 않아도) 서비스를 연속적으로 세 번 호출함을 볼 수 있습니다.
하지만, 이 호출에 대한 결과는 각각 5초간의 지연시간을 두고 화면에 출력하는데, 이는 서버측 결과 화면을 보면 그 이유를 알 수 있습니다. 만약, 서비스 인스턴스가 여러 개의 스레드를 만들어 요청을 처리했다면, 클라이언트의 각 요청에 대한 결과를 거의 동시에 받을 수 있겠지만, 이 경우에는 하나의 스레드 만이 동작하기 때문에 각 요청을 한번에 하나씩 처리할 수 있어 이러한 결과를 얻을 수 있는 것이죠. 참고로, 각 요청에 대한 처리는 FIFO(First In First Out)의 순서로 동작합니다.

서비스 인스턴스에서 단 하나의 스레드 만이 동작한다는 것은 서버 측 결과에서 Thread ID 값이 3으로 동일한 것을 봐도 증명이 가능합니다. 가끔, 이 thread id의 값이 각 요청마다 다른 값이 나올 수도 있습니다. 그렇다고 잘못된 결과값은 아닙니다. ConcurrencyMode.Single한번에 하나의 스레드만이 동작한다는 것을 명시하는 거지, 단 하나의 스레드만이 만들어진다라는 의미는 아니거든요~,, 약간 헷갈릴 수도 있는 부분인 것 같으니, 꼭 명심해주세요,, ㅎ

이 경우처럼 싱글 인스턴스, 싱글 스레드는 한번에 하나의 클라이언트 요청만을 처리할 수 있기 때문에 throughput을 감소시킨다는 단점이 있지만, 반면에 시스템 자원(resource)에 동시 접근 같은 문제가 일어나지 않아서, 이에 대한 추가적인 관리가 필요하지 않다는 장점도 있습니다.

Session-Level Instances

이번에는 세션 모드가 적용되었을 때, 서비스의 인스턴스가 어떻게 생성되는지 한번 살펴보도록 하겠습니다.

우선, 서비스에서 세션을 지원하도록 하기 위해 서비스 계약의 특성값을 다음과 같이 수정합니다.

[ServiceContract(Namespace = "http://RuAAService.co.kr/",

                     SessionMode = SessionMode.Required)]

interface IProductService

{

    [OperationContract]

    Product GetProduct();

}


SessionMode에 Required 값을 적용하여 이 서비스가 세션 모드를 지원해준다는 것을 명시해 줍니다.
다음은 ServiceBehavior 특성에서 InstanceContextMode의 값을 PerSession 으로, ConcurrencyMode의 값을 Multiple 로 수정을 해줍니다. 그리고 GetProduct 메서드가 한 인스턴스에서 몇 번 호출이 이루어지는지를 체크하기 위해 n_Calls 라는 이름의 필드를 추가해 주었습니다. lockThis 필드는 n_Calls 필드의 값을 증가시킬 때 다른 스레드에서 동시에 n_Calls의 값을 바꾸지 못하도록 하기 위한 목적으로 선언해주었습니다.


[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,

            ConcurrencyMode = ConcurrencyMode.Multiple)]

class ProductService : IProductService

{

    object lockThis = new object();

    private int n_Calls = 0;

       
       ... 중간 생략 ...

   
    public
Product GetProduct()

    {

        Console.WriteLine("{0} : GetProduct , Thread Id {1}", 
                            DateTime
.Now, Thread.CurrentThread.ManagedThreadId);

        Thread.Sleep(1000);

 

        Product p = new Product();

        p.ProductId = 1234;

        p.ProductName = "ABC Chocolate";

        p.Price = 1500.0;

        p.Company = "Lotteee";

        p.CreateDate = DateTime.Parse("2010-01-22");

 

        lock (lockThis)

        {

            p.calls = ++n_Calls;

        }

 

        return p;

    }

}


코드를 다 수정하셨으면, 결과를 확인해보도록 하겠습니다.

당연히, 서버 측 콘솔 어플리케이션을 실행 시키신 후에, 클라이언트 어플리케이션을 실행시켜야겠죠,,^^
아,, 이런~ 저와 같은 방법으로 수정을 한 후에 실행 시키면, 아마 예외가 발생하실겁니다.

예외의 이유는 간단합니다. Service Contract에서 세션 모드의 값을 Required로 설정을 해놓았는데, 실제 서비스를 호스팅할 때 세션을 지원하지 않는 BasicHttpBinding을 사용했기 때문입니다. 따라서, BasicHttpBinding을 WSHttpBinding 으로 수정해주시면 이 예외는 발생하지 않을 것입니다.

자~ 이 부분 수정을 다 하셨다면, 다시 실행을 해보도록 하겠습니다.

세션모드의 지원을 확인하기 위해서 클라이언트 어플리케이션을 두 개 연속해서 실행을 시킵니다.

다음은 서버 어플리케이션의 실행 화면입니다.


인스턴스가 두 개 생성된 것을 확인할 수 있네요,, 왜 인스턴스가 두 개 생성되었을까요?? 당연히 클라이언트 어플리케이션이 두 개 실행이 되었기 때문입니다. 세션을 지원하는 서비스이니깐요,,

자~ 다음은 두 클라이언트 어플리케이션의 실행화면입니다.





각 클라이언트는 호출한 횟수가 1~3인 것을 확인할 수 있습니다. 이것은 각 클라이언트 마다의 서비스 인스턴스가 따로 데이터를 유지한다는 것을 의미하는 것이죠. 이해 되셨죠?? ^^

이번 포스팅은 이것으로 마무리 하려 합니다.
이번에도 포스팅이 조금 늦었습니다. 바로 하려고 했는데 이게 마음처럼 쉽지가 않네요. ^^;;

어찌됐든, 다음 포스팅때 뵙도록 하겠습니다. ㅎ
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 좋은 글 감사합니다.

Posted by 오태겸(RuAA)

봄이 오고 있네요,, 
날씨가 많이 따뜻해졌고, 해도 부쩍 길어졌음을 느낍니다.
최대한 빠른 시일 내에 포스팅을 하려 했는데, 그동안 무기력증(?)에 빠져있다보니,,
하는거 없이 시간만 보내버렸네요,, ^^;;
봄이 찾아온 만큼 새로운 마음가짐으로 다시 시작해보겠습니다. 아자~!

이번 포스팅의 주제는 WCF의 Behaviors 중에서 서비스의 동시성(Concurrency)을 컨트롤 할 수 있는 Behavior 입니다.

Behavior는 서비스가 동작할 때(그러니깐 런타임 시) 동작에 영향을 끼치는 클래스들로, 서비스 클래스의 특성으로 지정하거나, 환경 설정파일을 통해 지정할 수 있습니다.

Behavior와 관련된 여러 가지 내용 중 이번 포스팅에선 동시성(Concurrency)에 대해서 얘기해보도록 하겠습니다.

동시성이라 함은, 여러 task 들이 동시에 동작하는 것을 말합니다.
동시성은 다들 아시겠지만, 그리고 아주 당연하게도 시스템의 throughput(출력률)에 큰 영향을 끼칩니다. 일정 시간동안 처리할 수 있는 작업의 양이 커지기 때문이죠.

WCF 에서는 동시성을 컨트롤할 수 있는 두 종류의 behavior 가 있습니다. 바로 “InstanceContextMode”“ConcurrencyMode” 입니다.

InstanceContextMode는 생성되는 서비스의 인스턴스를 조절할 수 있는 behavior로 다음과 같은 세 종류의 값으로 설정할 수 있습니다.

  • Single : 이 값은 서비스로 들어오는 모든 요청을 하나의 인스턴스에서 처리하도록 설정합니다.
  • PerCall : 서비스로 들어오는 요청마다 서비스의 인스턴스가 만들어지도록 하기 위한 설정입니다.
  • PerSession : 클라이언트 세션마다의 서비스 인스턴스를 생성하기 위한 설정이며, 만약 세션을 사용하지 않는 채널일 때, 이 값으로 설정이 된다면, PerCall과 같은 방식으로 동작합니다.

그리고, InstanceContextMode의 기본값은 PerSession으로 따로 어떠한 값도 설정되어 있지 않은 경우엔 세션 수에 따라 서비스 인스턴스가 생성됩니다.

WCF에서 동시성을 조절할 수 있는 또 다른 모드인 ConcurrencyMode는 하나의 서비스 인스턴스 내에서 동작하는 스레드를 통한 동시성을 컨트롤하는 behavior입니다.
다음은 ConcurrencyMode에서 설정할 수 있는 값에 대한 설명입니다.

  • Single : 하나의 서비스 인스턴스 내에 오로지 하나의 스레드만이 동작하도록 설정하는 값입니다. 따라서, 이 값으로 설정되어 있는 경우엔 스레딩 문제를 고려하지 않아도 된다는 장점이 있습니다.
  • Reentrant : 이 설정 역시 하나의 서비스 인스턴스에서 하나의 스레드만이 동작하도록 하는 설정값입니다. 하지만, 이 설정값이 Single과 다른 점은 하나의 스레드가 동작하는 도중에 다른 작업이 처리될 수 있다는 것입니다. 이 작업의 처리가 완료되면 이 전의 작업이 계속해서 동작됩니다.
  • Multiple : 하나의 서비스 인스턴스에서 하나 이상의 스레드가 동작할 수 있도록 하는 설정입니다. 이 값으로 설정되어 있는 경우엔 여러 개의 스레드에서 서비스 개체를 변경할 수 있기 때문에 항상 동기화와 상태 일관성을 처리해 주어야 합니다.

ConcurrencyMode와 InstanceContextMode의 값을 적절하게 조합하면, 서비스의 기능에 맞게 동시성과 인스턴스 관리를 할 수 있습니다. 지금 이러한 내용을 글로 써내려가봤자 설명하기도 힘들고, 받아들이기도 힘이 들겁니다. 따라서 이러한 내용은 역시 실제 코드를 작성하고, 결과를 보면서 이해하는게 가장 쉽고 빠른 방법이겠죠 ^^

네~ 이제 InstanceContextMode와 ConcurrencyMode의 값을 적절하게 조합하여 서비스에 적용하는 실습을 해보도록 하겠습니다.

우선, 가장 먼저 세션을 사용하지 않는 환경에서 InstaceContextMode와 ConcurrencyMode의 기본값을 사용한 서비스를 구현해보겠습니다. InstanceContextMode의 기본값은 PerSession 이며, ConcurrencyMode의 기본값은 Single 입니다. 이 기본값은 따로 설정해주지 않아도 적용된다는거 아시죠? ㅎ

다음은 서비스를 구현한 클래스의 코드 입니다.

class ProductService : IProductService

{

    ProductService()

    {

        Console.WriteLine("{0}: 서비스의 새로운 인스턴스 생성!!", DateTime.Now);

    }

 

    public Product GetProduct()

    {

        Console.WriteLine("{0} : GetProduct 호출, Thread Id {1}", DateTime.Now,
Thread.CurrentThread.ManagedThreadId);

        Thread.Sleep(5000);

 

        Product p = new Product();

        p.ProductId = 1234;

        p.ProductName = "ABC Chocolate";

        p.Price = 1500.0;

        p.Company = "Lotteee";

        p.CreateDate = DateTime.Parse("2010-01-22");

 

        return p;

    }

}


저번 포스팅에서 사용했던 서비스의 코드를 살짝 수정 해보았습니다.
서비스 클래스 생성자를 만들어 단순하게 인스턴스가 생성되었다는 메시지를 출력해주는 코드를 추가하였구요, GetProduct 메서드 내에서는 현재 스레드의 ID 값을 출력해주는 코드를 추가하였습니다.

다음은 이 서비스를 호출하는 클라이언트 코드입니다. 코드를 보시면 아시겠지만 클라이언트에서 서비스 메서드를 비동기로 호출하고 있습니다. 혹시 WCF 서비스를 비동기로 호출하는 클라이언트를 만들어보시지 않은 분이 계시면 제가 예전에 올렸던 포스팅을 참고해주시기 바랍니다. (http://ruaa.tistory.com/entry/async-call) 자세한 설명은 없지만 대충은 이해하실 수 있으실겁니다 ^^;;

namespace MySvcAsyncClient

{

    class Program

    {

        static int c = 0;

        static void Main(string[] args)

        {

            ProductServiceClient proxy = new ProductServiceClient();

            for (int i = 0; i < 3; i++)

            {

                Console.WriteLine("{0}: GetProduct 메서드 호출", DateTime.Now);

                proxy.BeginGetProduct(GetProductInfoCallback, proxy);

                Thread.Sleep(100);

                Interlocked.Increment(ref c);

            }

            while (c > 0)

            {

                Thread.Sleep(100);

            }

        }

 

        static void GetProductInfoCallback(IAsyncResult ar)

        {

            ProductInfo productInfo = ((ProductServiceClient)ar.AsyncState)
                                           .EndGetProduct(ar);

            Console.WriteLine("{0} : ProductName : {1}",
                                        
DateTime.Now, productInfo.Name);

            Interlocked.Decrement(ref c);

        }

    }

}


Main 메소드 내에서는 for 문을 사용하여 3번 반복하여 GetProduct 메소드를 비동기로 호출하고 있으며, 각각의 비동기 호출에 의한 작업이 끝이 나면 AsyncCallback 대리자인 GetProductInfoCallback 메소드가 호출되며, 서비스에서 받은 Product 데이터를 화면에 출력해줍니다.

이렇게 코드를 작성하고 나면, 역시 결과가 궁금해 질겁니다. 다음은 이 코드에 대한 결과 화면입니다.

[서버]


[클라이언트]


클라이언트 측 결과 화면을 보면 동시에 서비스의 메소드를 세번 호출하는 것을 확인할 수 있습니다. 그리고 6초 정도의 시간 후에 차례대로 결과값을 가져와서 출력하는 것을 볼 수 있습니다.

서비스 측 결과 화면을 확인해 보면, 각 호출마다 생성자를 통해 새로운 인스턴스를 생성하고, 인스턴스 내에 하나의 스레드를 통해 GetProduct 메소드를 호출하는 것을 확인할 수 있습니다.

여기서 잠깐 의문이 들지도 모르겠습니다. 제가 분명, InstanceContextMode의 기본값은 PerSession 이라고 했는데 왜 서버에선 클라이언트의 호출마다 새로운 인스턴스를 생성한 것일까요?

답은 아주 간단합니다. 서비스를 호스팅할 때 사용했던 binding의 종류가 BasicHttpBinding 이었던 것 기억하시나요? BasicHttpBinding의 경우엔 세션을 사용하지 않기 때문에, 이 경우엔 실제로 InstanceContextMode.PerCall 과 같은 형식으로 동작하게 되는 것입니다.

기본값으로 설정한 경우를 알아봤으니, 이번엔 두 모드의 값을 바꿔서 서비스에 적용해보겠습니다.

인스턴스는 모든 호출에 대해 하나만 생성하도록하고, 스레드의 갯수는 하나 이상으로 만들 수 있게끔 설정한 후에 결과값을 살펴보죠~

앞에서 한번 언급했지만 서비스의 Behavior를 적용하는 방법은 서비스 클래스에 특성으로 설정하는 방법과 config 파일에 설정하는 방법이 있습니다. 여기서는 클래스에 특성으로 설정하는 방법을 사용해보겠습니다.

서비스 클래스의 코드를 다음과 같이 굵은 글씨로 적용된 부분만을 추가해보죠~

class ProductService : IProductService

{
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,

            ConcurrencyMode=ConcurrencyMode.Multiple)]

    ProductService()

    {

        Console.WriteLine("{0}: 서비스의 새로운 인스턴스 생성!!", DateTime.Now);

    }

 

    ... 생략 ...

}


이렇게만 수정한 후에 솔루션을 실행시켜 보면, 클라이언트 측 화면은 변화가 없지만 서버 측 결과 화면은 다음과 같이 변화된 것을 확인하실 수 있으실겁니다.



달라진 점이 무엇인지 보이시죠? ^^

네,, 맞습니다. 인스턴스가 하나만 생성되었다는 점이죠. 아~ 그러고보니 동작한 스레드의 ID 값들이 모두 다른 것도 보이네요. 이 말은 곧, 하나의 인스턴스에 여러 개의 스레드가 생성되었다는 것을 의미하는 것이겠죠. 앞에서 설정했던 InstanceContextMode의 값과 ConcurrencyMode의 값이 어떻게 서비스의 동시성에 적용되었는지 이해가 가실겁니다.

이 외에도 서비스의 동시성에 적용할 수 있는 두 모드의 조합이 더 있지만, 다음 포스팅에서 더 다루도록 하겠습니다. 글도 길어졌고, 아직 담아야 할 내용도 많으니깐요.
이번에는 정말 다음 포스팅때 까지 많이 걸리지 않을 것입니다. 약속드릴께요~ ^^;;

제 포스팅에 항상 댓글 남겨주시고 응원해주시는 분들께 감사 드리며, 또 너무 오랜만에 글을 남겨 죄송한 마음도 듭니다. 제가 잠깐 주춤하긴 했지만, 앞으로는 계속 꾸준한 모습 보여드리려 노력하겠습니다. ^^

감사합니다.
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 붉은이리 2010/05/03 15:04

    항상 좋은 내용 감사합니다...

    많이 도움이 됩니다...ㅎㅎ

  2. coolpixer 2010/06/22 11:22

    글 잘 보고 있습니다.
    다만 위의 내용 가운데

    "우선, 가장 먼저 세션을 사용하지 않는 환경에서 InstaceContextMode와 ConcurrencyMode의 기본값을 사용한 서비스를 구현해보겠습니다. InstanceContextMode의 기본값은 Single 이며, ConcurrencyMode의 기본값은 PerSession 입니다. 이 기본값은 따로 설정해주지 않아도 적용된다는거 아시죠? ㅎ"

    이 부분에서 InstanceContextMode와 ConcurrencyMode의 설정값이 뒤바뀐 것으로 보입니다.

Posted by 52
2. GetHashCode

객체 값에 대한 HashCode를 반환해주는 Method입니다. 근데 이 Method가 반환해 주는 Hashcode는 그닥 쓸모가 없다고 합니다. 각 객체마다 유일한 Hashcode를 보장해 주어야 하지만 그렇지 못하기 때문입니다.

암튼 디자이너는 어떠한 객체라도 HashTable에 담길 수 있다면 여러모로 편리할 것이라고 판단해서 모든 Object를 상속하는 객체는 이 Method를 통해 Int32형태의 hashcode를 얻을 수 있도록 설계했다고 합니다.

하지만 결국 이 Method로 유일한 hashcode를 제대로 반환해 줄 수 있도록 사용하기 위해서는 override를 통해 재작성하여 사용해야 합니다. 그래서 CLR via C# 에서는 Object의 Method로 정의되어 있을 것이 아니라 interface로 정의해 놓는게 맞는게 아닌가 라고 말합니다.

그런데 이 GetHashCode를 재정의해서 사용하기 위해서는 기억해야 할 점이 있습니다. 지난 번에 공부했던 Object의 Method인 Equals()를 재정의 한다면 이 GetHashCode()도 재정의를 해 주어야 한다는 점입니다. 만약 Equals()만 재정의 한다면 컴파일시 warning을 발생합니다.

------ Build started: Project: Test04, Configuration: Debug x86 ------
C:\Documents and Settings\XPMUser\my documents\visual studio 2010\Projects\TestSolution\Test04\Program.cs(8,11): warning CS0659: 'Test04.Animal' overrides Object.Equals(object o) but does not override Object.GetHashCode()

Animal이라는 Class에서 Equals()만 재정의 했더니 위와 같은 경고 메시지를 발생하는 것을 볼 수 있습니다. 이유는 두 객체가 같다면 (Equals()의 값이 true라면) 두 객체의 hashcode도 같을 것이라는 가정을 System.Collections.HashTable 타입과 System.Collections.Generic.Dictionary 타입이 하고 있기 때문입니다.
결국 객체의 동질성을 판단하는 Equals()의 알고리즘에 의해 두 객체가 같다고 판명이 난다면 GetHashCode()를 구하는 알고리즘도 Equals()을 판단하는 알고리즘과 연관되어 같은 값을 반환할 수 있도록 구현해 주어야 합니다.



GetHashCode()는 객체가 속한 AppDomain수준에서 유일할 수 있는 ID를 숫자로 반환하게 되어 있으며 이 값은 객체의 일생동안 바뀌지 않는 것이 보장되지만 이 객체가 가비지 수집기에 의해 수집되면 수집된 객체 hashcode를 의미하는 ID가 다른 새로운 객체에 재활용될 수 있다고 하네요. 그래서 유일한 값을 보장 못한다고 말하는데 그렇다면 AppDomain 수준에서 객체의 유일한 값을 보장해주는 HashCode를 반환해주는 Method가 CLR에 없을까요?

아니 있습니다. System.Runtime.ComplierServices 네임 스페이스에 있는 RuntimeHelpers 클래스의 정적 Method인 GetHashCode 메서드를 제공하고 있습니다. 객체가 Object의 GetHashCode 메서드를 재정의했다고 하더라도 해당 Object를 RuntimeHelpers.GetHashCode() 메소드의 인자로 제공하면 유일한 값을 보장받을 수 있는 ID를 제공해 줍니다.

아 그럼 Object의 GetHashCode()를 쓸게 아니라 저 RuntimeHelpers.GetHashCode()를 쓰면 되겠구나.

하면 되겠죠.


그런데 이 GethashCode()가 VS2010에 포함되어 있는 4.0X 버전의 mscorlib.dll 에서 구현하고 있는 것과 이전 버전에서 구현하고 있는 것이 좀 틀립니다.

우선 기존의 CLR의 mscorlib.dll에서 제공하고 있는 GetHashCode()의 내부를 살펴보도록 하겠습니다.


내부에서 Object.InternalGetHashCode()라는 Method를 호출하고 있네요. 저 함수를 들어가 보면


위와 같은 코드를 볼 수 있습니다. 함수 선언을 보면 internalcall이라는게 붙어 있는데 이것은 unmanaged코드가 불리는 것이라고 생각하면 됩니다. CLR의 관리되는 코드가 아니라 nativecode의 영역에서 구현되어 있다는 말이지요.

그래서 실제 GetHashCode는 다음과 같이 구현이 되어 있다고 합니다.

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {
    
    CONTRACTL
    {
        THROWS;
        DISABLED(GC_NOTRIGGER);
        INJECT_FAULT(FCThrow(kOutOfMemoryException););
        MODE_COOPERATIVE;
        SO_TOLERANT;
    }
    CONTRACTL_END;

    VALIDATEOBJECTREF(obj);
    
    DWORD idx = 0;
    
    if (obj == 0)
        return 0;
    
    OBJECTREF objRef(obj);

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);

        
    idx = GetHashCodeEx(OBJECTREFToObject(objRef));

    
    HELPER_METHOD_FRAME_END();

    return idx;
}
FCIMPLEND

INT32 ObjectNative::GetHashCodeEx(Object *objRef)
{
    CONTRACTL
    {
        MODE_COOPERATIVE;
        THROWS;
        GC_NOTRIGGER;
        SO_TOLERANT;
    }
    CONTRACTL_END

    VALIDATEOBJECTREF(objRef);

    DWORD iter = 0;
    while (true)
    {
        DWORD bits = objRef->GetHeader()->GetBits();

        if (bits & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX)
        {
            if (bits & BIT_SBLK_IS_HASHCODE)
            {
                return  bits & MASK_HASHCODE;
            }
            else
            {
                SyncBlock *psb = objRef->GetSyncBlock();
                DWORD hashCode = psb->GetHashCode();
                if (hashCode != 0)
                    return  hashCode;

                hashCode = Object::ComputeHashCode();

                return psb->SetHashCode(hashCode);
            }
        }
        else
        {
            if ((bits & (SBLK_MASK_LOCK_THREADID | 
(SBLK_MASK_APPDOMAININDEX << SBLK_APPDOMAIN_SHIFT))) != 0)
            {
                objRef->GetSyncBlock();
            }
            else
            {
                if (bits & BIT_SBLK_SPIN_LOCK)
                {
                    iter++;
                    if ((iter % 1024) != 0 && g_SystemInfo.dwNumberOfProcessors > 1)
                    {
                        YieldProcessor();
                    }
                    else
                    {
                        __SwitchToThread(0);
                    }
                    continue;
                }

                DWORD hashCode = Object::ComputeHashCode();

                DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | 
BIT_SBLK_IS_HASHCODE | hashCode;

                if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
                    return hashCode;
            }
        }
    }
}


Hashcode 소스이지만 이 소스만 읽어서는 어떤 알고리즘으로 hashcode를 생성하는 건지 정확히 파악하기가 어렵군요. :)

암튼 이런식으로 GetHashCode()가 구현되어 있었습니다.

그런데 4.0 버전에서의 구현은 다음과 같습니다.



어라 어디서 많이 보던 함수를 호출 하고 있군요. 아까 앞에서 살펴본 AppDomain상에서 객체마다 유일한 ID를 제공하는 것을 보장해준다고 하는 System.Runtime.CompilerServices.RuntimeHelpers::GetHashCode(object)를 호출하고 있습니다!


System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode()는 역시 unmanaged code를 invoke하게 되어 있는 것 같습니다. 내부 구현 코드를 찾아볼 수 있으면 좋을텐데 찾기가 힘드네요;

결국 내부 코드 구현으로 봤을 때 .NET 4.0기반의 CLR에서는 Object.GethashCode()를 사용하는 것과 System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode()를 사용하는 건 동일합니다.



GetHashCode()를 재정의 할 때 고려해야 할 점을 생각해 볼까요.

1. 무엇보다 잘 분포된 난수를 생성할 수 있는 그러니까 같은 Application 환경에서 유일한 값을 보장할 수 있는 알고리즘을 사용해야 합니다.
2. 정의하는 알고리즘 상에서 상위 클래스의 GetHashCode() 를 호출해서 자신의 Hashcode에 값을 반영하는 경우가 있겠지만 기본적으로 제공하는 Object의 GetHashCode()는 가능하면 사용하지 말아야 합니다. 빠른 성능을 고려해서 만든 알고리즘이 아니기 때문입니다.
3. 알고리즘은 적어도 하나의 인스턴스 필드는 이용해야 합니다. 미리 정의되어 있는 정적 값만을 이용하면 안된다는 말이겠지요.
4. 사용되는 필드는 불변의 속성을 가져야 합니다. 즉 필드값은 한번 생성되면 그 객체가 소멸될 때까지는 절대 값이 바뀌어서는 안됩니다.
5. 객체가 같은 값을 지니고 있다면 HashCode도 같은 값을 반환해야 합니다.

그리고 이 GetHashCode()를 사용할 떄의 주의점도 있습니다. 절대로 이 메소드를 통해 연산한 결과를 저장하여 사용하지 말아야 한다는 점인데 예를 들어 설명하자면 어떤 사이트에서 회원 정보를 받을 때 암호를 GetHashCode()를 이용해서 만들어진 hashcode를 DB에 저장해선 안된다는 것입니다.

지금까지 알아본것 처럼 CLR이 업데이트 되면서 내부 구현이 바뀌게 된다거나, 사용자가 직접 재정의한 GetHashCode()의 알고리즘이 변경된다면 저 DB에 저장된 hashcode는 모두 쓰레기가 되어 버리게 될 테니까요.



크리에이티브 커먼즈 라이선스
Creative Commons License

'.NET Framework 4.0 > CLR' 카테고리의 다른 글

8. System.Object (2)  (0) 2010/03/03
7. System.Object  (0) 2010/02/16
6. Assembly - GAC(Global Assembly Cache)  (2) 2010/02/02
5. Assembly - Strongly named assemblies  (0) 2010/01/26
4. Assembly  (0) 2010/01/15
3. MSCorLib & Metadata  (2) 2010/01/06
TAG CLR

댓글을 달아 주세요

Posted by 엄준일(땡초)

.NET Framework 4.0 에 포함될 MEF(Managed Extensibility Framework) 은 컴포넌트를 조합하는 방식으로 컴포넌트의 재사용성과 보다 컴포넌트를 동적으로 사용할 수 있는 프레임워크입니다.

하 지만 MEF 는 기존에 제네릭 타입(Generic Types) 을 지원하지 않습니다. 이미 C# 2.0 부터 지원하는 Generic Type 을 MEF 에서 지원하지 않는 것도 참 아이러니 합니다. 여기에 대한 내용은 아래의 링크를 참고하십시오.

[.NET/.NET Framework] - MEF 에 Generic Type 을 지원하기 위해서..?
[.NET/.NET Framework] - MEF 는 Generic Type 을 지원하지 않는다!

MEFGeneric 이란?

안타깝게도, MEF 에 Generic Type 을 지원하기 위해 적당한 대안이 아직 전세계적으로도 없다는 것입니다. Microsoft 의 MEF 개발 팀 리더도 MEF V1.0 버전에는 지원하지 못할거라고 합니다. 

http://codebetter.com/blogs/glenn.block/archive/2009/03/21/why-doesn-t-mef-support-open-generics-for-exports-because-mef-is-not-type-based.aspx
http://codebetter.com/blogs/glenn.block/archive/2009/08/20/open-generic-support-in-mef.aspx

MEFGeneric 은 전세계적으로 처음으로 정식으로 Generic Type 을 지원합니다. 세계 최초로 한국에서, 그리고 닷넷엑스퍼트에서, 그리고 나로부터 문제를 해결하니, 로또 맞은 기분이네요.

MEFGeneric 은 기존의 MEF 를 Core(코어) 소스 코드를 수정/확장하여 Generic Type 을 지원하도록 하였습니다. 필자는 이 소스 코드를 codeplex 사이트를 통해 공개하였으니, 아래의 사이트를 참고하세요. ^^

   

MEFGeneric is a framework to support CLR Generic types in MEF (Managed Extensibility Framework).

http://mefgeneric.codeplex.com

 

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 워너비 2010/03/02 11:51

    도망나간 정신을 잡고 오랜만에 들렀더니 이런 멋진 걸 만드셨군요!

Posted by 52
오늘의 주제는 이겁니다.

CLR의 모든 타입은 System.Object로부터 파생된다.

자. 오늘의 공부 끝!


이라고 말하고 싶지만 그러면 안되겠죠;;

그럼 모든 타입의 기본인 System.Object에 대해서 좀 더 공부를 해 봐야겠습니다. System.Object는 Class입니다. Class니까 Method과 Property 들로 구성이 되어 있겠지요.

Object의 Public Method들에 대해서 알아보기로 하죠. IDE 편집창에서 Object를 하나 선언하고 Public Method들이 무엇이 있는지 살펴봅시다.



네 저렇게 4개의 Method가 존재합니다. 이 Method들은 Object의 public Method인 만큼 CLR의 모든 타입들은 저 4개의 Method를 기본적으로 갖고 있게 됩니다. 저 Method들을 알아보기 위해 우선 좀 말이 안되긴 하지만 다음과 같은 클래스들을 만들어 놓습니다.

    class Animal
    {
        Int32 Age;
        string Name;

        public Animal(string name, Int32 age)
        {
            Age = age;
            Name = name;
        }

        public Int32 GetAge()
        {
            return Age;
        }

        public string GetName()
        {
            return Name;
        }

        public virtual string Say()
        {
            return "Can't say";
        }

        public static Int32 GetLegs(Animal animal)
        {
            Int32 legs = 2;

            if (animal.GetType().ToString().Equals("Test04.Animal"))
                legs = 4;

            return legs;

        }
    }

    class Human : Animal
    {
        public Human(string name, Int32 age) : base(name, age)
        {
        }
        public override string Say()
        {
            return "Oh! Yeah!";
        }
    }


Animal 클래스가 있고 Animal에서 상속받은 Human 클래스가 있습니다. Animal의 class 선언에는 생략되어 있지만-

class Animal 은 class Animal : System.Object 와 같이 쓸 수 있습니다. 둘은 같은 의미입니다.


1. Equals

비교 하려는 객체가 같은 값을 가지고 있으면 true 아니면 false를 return 합니다. 같은 값을 가지고 있다는 말은 무슨 의미일까요? 테스트 해 봅시다. 다음과 같은 코드를 작성해 봅니다.

            Animal dog = new Animal("happy", 3);
Human korean = new Human("Park", 33);

            if (dog.Equals(korean))
                Console.WriteLine("dog == korean");
            else
                Console.WriteLine("dog != korean");

dog라는 Animal class의 객체를 선언하고 korean이라는 Human의 객체를 선언하고 이 둘을 비교합니다. 뭐가 찍힐까요? 당연히 dog != korean 이 찍힙니다.

            Animal cat = new Animal("nero", 1);

            if (dog.Equals(cat))
                Console.WriteLine("dog == cat");
            else
                Console.WriteLine("dog != cat");

자 이번엔 어떨까요? 같은 Animal 객체인 cat을 선언해 봅니다. 둘은 같다고 인정될까요? 아닙니다. 역시 dog != cat이 찍힙니다.

            Animal dog2 = new Animal("happy", 3);
            if (dog.Equals(dog2))
                Console.WriteLine("dog == dog2");
            else
                Console.WriteLine("dog != dog2");

아.. 그러면 이번엔 객체 이름은 다르지만 내부의 property값은 같은 dog2라는 객체를 선언하고 둘을 비교해 봅시다. 같다고 인정해 줄까요? 아.. 역시 다르다고 합니다. dog != dog2 라고 찍힙니다.

도대체 그럼 뭐를 어떻게 해야 같은거야? 라고 생각하면서 마지막으로 다음과 같이 테스트 해 봅시다.

            Animal dog3 = dog;

            if (dog.Equals(dog3))
                Console.WriteLine("dog == dog3");
            else
                Console.WriteLine("dog != dog3");

dog 객체를 새로 생성한 dog3에 할당합니다. 여기선 어떤 값이 찍힐까요? 아. 드디어 두 객체가 같다고 인정해 줍니다. dog == dog3 가 찍힙니다!

Object.Equals()의 같다는 의미는 좀 어렵네요. 두 객체가 포함하는 property등의 값이 같다고 equal이 아닙니다. 두 객체가 참조하는 것이(주소값 이라고 해야 할까요? C++적인 의미로 말입니다.) 이 완전히 일치해야지 equal입니다. 

음 개념적으로는 대충 이해가 되는데 좀 더 자세히 알아보고 싶어지는군요.  앞으로 자주 보게 될 ILDASM을 꺼내봐야겠습니다. ILDASM으로 지금까지 작성한 코드들이 어떻게 CLR에 들어가는지 살펴봅시다.




ILDASM을 이용하여 작성한 코드들을 MSIL코드로 볼 수 있습니다. 하나하나씩 따라가보면서 간단하게나마 분석을 해보려고 합니다. 일단 Main 앞 부분입니다. main()함수에서 사용하는 지역변수들을 초기화 하고 알아보기 쉽게 index를 붙여서 태깅해 놓고 시작하네요.

            Animal dog = new Animal("happy", 3);
            Human korean = new Human("Park", 33);

  IL_0001:  ldstr      "happy"
  IL_0006:  ldc.i4.3
  IL_0007:  newobj     instance void Test04.Animal::.ctor(string, int32)
  IL_000c:  stloc.0
  IL_000d:  ldstr      "Park"
  IL_0012:  ldc.i4.s   33
  IL_0014:  newobj     instance void Test04.Human::.ctor(string, int32)
  IL_0019:  stloc.1

1. [IL_0001] ldstr은 스트링 개체를 스택으로 PUSH합니다. "happy"가 스택에 PUSH됩니다. 
2. [IL_0006] ldc.i4.3 은 정수값 3을 역시 스택으로 PUSH합니다. 
3. [IL_0007] newobj는 받아야 할 argument들 갯수만큼 스택에서 POP 한 후 새로운 객체를 생성하고 그 객체를 스택에 PUSH하게 됩니다. 
4. [IL_000c] stloc.0 는 스택에서 값을 POP하여 지역변수 인덱스 0 에 넣습니다. 

IL_0001 부터 IL_000c까지의 내용입니다. 그대로 하면 어떻게 되나요. 위의 C#코드에서 처럼

Animal dog = new Animal("happy", 3); 

을 수행한 결과가 됩니다. 변수 dog는 지역변수 인덱스 0 으로 태깅되어 있지요. IL_000d 부터 IL_0019까지는 korean 객체를 생성하는 과정입니다. IL_0012의 ldc.i4.s 33은 정수값 33을 스택에 PUSH하라는 명령입니다.

            if (dog.Equals(korean))
                Console.WriteLine("dog == korean");
            else
                Console.WriteLine("dog != korean");

  IL_001a:  ldloc.0
  IL_001b:  ldloc.1
  IL_001c:  callvirt   instance bool [mscorlib]System.Object::Equals(object)
  IL_0021:  ldc.i4.0
  IL_0022:  ceq
  IL_0024:  stloc.s    CS$4$0000
  IL_0026:  ldloc.s    CS$4$0000
  IL_0028:  brtrue.s   IL_0037
  IL_002a:  ldstr      "dog == korean"
  IL_002f:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0034:  nop
  IL_0035:  br.s       IL_0042
  IL_0037:  ldstr      "dog != korean"
  IL_003c:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0041:  nop

자 다음은 두 객체를 비교하는 부분입니다. 

1. [IL_001a] ldloc.0 은 0번 지역변수의 값을 스택에 PUSH합니다. 
2. [IL_001b] ldloc.1 은 1번 지역변수의 값을 스택에 PUSH하겠지요.
3. [IL_001c] callvirt 는 런타임에 바인딩된 method를 호출합니다. (call뒤에 virt가 붙은 건 호출 할 method가 virtual로 선언되어 있다는 걸 의미합니다. 실제로 Object.Equals()는 'public virtual bool Equals(object obj);' 로 선언되어 있습니다.) 호출하기 전 method와 관련된 객체와 해당 method의 인수와 관련된 값을 스택에서 POP합니다. 그리고 해당 method의 결과값이 있으면 스택에 PUSH하게 됩니다. 여기서는 equals의 값을 스택에 PUSH하게 되겠네요.
4. [IL_0021] ldc.i4.0 는 정수값 0을 스택에 PUSH하게 됩니다.
5. [IL_0022] ceq 는 비교 명령입니다. 비교에 사용될 값 value0과 value1을 스택에서 POP합니다. 여기서는 4번에서 넣었던 정수값 0과 3번 equals의 결과값이 각각 value0과 value1에 들어가게 되겠지요. 그리고 두 값을 비교해서 같으면 1이 스택으로 PUSH되며 틀리면 0이 스택으로 PUSH됩니다.
6. [IL_0024] stloc.s    CS$4$0000 스택에서 값을 꺼내어 CS$4$0000 이라는 지역변수에 넣습니다. 맨 위에서 보면 알다시피 CS$4$0000은 bool값을 가진 지역변수인데 우리가 직접 선언한 변수는 아니지요. if문에서 비교를 해서 나온 bool값을 저장하기 위해 CLR이 생성한 임시 지역 변수입니다. 결국 ceq로 비교된 값이 CS$40000 이라는 변수에 들어가게 됩니다.
7. [IL_0026] ldloc.s    CS$4$0000 은 지역변수 CS$4$0000의 값을 스택에 PUSH합니다. 아 이거 왜 뺐다가 다시 넣는거야.
8. [IL_0028] brtrue.s  IL_0037 는 스택에서 값을 하나 POP한다음에 이 값이 true거나 null이 아니거나 0이 아닌경우 IL_0037로 점프하게 됩니다. 결국 bool 형식의 CS$4$0000 값이 true면 IL_0037로 점프를 하게 되겠지요. 다시 정리하면 5번에서의 ceq의 값이 같으면 IL_0037로 점프를 하게 되는 것입니다. 또 거슬러가면 ceq의 값이 같기 위해서는 3번의 결과 값이 0이 되어야 하는데 3번의 결과 값이 0이라는 얘기는 equals()의 결과값이 0 그러니까 false라는 이야기죠. 결국 dog != cat 이면 IL_0037로 점프하는겁니다.
9. [IL_002a] ~ [IL_002f] 스택에 스트링값(dog == korean)을 넣고 Console.WriteLine()을 호출합니다.
10. [IL_0034] nop 아무일도 하지 않습니다.
11. [IL_0035] br.s       IL_0042 는 어떠한 조건에 상관없이 IL_0042로 점프하게 됩니다. [IL_0042]는 if문이 모두 끝난 다음의 명령문이 있는 주소입니다.
12. [IL_0037] ~ [IL_003C] 는 dog != korean 을 찍겠지요.

와 길고 복잡합니다. 첨부된 소스를 컴파일 하고 컴파일 된 실행 파일을 ILDASM으로 보면 계속 뒤의 코드를 볼 수 있는데 쭉- false를 리턴했던 dog2까지의 비교는 위와 동일한 패턴의 코드로 진행됩니다. 무엇보다 주시해야 될 부분은 cat이나 dog2의 객체는 쭉- newobj로 새로 생성한다는 점입니다.

            Animal dog3 = dog;

if (dog.Equals(dog3))
                Console.WriteLine("dog == dog3");
            else
                Console.WriteLine("dog != dog3");

  IL_00aa:  ldloc.0
  IL_00ab:  stloc.s    dog3
  IL_00ad:  ldloc.0
  IL_00ae:  ldloc.s    dog3
  IL_00b0:  callvirt   instance bool [mscorlib]System.Object::Equals(object)

이제 true를 반환했던 dog3입니다. IL코드를 읽어보면 알겠지만 dog3의 값은 지역변수 인덱스 0인 dog의 값(주소)를 가져와서 dog3 지역변수에 그대로 넣습니다. 이건 뭘 의미하는건가요?

Animal dog3 = dog;

를 수행한다는 것은 dog3라는 새로운 객체 공간을 생성해서 dog의 값을 복사해 오는 것이 아니라 dog라는 객체가 가리키는 값(주소)를 그대로 dog3와 연결 시킨다는 의미입니다. 결국 dog와 dog3가 가리키는 값(주소)는 같은 값(주소)입니다. equals()는 그래서 같다. 라고 결과를 반환했던 것입니다. equals()가 하는 일은 정말 간단하네요. 내용이고 자시고 간에 그냥 값이 같으면 true입니다. 

간단한 내용을 정말 길게 돌아서 확인했네요.

휴-

Object는 다음에 계속 하기로 하지요. 오늘은 이만!!

참고 소스 : 

크리에이티브 커먼즈 라이선스
Creative Commons License

'.NET Framework 4.0 > CLR' 카테고리의 다른 글

8. System.Object (2)  (0) 2010/03/03
7. System.Object  (0) 2010/02/16
6. Assembly - GAC(Global Assembly Cache)  (2) 2010/02/02
5. Assembly - Strongly named assemblies  (0) 2010/01/26
4. Assembly  (0) 2010/01/15
3. MSCorLib & Metadata  (2) 2010/01/06
TAG CLR

댓글을 달아 주세요

Posted by 오태겸(RuAA)
2010년도 어느새 한달이 지나가고 두번째 달이 되었습니다. 한달 한달이 왜 이렇게도 빨리 지나가는지... 할 일은 많은데 시간만 무심하게 지나가는 것 같습니다... 드래곤 볼의 "시간의 방" 같은 곳이 있었으면 하는 허무한(?) 생각도 들고.. ㅎ

이번 포스팅은 저번 주제에 이어서 Contract 중 Data Contract에 대해 이야기 해볼까 합니다.

간단히 얘기하면, Data Contract는 WCF 서비스에서 사용하는 개체에 대한 정보를 클라이언트에서 인지할 수 있게끔 XSD 형태로 매핑시켜주는 역할을 합니다.
저번 포스팅에서 충분히 설명했듯이 클라이언트에서는 서비스에 대한 정보를 WSDL을 통해 얻게 됩니다. 이때, 서비스에서 사용하는 개체에 대한 정보 역시 이 WSDL을 통해 전달되는데, 이렇게 XSD 형태로 매핑시켜주는 역할을 하는 것이 Data Contract 입니다.

그럼, 일단 코드를 한번 보고 설명을 이어 나가겠습니다. 역시, 말보다는 코드를 봐야 "아~ 이게 Data Contrat 구나~" 하실겁니다,, ㅋ

저번 포스팅에서 만들었던 서비스에 다음과 같이 Product라는 이름의 새로운 클래스를 추가하였습니다.

using System.ServiceModel;

using System.Runtime.Serialization;

 

namespace MyService

{

    [DataContract]

    public class Product

    {

        [DataMember]

        public int ProductId;

 

        [DataMember]

        public string ProductName;

 

        [DataMember]

        public string Company;

 

        [DataMember]

        public double Price;

 

        [DataMember]

        public DateTime CreateDate;

    }

}

ServiceContract 속성을 줬듯이 DataContract 속성(attribute)은 서비스에서 사용할 새로운 클래스에 지정해 주면 됩니다. 그리고 이 클래스의 필드들에겐 DataMemer 속성을 줬습니다.

이제 이 클래스를 서비스에서 사용하도록 기존 서비스의 코드를 다음과 같이 살짝 바꾸었습니다.

[ServiceContract(Namespace = "http://RuAAService.co.kr/")]

interface IProductService

{

    [OperationContract]

    Product GetProduct();

}

class ProductService : IProductService

{

    public Product GetProduct()

    {

        Product p = new Product();

        p.ProductId = 1234;

        p.ProductName = "ABC Chocolate";

        p.Price = 1500.0;

        p.Company = "Lotteee";

        p.CreateDate = DateTime.Parse("2010-01-22");

 

        return p;

    }

}


사용하는 방법은 특별히 따로 존재하지 않습니다. 다른 클래스들 처럼 그냥 사용하면 되는 것입니다. 여기서 조금 주목해야할 점은 GetProduct 메소드의 반환 타입이 우리가 새로 생성한 Product 클래스라는 것입니다.

Product  클래스는 서비스에서 생성한 클래스이기 때문에 클라이언트에선 알 수 없는 타입이겠죠. 서비스를 이용하는 프로그램이 아닐때는 그냥 참조 추가를 이용해 dll 파일을 참조하여 다른 클래스를 사용할 수 있지만, 서비스를 사용할 땐 이 방법을 쓸 수는 없습니다.

이러한 문제(?)를 해결하는 방법이 바로 wsdl에 이 클래스에 대한 정보를 추가하여 클라이언트에서 인지할 수 있게 하는 것인데, 이걸 Data Contract가 해주게 되는 것입니다.

그럼, 이 클래스에 대한 정보를 닷넷 컴파일러가 어떤 형태의 xsd로 매핑시켜주는지 눈으로 확인해 보겠습니다.

서비스를 실행시킨 상태에서 브라우저를 이용하여 http://localhost:8000/ProductService?wsdl=wsdl0 위치로 이동해 봅니다. 그럼, <wsdl:types> 라는 엘리먼트를 볼 수 있고, 그 밑에 서비스에서 사용하는 여러 가지 스키마에 대한 정보를 명시한 엘리먼트를 확인할 수 있습니다.


이 스키마들 중에 http://localhost:8000/ProductService?xsd=xsd2 위치로 이동해보겠습니다. 그럼 다음과 같은 내용을 확인할 수 있을 것입니다.


complexType 엘리먼트는 우리가 생성한 클래스의 이름을 나타내고 있고, 그 밑으로는 클래스의 멤버들에 대한 정의가 엘리먼트로 포함되어 있는 것을 확인할 수 있습니다.

이제 어떻게 클라이언트에서 이러한 클래스에 대해 인지할 수 있는지,, 아시겠죠? ^^

아~ 참고로 클래스의 필드들을 DataMember 속성을 이용해 노출을 했었는데, 만약 필드에 DataMember 속성을 적용시키지 않으면 아예 노출이 안된다는 것을 유념해주시기 바랍니다.
그리고, 필드들의 한정자가 public 이든, private 이든 이 역시 상관없다는 것도 알아두시면 좋을 것 같습니다. 오로지 DataMember 속성이 적용되었는지만 신경을 써주시면 됩니다.

DataContract 와 DataMember는 몇 개의 프로퍼티(property)를 가지고 있습니다.

DataContract는 Name과 Namespace 프로퍼티를 가지고 있는데, 이는 ServiceContract가 가지고 있는 프로퍼티와 같은 역할을 하는 녀석들이기 때문에 따로 긴 설명을 필요없을 듯 합니다.

그리고, DataMember 는 Name, Order, IsRequired 라는 프로퍼티들을 가지고 있습니다.
Name은 클라이언트에 노출되는 멤버의 이름을 명시적으로 설정하는 역할을 하구요, Order는 클라이언트에 멤버들이 노출될 때 그 순서를 정하는 역할을 수행합니다. (이 순서를 명시적으로 설정하지 않으면 기본적으로 멤버들은 알파벳 순으로 xsd에 나타나게 됩니다.) 사실 .NET 어플리케이션 끼리는 이 순서가 그리 중요하지 않습니다. 하지만 다른 플랫폼의 어플리케이션 간의 상호 운용성을 고려할 땐 중요할 수가 있습니다. 클라이언트로 부터 메시지를 받았을 때, 이 순서가 다르면 서버에선 인식할 수가 없게 되어버리거든요.
IsRequired는 멤버가 적어도 한번은 포함이 되어야 하는지에 대한 정의를 내려주는 역할을 합니다. 이 프로퍼티의 값이 true인 경우엔 설정된 멤버는 메시지에 적어도 한번은 꼭 포함되어야 함을 의미하는 것이죠~

그럼, 이러한 속성들을 적용했을 때, 어떻게 xsd로 표현되는지 한번 보도록 하겠습니다.
Product 클래스를 다음과 같이 살짝 수정해보았습니다.


[DataContract (Namespace="http://RuAAService.co.kr/Product", Name="ProductInfo")]

public class Product

{

    [DataMember(Name = "ID", Order = 1, IsRequired = true)]

    public int ProductId;

 

    [DataMember(Name = "Name", Order = 2, IsRequired = true)]

    public string ProductName;

 

    [DataMember(Order = 3)]

    public string Company;


   
[
DataMember(Name = "Value", Order = 4, IsRequired = true)]

    public double Price;

 

    [DataMember(Order = 5, IsRequired = true)]

    public DateTime CreateDate;

}

이제 xsd를 어떻게 확인하시는지 아시죠? xsd를 확인해 보면 다음과 같이 나타남을 확인할 수 있으실 겁니다.


complextType 엘리먼트에 name 속성의 값이 바뀌고, 네임스페이스의 값도 바뀌고,, 여러가지가 바뀐 것을 확인할 수가 있습니다. 프로퍼티 설정과 xsd를 비교해가면서 어떻게 적용되는지 유심히 살펴보시기 바랍니다. ^^

Company 멤버의 경우 IsRequired 속성을 따로 명시해주지 않았는데, 이 경우엔 minOccrus=0 으로 표현되는 것을 볼 수가 있습니다. 이게 기본값이며, 포함되지 않아도 됨을 나타내주는 것이죠~

이렇게 해서 Data Contract에 관한 기본적인 내용은 끝이 난 것 같습니다.

마지막으로, 수정한 이 서비스를 이용하도록 클라이언트 코드 역시 수정을 조금 해보구요~ 결과 화면을 보는 것으로 이번 포스팅을 마치도록 하겠습니다.

수정한 클라이언트의 코드는 다음과 같습니다.

static void Main(string[] args)

{

    // 프록시 클래스 인스턴스 생성

    ProductServiceClient myService = new ProductServiceClient();

    // 서비스 Operation 호출

    ProductInfo product = myService.GetProduct();

 

    Console.WriteLine("Product ID : {0}", product.ID);

    Console.WriteLine("Product Name: {0}", product.Name);

    Console.WriteLine("Made by :{0}", product.Company);

    Console.WriteLine("Price : {0:c}", product.Value);

    Console.WriteLine("Created Date : {0}", product.CreateDate.ToShortDateString());

}

이 코드에 대한 추가적인 설명은 필요없겠죠? 이제 다들 "이까이꺼~" 하실테니깐,, ^^

수정을 모두 완료한 후에 서비스와 클라이언트를 동시에 실행시키면 다음과 같은 결과 화면을 확인하실 수 있으실 겁니다. 


네~ 무사히 서비스에서 Product 클래스에 대한 데이터를 받아왔고, 이 데이터를 화면에 잘 뿌려주는군요...

아직까지는 WCF의 기본이 되는 내용에 대해서만 포스팅을 하고 있기에 그렇게 어렵지 않으실 거라 생각합니다.
저도 WCF에 대해 마스터하지 못하였지만, WCF를 이제 시작하는 분들께 조금이라도 도움이 되었으면 하는 마음으로 포스팅을 하고 있는 것이라, 아주 기본이 되는 내용들을 주제로 진행하고 있습니다.

이렇게 하나씩 채워나가다 보면, 언젠가 좀 더 복잡한 주제로 포스팅을 할 수 있는 시간도 올꺼라 생각합니다.
너무 조급해하지 마시고 따뜻한 시선으로 지켜봐주셨으면 합니다.
(뭐,, 질책이나 조언도 감사한 마음으로 받겠습니다 ^^)

그럼, 다음 포스팅때 뵙겠습니다 ^^
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 저렙프로그래머 2010/02/09 09:43

    일빠네요 ㅎㅎ 강좌 잘 보고 있습니다. 친절한 강좌 감사드립니다.

  2. 잘 보고 있습니다. ^^ 다음 강좌도 빨리 나오면 좋겠네요. ^^

  3. 안녕하세요. 감사히 보고 있는데
    .NET 3.5 (2008)에서는 DataContract가 없는데 혹시 다른 방식으로 구현이 가능한지요?

    • .NET 3.5 버전에서도 DataContract는 존재합니다. System.Runtime.Serialization을 참조 추가하시고 using 구문을 이용해서 네임스페이스를 추가하시면 사용할 수 있으실거에요~ ^^

Posted by 52

오늘은 GAC (Global Assembly Cache)에 대해서 공부해 보도록 하겠습니다. GAC는 보통 윈도우 설치 폴더 아래에 위치합니다. 만약 윈도우 설치 폴더가 C:\Windows 라면

C:\Windows\Assembly

에 위치해 있습니다. 윈도우 탐색기를 이용하여 한번 확인해 봅시다.


윈도우 7에서는 이렇게 Assembly 별로 세부 정보들을 정리하여 보여주고 있네요. 지난 주에 배웠던 Strongly named assemblies의 정보들이 보입니다. 이름과 버전, culture(locale) 그리고 공개 키 토큰 정보 입니다. 실제로는 기능별/제품별로 굉장히 많은 하위 디렉토리 구조로 구성되어 있습니다. 이런 식으로요.




자 그러면 이 GAC라는 곳에 우리가 만든 Strongly named assemblies를 복사하면 우리의 Assembly를 전역적으로 사용할 수 있는 것일까요? 당연히- 아닙니다.

GAC에 넣어주는 도구가 있습니다. 바로 GACUtil.exe 라는 툴입니다. 실행시켜보면 다음과 같은 다양한 옵션에 대한 안내를 해 줍니다.



다양한 옵션 설명 중 /i 가 바로 assembly를 GAC에 인스톨하는 옵션임을 알 수 있습니다. 그리고 /u가 지정된 assembly를 언인스톨하는 옵션이군요. 이 툴을 이용하여 우리가 만든 Strongly named assembly를 GAC에 설치할 수 있습니다.

GAC라는 영역이 한번 등록시켜놓으면 편리하게 어느 어플리케이션이나 편리하게 사용할 수 있게 됨은 사실이지만 GAC에 Assembly가 설치되게 되면 단순 파일 복사, 삭제 등의 작업만으로 해당 Assembly를 관리할 수 없게 됩니다. 그래서 손쉬운 배포를 위해서는 가능하면 GAC에 등록하는 형태의 배포가 아닌 전용배포의 방식이 관리하기에 편리한 면도 있습니다.

이 GAC가 존재함으로써 시스템 전역적으로 assembly를 사용할 수 있다는 것 외에 어떤 장점이 있을까요. 우선 지난 주에 언급했던 내용 중에 DLL Hell과 관련된 이야기를 기억하시는지요? 다른 회사에서 만든 같은 이름의 DLL 모듈로 인한 충돌 문제나 같은 회사에서 만든 같은 이름의 DLL모듈이라고 해도 버전이 틀려서 발생하는 문제등을 GAC는 훌륭히 처리해 줍니다. 같은 이름을 가진 Assembly라고 하더라고 GAC의 정해진 알고리즘에 의해 제각기 분리된 영역에 설치되게 되고 각 어플리케이션이 요구하는 Assembly를 정확히 구별하여 사용할 수 있도록 구성해 주는 것입니다.


그렇다면 GAC의 내부의 구조에 대해 간략하게 살펴보도록 할까요?


우선 Assembly 디렉토리 내를 살펴보니 다음과 같이 구성되어 있네요. 여기서

GAC 는 CLR 1.0, 1.1 버전에서 생성된 Assembly들이 포함되어 있습니다. 이 곳에 있는 Assembly들은 오직 x86 그러니까 32비트 OS환경 또는 64비트 OS환경에서 WoW64기술이 적용되어 실행됩니다. 이곳에는 Native x86코드가 포함되어 있는 경우도 있습니다.

GAC_32는 CLR 2.0 버전에서 생성된 Assembly들로써 32비트 환경의 OS 또는 64비트 OS환경에서 WoW64가 적용되어 실행되는 Assembly들이 저장되어 있습니다. 여기도 역시 Native x86코드가 포함되어 있는 assembly도 있습니다.

GAC_MSIL은 CLR 2.0 버전에서 생성된 Assembly들이 저장되어 있는데 순수하게 IL코드들로만 구성되어 있는 Assembly들이 저장되어 있습니다. 이 디렉토리 하부의 Assembly들은 32비트나 64비트의 OS 환경을 가리지 않습니다.

위의 이미지에는 나와있지 않지만 GAC_64라는 디렉토리도 있습니다. GAC_32와 비슷한 이름으로 보아 유추가 가능하겠지요? CLR 2.0 버전에서 생성된 Assembly들이며 64비트 OS 환경에서 실행되는 Assembly들이 저장되어 있는 공간입니다.

그리고 NativeImages.. 로 시작되는 디렉토리도 보이네요. 이 곳에는 NGen.exe라는 CLR의 Native Code 컴파일러를 이용하여 컴파일된 Assembly들이 저장되는 곳입니다.

NGen.exe


자 이젠 좀 더 아래 디렉토리로 들어가 볼까요? GAC_MSIL 디렉토리를 들어가 봅시다.



아 딱 보니 대충 짐작이 가네요. Namespace별로 디렉토리들이 구성되어 있는 것을 알 수 있습니다. 또 들어가 봅니다.



오 이것도 대충 짐작이 갑니다. 버전과 공개키 토큰이군요. 원래는 다음과 같은 형식으로 구성되어 있습니다.

(버전)_(Culture)_(공개 키토큰)

그런데 저 디렉토리 안에 있는 Assembly는 Culture 정보가 없기 떄문에 언더바(_) 두개로 Culture 정보가 생략되어 있네요. 그럼 저런 형식의 디렉토리 안을 들어가 보면 assembly 파일이 있겠지요?


네. 존재하네요. GAC는 이런식의 구조로 Assembly를 관리하고 있습니다. :)


크리에이티브 커먼즈 라이선스
Creative Commons License

'.NET Framework 4.0 > CLR' 카테고리의 다른 글

8. System.Object (2)  (0) 2010/03/03
7. System.Object  (0) 2010/02/16
6. Assembly - GAC(Global Assembly Cache)  (2) 2010/02/02
5. Assembly - Strongly named assemblies  (0) 2010/01/26
4. Assembly  (0) 2010/01/15
3. MSCorLib & Metadata  (2) 2010/01/06

댓글을 달아 주세요

  1. 첨언하자면, 탐색기에서 Assembly 폴더에 복사하셔도 GAC 에 등록이 됩니다.

Posted by 엄준일(땡초)

먼저 이전 포스트의 "MEF 는 Generic Type 을 지원하지 않는다!" 에서 언급했고, .NET CLR 2.0 부터 Generic Type 을 지원함에도 불구하고, .NET Framework 4.0 에 포함되는 MEF 가 Generic Type 을 지원하지 않는다는 것은 솔직히 납득하기가 어렵습니다. MEF 개발 PM 이 말하는 강력한 계약 기반(Strongly Contract Based) 의 모델이라는 점은 머리로는 이해는 되지만, 사실 안될 것도 없습니다. -_-;

MEF 가 갖는 대표적인 키워드인 Composable 은 현재 Generic Type 을 지원하지 않지만, 상당히 매력이 있습니다. 이미 현대적인 프레임워크는 Modular 에 집중하고 있고, MEF 는 더 나아가 Modular + Composite 이라는 상당한 매력을 가진 프레임워크입니다.

일단 서두는 이쯤에서 접어두고, MEF 가 Generic Type 을 지원하기 위한 몇 가지 공개되어 있는 방법을 알아보고, 다시 이야기를 나누어 봅시다. 
   

How to support Generic Type of MEF ?    

첫 번째 방법 - Factory Provider

가장 간단한 방법이 바로 Factory Pattern 을 이용한 방식입니다. 객체의 생성은 Factory 를 통해 생성하도록 하고, Factory 는 객체의 Type 을 받음으로써 객체의 생성을 Factory 에게 모두 의존하는 방법입니다. 우선 아래의 링크를 참고하세요.

 ExportProvider 를 재정의하여 객체의 Type 을 등록하여 원하는 Type 의 객체를 생성하도록 합니다.

1: public interface IService { }

2: public interface IUserService : IService { }

3:  

4: [Export]

5: public class UserController {

6: [ImportingConstructor]

7: public UserController(IUserService userService) { }

8: }

9:  

10: // in your application

11: private void Compose() {

12: var catalog = new AttributedAssemblyPartCatalog(Assembly.GetExecutingAssembly());

13: var factoryProvider = new FactoryExportProvider<IService>(GetService);

14: var container = new CompositionContainer(catalog, factoryProvider);

15: container.AddPart(this);

16: container.Compose();

17: }

18:  

19: public IService GetService(Type type) { return ... }

   

하지만, 이 방법은 상당히 문제가 많은 방법입니다. 가장 즐겨쓰고, 흔히 볼 수 있는 Pattern 이기 때문에 추가되는 Factory 마다 객체 등을 Factory Provider 에 등록을 해 주어야 합니다. 그 뿐만이 아니죠. Factory Pattern 의 특성상 객체를 생성하는 Factory 는 일일이 각 객체의 타입을 체크하여 반환해 주어야 합니다.

그리고 위의 코드에서는 Type 인자가 1개이지만, 그 이상이라면??? 가령, Generic Type Class<T1,T2,T3,T4,T5> 가 된다면 대략 난감하겠죠. 일단 작은 코드에서는 쓸만할 수 있지만, 꾸준히 성장하는 코드라면 이러한 Factory 방식은 코드의 변경이 너무 잦아집니다.

   

두 번째 방법 - Type Mapping

MEF 는 Codeplex 에 공개가 되어있고, MEF Contrib 으로 불리우는 MEF 의 확장 라이브러리 입니다. MEF Contrib 의 가장 큰 특징 중에 하나인 ComposablePartCatalog 를 재정의 하는 Generic Catalog 를 지원해 줍니다. 이 링크에서 Type Mapping 을 통한 문서를 볼 수 있습니다.

public class GenericCatalogContext
{
protected AggregateCatalog _aggegateCatalog;
protected GenericCatalog _genericCatalog;
protected ImportDefinition _repositoryImportDefinition;

public GenericCatalogContext()
{
var typeCatalog = new TypeCatalog(typeof(OrderProcessor), typeof(RepositoryTypeLocator));
_aggegateCatalog =
new AggregateCatalog();
_aggegateCatalog.Catalogs.Add(typeCatalog);
_genericCatalog =
new GenericCatalog(_aggegateCatalog);
string orderProcessorContract = AttributedModelServices.GetContractName(typeof(OrderProcessor));
var orderProcessPartDefinition = typeCatalog.Parts.Single(p => p.ExportDefinitions.Any(d => d.ContractName == orderProcessorContract));
_repositoryImportDefinition = orderProcessPartDefinition.ImportDefinitions.First();
Context();
}

public virtual void Context()
{

}
}

[InheritedExport]
public abstract class GenericContractTypeMapping
{
public GenericContractTypeMapping(Type genericContractTypeDefinition, Type genericImplementationTypeDefinition)
{
}

public Type GenericContractTypeDefinition { get; }
public Type GenericImplementationTypeDefinition { get; }
}

public class RepositoryTypeLocator : GenericContractTypeMapping
{
public RepositoryTypeLocator()
:
base(typeof(IRepository<>), typeof(Repository<>))
{
}
}

public class Repository<T> : IRepository<T>
{
}

이러이러한 과정을 통해서 아래와 같이 Type Mapping 을 통해 Generic Type 을 사용할 수 있습니다.

[TestFixture]
public class When_querying_catalog_for_an_order_repository_and_no_closed_repository_is_present : GenericCatalogContext
{
[Test]
public void order_repository_part_definition_is_created()
{
Assert.IsNotNull(_result.Item1);
}

[Test]
public void order_repository_export_is_created()
{
Assert.IsNotNull(_result.Item2);
}

public override void Context()
{
_result = _genericCatalog.GetExports(_repositoryImportDefinition).Single();
}

private Tuple<ComposablePartDefinition, ExportDefinition> _result;
}

Contract Type 와 Mapping Type 을 매핑하여 Locator 로 등록하여 주고, 각각 Mapping Class 를 통해 실제 계약의 Generic Type 매핑이 이루어 집니다.

다시 말해서, Generic Class 별로 Locator Class, Mapping Class, 그리고 Mapping Context Class 를 만들어주어야 합니다. 배보다 배꼽이 더 커지는 격입니다. 일단, 아이디어는 좋지만 안쓰고 말랍니다.

   

세 번째 방법 - MEF + Unity 조합

아마도 가장 이상적인 방법이긴 합니다. Unity Application Block 은 Unity Container Extension 을 지원하기 때문에 객체의 Register, Resolve 등의 이벤트를 가로채서 Unity 의 기능을 확장할 수 있습니다. 이 이벤트를 MEF 에서 받도록 하여 MEF 의 ExportProvider 의 GetExportsCore 를 통해 Unity 의 객체에서 Resolve 하도록 하는 방법입니다.

UnityContainerExtension 을 재정의하여, 아래와 같이 이벤트를 받고, 이것을 MEF ExportProvider 로 전달하는 방법입니다.

UnityContainerExntension 에서는 아래와 같이...

protected override void Initialize()
{
this.Context.Registering += new EventHandler<RegisterEventArgs>(Context_Registering);
this.Context.RegisteringInstance += new EventHandler<RegisterInstanceEventArgs>(Context_RegisteringInstance);
}

MEF 의 ExportProvider 에서는 아래와 같이…

protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
{
if (definition.ContractName != null)
{
Type contractType;
if(Mapping.TryGetValue(definition.ContractName, out contractType))
{
if (definition.Cardinality == ImportCardinality.ExactlyOne || definition.Cardinality == ImportCardinality.ExactlyOne)
{
var export = new Export(definition.ContractName, () => serviceLocator.GetInstance(contractType));
return new List<Export> { export };
}

}
}
return Enumerable.Empty<Export>();
}

일단 가장 완벽해 보입니다만, 이 속에는 그 이상 많은 문제들이 생기게 됩니다. MEF 도 내부적으로 Injection(주입) 기법을 사용하고, Unity 에서도 Injection 을 사용하는데 바로 이 Injection 방법이 달라지게 되는 것입니다. 즉, MEF 기반의 코드와 Unity 기반의 코드의 Injection 선언 방법이 틀려지고, 서로 호환할 수 없다는 것입니다.

결국 DI 프레임워크는 특정 DI Container 에 의존할 수 밖에 없어지고, 더불어 Compisite 과 Injection 은 두 가지의 사용 방법이 혼재될 수 밖에 없다는 것이죠.

   

Conclusion

MEF 에서 Generic Type 을 사용하고 싶어서 안달이 난 1은 여러 가지 방법을 찾아보았지만, 사용성, 재사용성, 확장성, 유연성 등 모든 면에서 원하는 해답을 찾지 못했습니다. 그리고 현재까지 MEF 에서 Generic Type 을 지원하기 위한 대략적인 3가지 방법을 정리해보도록 하죠.

  

장점

단점

MEF Factory Export Provider

  • 구현이 쉽다
  • Factory 의 관리가 힘들다
  • Factory 의 확장이 힘들다
  • 모든 Factory 를 Catalog 로 관리해야 한다.

MEF Contrib Type Mapping

  • 합리적이다
  • Type Mapping 코드가 복잡하다
  • Mapping/Locator/Context 클래스를 구현해야 한다
  • 상속 기반이다

MEF + Unity Integrated

  • 합리적이고 , 구현이 쉽다
  • Injection 기법이 서로 달라진다
  • Injection 코드가 서로 달라진다
  • Injection 이 호환되지 않는다
  • 각각의 객체간의 Composite 이 불가능하다

이제 슬슬 머리가 아파옵니다. 향후 .NET Framework 4.0 에서 가장 큰 빛을 보게 될 MEF 이지만, Generic Type 을 지원하지 않는다는 것은 가장 큰 오점이 아닐까 생각합니다. 우선 이쯤에서 마무리하고 어떻게 해야 할지 생각해 보도록 하지요.


MEF 에서 Generic Type 문제는 코드 플랙스에 MEFGeneric 으로 공개하였습니다.
[.NET/.NET Framework] - MEFGeneric 코드 플랙스에 공개합니다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

Posted by 엄준일(땡초)

.NET Framework 4.0 에 포함이 될 Managed Extensibility Framework(이하 MEF) 는 Generic Type 을 지원하지 않습니다. ( MEF is not supporting Generic Type!!!! )   

상당히 충격입니다. MEF 는 현재 Generic Type 을 지원하지 않습니다. 이것을 가지고 현재 중요한 프로젝트를 진행하기 위해 여러 가지 리뷰를 해 보고 있습니다만, MEF 가 Generic Type 을 지원하지 않는 것은 쉽게 말해 'MEF 는 아직…' 이라는 결론이 나는군요.    


Managed Extensibility Framework Basic

이것을 이해하기 위해서는 MEF 의 기본부터 이해해야 할 필요가 있습니다. 자세한 내용은 아래의 필자의 블로그 링크를 클릭하시면 Managed Extensibility Framework 에 대한 아티클을 볼 수 있습니다.

우선 이러한 원인은 MEF 가 Contract Model(계약 모델) 기반이라는 있다는 이유 입니다. 우리가 흔히 사용하는 계약 모델은 쉽게 이야기하면 제공자와 소비자로 구분할 수 있습니다. 제공자와 소비자의 거래가 성립이 되기 위해서는 바로 계약이라는 것이 필요하죠. MEF 로 비유하자만 Import/Export 가 바로 그것이며 그 계약을 성립시켜 주는 것이 MEF Container 와 Composition Batch 로 볼 수 있습니다.

바로 이러한 계약 기반과 Composable Part 라는 개념으로 기존의 컴포넌트의 재사용성을 높일 수 있게 되며, 좀 더 동적이며, 추상화가 가능한 프레임워크 입니다. 더 쉽게 얘기하면, 새로운 C 라는 컴포넌트는 A 와 B 라는 컴포넌트와 계약하여 결합시키거나, 기존 컴포넌트를 변형시키는 등 Composable Application 을 만들기 위해 계약의 명세만 알면 다양한 컴포넌트를 재생산, 변형, 다양성, 재활용 등을 할 수 있습니다.

 

MEF 는 내부적으로 이러한 명확한 계약을 위해 여러 가지 방법으로 계약을 정의할 수 있습니다. 기본적으로 ExportAttribute 을 사용하여 String, CLR Type, ExportMetadata 를 사용하게 되어 있지요. 하지만 MEF 는 모든 계약의 명세는 바로 String 을 사용하는 데에서 문제가 발생하게 됩니다. 그리고 이것이 Dependency Injection(DI) 와 Inversion Of Control(IoC) 와 다른 점입니다. 대부분의 DI 프레임워크는 Object 의 Lifecycle 을 관리하고 객체의 의존성을 낮추기 위해 역제어 하는 것에 초점이 맞추어져 있기 때문에 CLR Type 기반으로 Container 에 등록이 됩니다.

예를 들어 보면, 아래와 같은 것이 MEF 에서는 계약 명세 규격에 어긋난다는 의미입니다. (특정 DI 프레임워크에 종속되지 않는 코드입니다)

var container = new Container();
container.Register<IUMC<>>();

var obj = container.Resolve<IUMC<string>>();
obj.SayHello();

   

Why MEF is not supporting Generic Type?

MEF 가 Generic Type 을 지원하지 않는 것에 이미 많은 사람들이 문제를 발견했고, 몇 가지 해결 방법이 있긴 있습니다.

이미 Ayende Rahien 이라는 사람의 블로그에는 MEF 가 Generic Type 을 지원하지 않는 것에 대한 이야기를 합니다. 내용을 보면 처음부터 Microsoft 의 MEF 개발 팀은 Generic Type 을 배제하고 있었던 것 같습니다. 하지만 Ayende Rahien 씨는 이 문제에 대해 반드시 해결해야 한다는 이야기를 MEF 개발 팀과 나누었습니다. 저도 이 문제가 반드시 해결 되리라 생각합니다만… 현재로써는 글쎄 ^^;

여기에서 MEF 개발 팀은 조금 구차한 변명을 합니다. 위에서 얘기한 MEF 의 기본은 계약 기반의 프레임워크라는 것입니다. 이 문제에 대해 추측을 해보면, MEF 가 Generic Type 을 지원한다는 것은 Strongly Contract Based 가 될 수 없기 때문이고, Generic Type 으로 인해 명확한 계약이 이루어질 수 없다는 것입니다. 특히 MEF 는 계약의 명세가 모두 MEF 가 내부적으로 관리하고 있기 때문에, Generic Type 에 의한 객체 의 계약 관리는 엄청난 메모리 사용량을 증가로 이어질 가능성이 충분합니다.

실제로 Microsoft 에서 MEF 개발 팀의 PM 을 맡고 있는 Glenn Block 씨는 이 아티클에서는 MEF v1 에서는 Generic Type 을 지원하지 못할 것이라고 합니다. 만약에 Generic Type 을 지원하게 된다면 차기 버전이 될 듯 합니다.

하지만, 다시 한번 MEF 는 계약 기반의 모델이라는 것을 생각하지 않을 수 없습니다. 만약 계약이 명확하지 않다면 계약 자체가 불명확하다는 의미입니다. C# 2.0 부터 지원하는 Generic Type 의 명확하지 않는 타입이 계약에 존재한다면 이것은 계약 자체가 성립되기 힘들다는 전제 조건을 포함하게 됩니다.

MEF 의 예를 들어 봅시다. 아래와 같은 Generic Type 의 계약이 존재합니다. (현재의 MEF 로는 전혀 불가능한 코드입니다^^;)

public interface IUMC<T>
{

void SayHello<T>();
}

[Export(typeof(IUMC<>))]
public class UMC<T> : IUMC<T>
{
public void SayHello()
{
// TODO Impl...
}
}

CLR(Common Language Runtime) 의 Generic Type 의 특성상 Generic T Parameter 는 굉장히 다형적입니다. UMC<string> 또는 UMC<int> 또는 모든 Class Type 이 T Parameter 에 대입될 수 있습니다. 단순히 어떤 타입도 올 수 있다는 것을 떠나 물건을 팔 사람은 도대체 소비자가 누구와 계약한 것인지 알 수 없고, 실제 상거래와 같은 상황이라면 사기와도 같다는 것이죠. 굳이 예를 들자면, 주민등록번호가 다름에도 불구하고, 주민등록증의 이름이 같은 동명인에게 언제든지 계약을 할 수 있다는 것이죠.

DI(Dependency Injection / IoC) 는 CLR Type 을 기반으로 합니다. 일부 DI 프레임워크는 Tag 와 같은 Contract Data 를 제공하기는 하지만 이것은 Metadata 그 이상의 역활을 하지 않습니다. 즉 Contract(계약) 와는 전혀 무관하다는 이야기 입니다. 객체를 질의(Query) 하기 위함이지 Composable 을 위한 것은 아닙니다.

 

OK! I'm understand. But…!!

처음부터 MEF 는 계약 기반의 Composable/Plugin Model/Contract Based 라는 용어를 자주 만나게 됩니다. 그리고 계약 자체라는 의미에서 Generic Type 은 가장 큰 장애 요소임이 확실합니다. 그렇기 때문에 현존하는 모든 DI(Dependency Injection) 프레임워크는 계약(Contract) 라는 용어를 절대 사용하지 않습니다. 목적 자체가 계약과는 전혀 무관하기 때문입니다.

하지만, MEF 의 계약 모델은 내부적으로 String Based Contract 를 사용하고 있고, Generic Type 또한 String 으로 표현이 가능하기 때문에, 문자열의 Parsing 만으로 어느 정도의 Generic Type 을 지원할 수 있을 거라고 생각했습니다.

필자는 처음 MEF 를 본 순간 "이것을 물건이다!" 라는 걸 느꼈습니다만, 아마도 MEF 개발 팀은 두 가지의 고민을 했을 거라고 생각합니다. Silverlight 를 지원할지, Generic Type 을 지원할지에 대한 범용성에 대해서 말입니다. 하지만, Generic 에 대해 많은 피드백을 받음에도 불구하고 MEF v1 에 지원하지 않을 듯한 대답은 사실 "구차한 변명" 으로 밖에 들리지 않는답니다. 결국, 현재 MEF 는 Silverlight 를 지원하는 등 .NET Framework 의 범용성에 치중하였고, 결국 Generic Type 은 현재 시점에서 릴리즈 시점까지 구현이 불가능할 거라고 예상합니다.

아쉽긴 하지만, 현재 MEF 가 불가능한 Generic Type 에 대한 영역은 몇 가지 Open Source 에서 제공을 하고 있습니다. 단지 실제 사용성에 대한 의구심과 필자의 견해로는 안쓰는게 나을 것 같다는 판단입니다.

다음에 당장 지원하지 않는 Generic Type 을 어떻게 사용할지 알아보고 함께 돌파구를 찾아보도록 하겠습니다. 


MEF 에서 Generic Type 문제는 코드 플랙스에 MEFGeneric 으로 공개하였습니다.
[.NET/.NET Framework] - MEFGeneric 코드 플랙스에 공개합니다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

Posted by 오태겸(RuAA)
지금까지 세번의 포스팅으로 WCF의 기초에 대해 알아보았습니다.
기초를 뗏으니, 이제 기본으로 넘어가야죠~ ^^

WCF의 기본 중에서 가장 먼저 알려드리고 싶어 꺼내 든 주제는 Contract 입니다.

Contract의 사전적 의미는,, "계약" 이죠.
(네이X 사전에서 찾아보니 계약이란 의미 외에 살인 청부, 줄어들다, 수축하다,, 등의 의미가 있군요,,)

그럼, "계약"의 사전적 의미는 무엇일까요? (약간 글이 엉뚱한 방향으로 흐르는 것 같지만,, ^^;;)
역시, 네이X 국어 사전에서 찾아본 결과,, 
"관련되는 사람이나 조직체 사이에서 서로 지켜야 할 의무에 대하여 글이나 말로 정하여 둠" 이라고 합니다.

잘 아시겠지만 계약은 약속이랑 비슷하지만 약속 보다는 좀 더 강력한 의미로 쓰입니다.(법적 효력도 있죠,,)

다시, 본론으로 돌아와서,, 그럼, WCF 에서의 Contract 는 무엇을 말하는 걸까요?
네,, 다들 예상 하셨겠지만, WCF 에서의 Contrat 역시 사전적 의미와 비슷하게 쓰이며, WCF 서비스와 클라어언트 사이에 어떤 계약을 정의할 때 사용하는 것을 말합니다.

좀 더 자세하게 설명을 하자면, 이 Contract 라는 것은 서비스와 클라이언트가 서로 통신할 때 사용하는 메세지의 명세(specification)를 정의하는 것을 말합니다.

WCF 는 총 세가지 타입의 Contract가 있으며, 다음은 이 타입들에 대한 정의입니다.

  • Service contract : 서비스에 의해 구현되는 기능들에 대해 설명하며, 서비스 계약(service contract)으로 정의 된 .NET 타입의 클래스는 WSDL의 services, port types의 엘리먼트로 매핑됩니다. 서비스 계약과 함께 Operation contract는 서비스 계약 내에서 정의되며, 서비스의 동작(operation)을 설명합니다.

  • Data contract : 서비스가 통신(communication)을 하는데 사용하는 데이터 구조를 나타냅니다. 이는 CLR 타입을 XSD(XML Schema Definitions) 로 매핑해주는 역할을 수행하는데, WCF 가 통신할 때 사용되어지는 데이터들을 어떻게 직렬화(serialization) 또는 역직렬화(deserialization)를 수행하는지를 설명합니다.

  • Message contract : 메세지 계약은 CLR 타입을 SOAP 메세지로 매핑해주며, SOAP 메세지의 포맷을 설명합니다. 메세지 계약은 SOAP 헤더에서 바디까지 컨트롤할 수 있게 해줍니다.

예전 포스팅에서 몇 번 언급한 점이기 때문에 다들 아실거라 생각하는데,, WCF 서비스는 많은 시스템과의 상호 운용성(interoperability)을 높이기 위해 WSDL을 사용합니다. 위에서 설명한 세 가지의 Contract 들은 WCF 서비스나 서비스에서 사용하는 여러 데이터들을 WSDL 또는 SOAP의 요소들로 매핑 시키는 역할을 수행합니다. 이 말은 곧, 서비스와 통신하기 위한 메세지의 포맷을 정의한다는 말이기도 하지요.(앞에서 언급했듯이,,)

이제 Contract의 정의에 대해선 조금 이해가 가시지요? ^^

그럼, 이러한 Contract들이 어떻게 쓰이는지 하나씩 살펴보겠습니다.
우선, Service Contract 와 Operation Contract에 대해 알아보구요, 한 두번에 걸쳐서 Data Contract와 Message Contract에 대해서도 알아보도록 하겠습니다.

서비스 계약은 지금까지 몇번 언급했던 것 같습니다.
다시 한번 더 얘기하자면, 서비스 계약은 서비스가 제공하는 여러 기능(동작)들을 정의하는 인터페이스이며, 서비스 자체를 정의한다고 생각하면 될듯 합니다. 
그리고, 서비스에서 제공하는 기능들을 외부로 노출(?)하기 위해 사용하는 Operation Contract가 있습니다. 이는 Service Contract 내부에서 정의됩니다.

백문이 불여일견이니, 코드를 한번 보도록 하죠. 음,, 예전에 만들었던 코드를 다시 살펴 보면서 이해하는게 좋을 것 같네요 ^^

[ServiceContract]
interface IProductService
{
    [OperationContract]
    string GetFirstName(string empID);
}


우리가 만들었던 첫 WCF 서비스의 코드 중 일부입니다.
서비스는 ServiceContract 특성을 인터페이스에서 선언하고, 그 메소드에 OperationContract 특성을 선언해줌으로써, 서비스 계약을 정의해주었습니다.
기억하시겠지만, 실제 구체적인 서비스의 기능은 Service Contract가 선언된 인터페이스를 상속 받아 구현해야 했었죠.

그럼, 이렇게 서비스가 정의됐을 때 WSDL이 어떻게 만들어지는지 살펴보겠습니다.
우리가 작성했었던 첫 WCF 서비스를 동작시킨 상태에서 웹 브라우저를 이용해 http://localhost:8000/ProductService?wsdl 로 접근해 보면 다음과 같은 화면을 확인할 수 있습니다.


이것이 바로 WCF 서비스가 제공하는 WSDL입니다.
앞에서 언급했듯이 Service Contract로 정의 되어진 정보는 WSDL의 "service", "port type" 엘리먼트로 매핑된다고 하였습니다. 또한 Operation Contract로 정의된 부분은 "operation" 엘리먼트로 매핑된다고 했었죠,, 
이것 역시 다음과 같이 확인할 수 있었습니다.





이렇게 WCF를 이용하여 만들어진 서비스는 WSDL로 제공되어지며, 클라이언트에서 서비스를 사용할 수 있게 되는 것입니다.

그리고, ServiceContract 특성 클래스에는 Namespace 속성을 제공합니다. 이 속성은 WSDL과 SOAP 메세지의 Namespace의 값을 명시적으로 설정할 수 있게 합니다.

다음과 같이 기존의 서비스를 조금 수정해 보았습니다.

[ServiceContract(Namespace="http://RuAAService.co.kr/")]
interface IProductService
{
    [OperationContract]
    string GetFirstName(string empID);
}


그리고, 다시 이 서비스의 WSDL을 확인해보면, 서비스의 네임스페이스가 다음과 같이 바뀌어져있는 것을 확인할 수 있습니다.



자~ Service Contract에 대한 설명은 여기까지 입니다.
이번에는 실습보다는 이론적인 설명이 위주였습니다. 그래서 조금 재미가 없을 수도 있을 것 같네요 ㅎ
하지만, 이론도 중요하다는 것,, 아시죠? ^^
아무 생각없이 닷넷 인터페이스 만들고 ServiceContract 특성을 주는 것 보다 이러한 작업으로 서비스가 어떻게 클라이언트에 노출되는지를 알고 있는 것이 서비스를 구성하고, 클라이언트를 개발하는데 있어서 많은 도움을 줄 것이라 확신합니다.

이 글 역시, 많은 분들에게 조금이라도 도움이 되었으면 하는 바람을 가지면서 이만 줄이겠습니다 ^^
다음 포스팅때 뵙죠~ ㅎ
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 좋은 글 너무 감사합니다.

Posted by 52
CLR에서는 Assembly를 크게 두 종류로 나눌 수 있습니다. Strongly named assemblies(강력한 이름의 어셈블리 - 뭔가 말이 좀 이상하지요?) 그렇지 않은 Assembly 입니다. 

Strongly named assemblies는 해당 assembly의 제공자를 확인할 수 있는 유일한 공개/개인키 쌍으로 서명되어 있고 이 키들은 assembly는 고유한 ID의 일부분으로 인식되어 안전하게 버전이 관리되며 이 키를 통해서 서명된 assembly는 어느곳에서라도 유일한 이름으로 인식되며 배포가 가능하게 됩니다. 

assembly는 크게 전용(Private)으로 배포하는 방식과 전역(Global)으로 배포하는 방법으로 배포 방법을 나눌 수 있습니다. 

전용 배포라는 것은 매우 일반적인 방법의 배포방식으로 배포할 프로그램의 기본 디렉토리와 그 디렉토리의 하위 디렉토리에 관련 assembly들이 설치되어 배포된 형태이며 전역 배포라는 것은 Global Assembly Cache - GAC(전역 어셈블리 캐시) 같은 잘 알려진 특정 디렉토리에 배포되는 방식을 말합니다.

Strongly named assemblies는 전용, 전역 배포가 가능하지만 그렇지 않은 assembly는 전용 배포는 가능하나 전역 배포는 안됩니다. 위에서 알았다시피 Strongly named assemblies가 아닌 경우 유일하게 해당 assembly를 인식할 수 있는 방법이 없으니 당연히 전역적인 배포가 불가능합니다.


자 그럼 Strongly named assemblies를 어떻게 만들 수 있는지 살펴보도록 하지요. 그 전에 잠깐 왜 Strongly named assemblies가 필요한지 잠깐 생각해 봅시다.

다양한 프로그램들이 자신들이 사용하는 assembly를 사용하려고 하는 상황입니다. 일반적으로 그렇듯이 여러 프로그램에서 자주 사용하는 assembly는 잘 알려진 디렉터리에 위치해 있어야 다양한 프로그램들이 사용하기 용이할 것입니다. 

CLR 프로그램 이전의 배포 방식을 되새겨 볼까요.

Visual Studio 6, 7 버전의 Visual C++ 프로그램을 살펴보면 Visual C++ 컴파일러로 생성된 프로그램들은 공통적으로 msvcp71.dll이라던가 msvcp60.dll 등의 공용 모듈을 사용하게 됩니다. 그래서 일반적으로 프로그램 설치시 위의 모듈들을 설치되는 프로그램과 같은 경로에 넣기도 하지만 windows 폴더의 system32 디렉터리에 넣어놓곤 하지요. 일반적으로 MS관련 프로그램을 설치하게 되면 위의 ms..로 시작하는 dll들은 system32 디렉터리에 설치가 되기 때문에 응당 위의 모듈이 모든 PC의 system32 폴더에 있겠거니 착각하고 배포본에 넣어놓지 않았다가 낭패를 보는 경우도 적지 않게 있었지요. 

MS관련의 공통모듈 뿐만이 아니라 다양한 프로그램에서 자주 사용하는 모듈들은 Windows가 설치된 PC라면 설치된 프로그램이 어느 디렉터리에 설치가 되던지 참조가 가능한 system32 폴더에 설치해 놓는 경우가 많습니다. 이러면서 문제가 발생하게 되었는데 다른 회사에서 같은 파일 이름을 가진 모듈을 system32 폴더에 설치를 하게 되었다던가, 같은 이름을 가지지만 버전이 틀린 파일을 설치하게 되어 특정 프로그램은 정확히 자신이 참조해야 하는 모듈을 찾을 수 없어 프로그램이 정상적으로 실행되지 않는 이른바 DLL Hell과 관련된 문제가 생기가 된 것입니다. 무엇보다 별 생각없이 system32같은 중요한 디렉터리에 자신들이 배포하는 프로그램과 관련한 모듈들을 무작정 설치하는 것도 DLL Hell을 만들게 된 중요한 원인 중 하나였지요.

최근에도 그런 문제가 있었습니다. 인터넷 익스플로러 8과 아래아 한글과의 충돌 문제였지요,

인터넷 익스플로러 8이 설치된 PC에 아래아 한글이 설치되면 인터넷 익스플로러 8이 시작하자마자 다운되는 문제가 발생했는데 그 원인은 이미 인터넷 익스플로러 8 이 사용하는 system32폴더에 설치된 최신 버전의 jscript.dll 모듈을 아래아 한글이 사용자 PC에 설치되면서 구 버전의 jscript.dll모듈로 교체 설치를 함으로써(같은 파일명이니 덮어씌워지는 형태로 설치가 되어버린 겁니다.) 인터넷 익스플로러 8은 자신이 사용하는 jscript.dll 파일을 찾지 못해 뻗어버리게 된 것이지요.

단순히 파일 이름만으로 프로그램 자신이 참조해야 하는 모듈 또는 assembly를 찾는 건 좋은 방법이 아니라는 것이 증명된 것입니다. 그래서 CLR의 경우 정확히 자신이 참조해야 하는 assembly를 식별하기 위해서 다음과 같은 4가지 요소를 이용하여 assembly를 식별하게 됩니다.

1. 파일 이름 (확장자를 제외한)
2. 버전 번호
3. 컬쳐 (로케일)
4. 공개키 (또는 공개키를 이용하여 생성한 작은 해시값)

어떤 프로그램이 자신이 원하는 assembly를 찾을 때 저 4가지 요소가 정확히 맞아야지 해당 assembly를 자신이 찾는 assembly라고 확신하고 사용하게 된다는 말이지요. Strongly named assemblies에 해당하는 이야기입니다.

저 4가지 요소중 1, 2, 3번 요소는 굉장히 드물기는 하겠지만 다른 회사와 동일 할수도 있습니다. 하지만 4번 공개키는 공개/개인키 쌍을 만들어서 사용하는 것이니만큼 해당 assembly의 제작자가 틀리다면 같을 수가 없을 것입니다.

Strongly named assemblies가 아닌 경우는 공개키를 제외한 요소들을 manifest metadata로 가질 수 있지만 assembly를 찾을 때는 단순히 파일 이름만을 이용하여 찾게 됩니다.

자 그럼 이젠 Strongly named assemblies를 만드는 방법을 살펴보도록 합시다.

우선은 키를 생성해야 합니다. 키를 생성하기 위해 .NET Framework에서는 SN.exe 라는 툴을 제공합니다. Visual Studio Command Prompt 2010을 실행하여 다음과 같이 실행해 보면-

SN -k VSTSTeam.keys

VSTSTeam.keys 라는 파일명의 공개/개인키의 쌍을 생성하게 됩니다. 이 파일은 바이너리의 형태로 공개/개인키의 쌍을 갖고 있습니다. 우리는 공개키를 사용해야 합니다. 공개키를 확인하기 위해서는 만들어진 키 파일을 이용하여 다음과 같이 SN.exe 를 한번더 실행해야 합니다.

SN -p VSTSTeam.keys VSTSTeam.publickey

결과로 VSTSTeam.publickey 라는 파일이 생성됩니다. 이 파일은 공개키를 포함하고 있는 바이너리 파일입니다. 이 파일의 공개키를 한번 확인해 봅시다.

SN -tp VSTSTeam.publickey

그럼 다음과 같이 굉장히 긴- 공개키 값을 확인할 수 있습니다.


이렇게 공개키는 확인할 수 있지만 개인키를 확인할 수 있는 방법은 제공하지 않고 있습니다.

이렇게 생성한 키를 이용하여 assembly를 컴파일 할 때 다음과 같은 Argument를 이용하여 assembly에 공개키를 포함시키게 됩니다.

csc /keyfile:VSTSTeam.keys hello.cs

당연히 Visual Studio IDE에서도 이런 작업이 가능합니다.



Sign the assembly 라는 체크박스에 체크를 한 후 새로 key를 생성해도 되고 이미 생성된 key 파일이 있다면 해당 key파일을 이용하여 사용하면 됩니다.



참고 자료
CLR Via C# 2nd. Ed.
크리에이티브 커먼즈 라이선스
Creative Commons License

'.NET Framework 4.0 > CLR' 카테고리의 다른 글

7. System.Object  (0) 2010/02/16
6. Assembly - GAC(Global Assembly Cache)  (2) 2010/02/02
5. Assembly - Strongly named assemblies  (0) 2010/01/26
4. Assembly  (0) 2010/01/15
3. MSCorLib & Metadata  (2) 2010/01/06
2. CLR! CLR! CLR!  (3) 2009/12/30
TAG CLR

댓글을 달아 주세요

Posted by 오태겸(RuAA)
요즘, 날씨가 심상치 않습니다. 세계적으로 여기저기서 한파, 폭설로 난리도 아니더군요~
몇몇의 과학자들은 지구가 예전 온도를 찾기 위한 자정작용을 하고 있는 것이라 하는데,,
어쨌든 지구가 정상이 아니란 건 사실인 것 같습니다.
설마,, 2012년에 정말 지구가 멸망하는건 아니겠죠? ^^;;;;;

WCF 세번째 이야기를 시작하겠습니다~ ^^
이번에는 지난 포스팅때 다 하지 못한 내용에 대해 이야기를 해볼까 합니다.
지난 포스팅에서는 Console 어플리케이션을 이용하여 간단한 WCF 서비스를 구현하였었죠,,
서비스를 구현하였으니, 이제 이 서비스를 이용할 수 있는 클라이언트를 구현해야 할 때입니다.

클라이언트도 Console 어플리케이션을 이용해보도록 하겠습니다. 지난번 서비스를 만들었던 같은 솔루션내에 새로운 프로젝트를 추가합니다. (새로운 솔루션을 만들어도 상관은 없습니다 ^^) 아래 그림과 같이 저는 "MyServiceClient"라는 이름의 콘솔 어플리케이션 프로젝트를 추가 하였습니다.


다음으로 클라이언트 프로젝트에 지난번 만들었던 서비스를 참조 추가해야합니다. 서비스를 참조 추가하는 방법은,, 다들 예상 했겠지만,, 아!주! 쉽습니다. ㅎ
비주얼 스튜디오 Solution Explorer의 클라이언트 프로젝트에서 마우스 오른쪽 클릭을 해주면, 다음과 같은 메뉴가 나타납니다. 여기서 "Add Service Reference"를 선택합니다.


그러면, 서비스를 참조 추가할 수 있는 창이 뜹니다. 이 창의 Address 텍스트 박스에"http://localhost:8000/ProductService" 를 입력하고, "Go" 버튼을 클릭합니다. (방금 입력한 주소는 서비스 엔드포인트 주소입니다. 기억나시죠?? ㅎ) 이렇게 하면~ 서비스를 찾는 메시지가 뜨고 얼마 뒤 다음 그림과 같이 서비스를 찾았다는 메시지가 뜨는 것을 확인할 수 있습니다.
참~!! 이때, 아주 당연한 얘기지만, 서비스가 실행 중이어야 합니다. ctrl + F5 를 눌러 서비스를 실행하는 콘솔 창을 띄워놓은 상태에서 서비스를 찾아야 합니다.


Namespace 텍스트 박스엔 "ServiceReference1"이라고 적혀있지만, 이는 개발자 마음대로 변경 가능합니다. 그래서, 저는 "MyService"라고 수정 후에 "OK" 버튼을 클릭하였습니다.

이 작업을 모두 완료하면 Solution Explorer의 모습이 다음 그림과 같이 조금 바뀌게 됩니다.


"Service Reference" 라는 폴더가 새로 생겼고, 그 밑에 방금 등록했던 "MyService"가 추가 되어있는 것을 확인할 수 있습니다.
이로써, 클라이언트에서 서비스를 사용할 준비는 거의 끝났습니다. 너무 쉬운가요? ^^ 

그렇습니다. 아주 쉽죠,, 이렇게 개발자가 쉽게 작업을 할 수 있는 건 역시 대부분의 일을 비주얼 스튜디오가 자동으로 해주는 일이 많기 때문입니다.

비주얼스튜디오가 자동으로 해주는 일은 바로, 서비스와 통신할 수 있는 proxy 클래스를 생성 해 주는 것입니다. 이는 예전 닷넷 웹서비스를 참조 했을 때, proxy 클래스를 생성해 주던 것과 아주 비슷합니다. 여기서 이에 대한 자세한 얘기는 생략하려 합니다. 저도 자동으로 생성되는 개체에 대해선 모르는 것이 많기도 하고, 여기서 이런 얘기까지 하는건 조금 초점이 빗나가는 이야기가 될 것 같기도 하거든요,,^^;;

어쨌든, 서비스와 통신을 할 수 있는 준비가 거의 끝났다는데 큰 의미가 있는 것 같습니다. ㅎㅎ

아ㅡ 본격적으로 서비스를 이용하기 전에 한가지 더 얘기를 하고 넘어가야할 것이 있습니다.
비주얼스튜디오에서 쉽게 서비스를 참조 추가할 수 있었던 또 다른 이유,, 바로 Metadata Exchange Endpoint 입니다.

WCF의 Metadata라 함은, WCF 서비스와 통신하는 방식을 설명한 것이라고 간단하게 생각하시면 됩니다. 그러면 MEX(Metadata Exchange Endpoint) 가 무엇인지 대충 감이 오시나요?

네,, 추측하신대로 이러한 메타데이터를 교환하기 위한 엔드포인트를 말합니다. 클라이언트에서 이 MEX로 요청 메시지(request message)를 보내면, MEX 에서는 서비스가 가지고 있는 엔드포인트에 대한 내용과 메시지의 포맷에 대한 내용을 WSDL 형태로 클라이언트에 전송해 주는 것입니다.

비주얼 스튜디오에서 "서비스 참조 추가"를 실행하였을 때, 프록시 클래스와 config 파일을 생성해 주는 것은 모두 MEX에서 보내준 WSDL을 이용한 것이죠~

MEX에 대해서까지 설명을 들으니, 클라이언트와 서비스가 통신하는 방법에 대한 그림이 대충 그려지지 않으시나요?ㅎ

그럼, MEX를 정의하지 않으면 WCF 서비스와 통신하지 못할까요?

꼭 그렇지만은 않습니다. 물론, 비주얼 스튜디오에서 지원해주는 "서비스 참조 추가"를 이용하진 못합니다.
하지만, svcutil.exe 라는 명령어를 이용하여 프록시 클래스를 생성해 줄 수 있고, 이를 통해 서비스와 통신할 수 있습니다. 이 명령어에 대해서도 정리할 수 있는 시간을 가질 예정이지만, 지금 당장은 그렇게 자세히 알 필요는 없을 듯 합니다. 우리에겐 Visual Studio 라는 훌륭한 툴이 있으니깐요~ ㅎ 
(개발자는 너무 툴에 종속적이면 좋지 않다 생각하고는 있지만,,, ^^;;;)

이제, 드디어,, 서비스에 정의 되어 있는 메소드를 호출하고 사용해 볼 시간이 왔습니다.

using MyServiceClient.MyService;

namespace MyServiceClient
{
    class Program
    {
        static void Main(string[] args)
        {
            ProductServiceClient proxy = new ProductServiceClient();
            string strResult = proxy.GetFirstName("WCF");

            Console.WriteLine(strResult);
        }
    }
}

위의 코드는 클라이언트 Console 어플리케이션의 Program.cs 파일 내용입니다.

using 구문을 이용하여 포함시킨 MyServiceClient.MyService 네임스페이스는 비주얼 스튜디오에 의해 생성된 프록시 클래스의 네임스페이스입니다.
이 네임스페이스에서 상위 네임스페이스는 서비스에 정의 된 네임스페이스에 "Client"가 붙은 형태로 생성되며, 하위 네임스페이스는 서비스 참조 추가를 할 때 입력하여 준 것과 동일한 형태로 생성되어 집니다.

그리고, ProductServiceClient 클래스는 비주얼 스튜디오에 의해 생성된 프록시 클래스 입니다. 이 클래스를 이용하면 서비스에 정의되어 있는 메소드들을 클라이언트에서 맘껏 사용할 수 있게 되는 것입니다. 아래와 같은 방식으로 말이죠~ ^^
string strResult = proxy.GetFirstName("WCF");

이로써, 클라이언트에 작성할 것도 모두 다 했습니다.

그럼, 실행을 한번 시켜 보도록 하죠~

현재, 솔루션에서 서비스 프로젝트와 클라이언트 프로젝트가 모두 동작해야 되기 때문에 솔루션의 속성을 조금 변경할 필요가 있습니다. 
아래 그림에서 볼 수 있듯이, 솔루션 속성 창에서 "Multiple startup projects"를 선택하고 두 개의 프로젝트 모두 "Start" 하도록 값을 변경하여 줍니다.


이제 실행만 하면 됩니다. 과감하게~ F5 키를 눌러 실행을 시켜 보죠~
아래와 같은 화면이 나타납니다. 두둥~~!!


결과 화면을 보고 실망하셨나요?? ^^;;
지금까지, 길~게 설명한 것에 비하면 결과는 아주 보잘것 없습니다. 하지만, 실망(?)하지 마시기 바랍니다 ^^;
처음부터 이 서비스는 아주 아주 간단한 서비스라고 말씀 드렸었고, 이 간단한 서비스를 이용해 우리는 WCF 에 대한 큰 그림을 그릴 수 있었으니깐, 결코 실망스러운 결과는 아닙니다.

이렇게 해서 WCF의 기초에 대해선 모두 끝이 났습니다.
글을 이용하지 않고, 옆에서 말로 설명을 했다면, 금방 끝날 수 있는 내용을 두 개의 포스팅에 걸쳐 설명을 했습니다. 
이렇게 하고 나니, 글을 이용해 정보를 전달한다는 것이 결코 쉬운 일이 아니란 것을 새롭게 깨닫게 되면서, 파워 블로거분들이 존경 스러워 지네요 ^^

포스팅을 시작한지 얼마되지 않아 많이 부족하지만, 꾸준히 하다보면 저도 다른 분들처럼 멋진 글을 쓸 수 있는 날이 오겠죠~ㅎㅎ 그때까지 포기하지 않고 열심히 하겠습니다 ^^

그럼, 다음 포스팅때 뵙겠습니다~
저작자 표시 비영리 변경 금지
크리에이티브 커먼즈 라이선스
Creative Commons License

댓글을 달아 주세요

  1. 강의 잘 봤습니다~ 감사합니다^^

  2. 강의 잘 보고있습니다. ^^ 많이 배워가네요..

  3. 감사합니다 많은 정보 얻고 갑니다~

  4. 이렇게 글로 써주시니 이해가 가는군요.
    말로 설명을 들었더라면 이해가 잘 안가도 끄덕거려야 했을겁니다.
    그리고선, 정작에는 깨닫지 못하는 거죠.

  5. 좋은 정보 감사히 잘보고 갑니다.
    질문좀 하나 해도 될런지요?
    서버쪽에 WCF OperationContract로 쿼리를 작성하고
    클라이언트를 실버라이트 프로그램으로 서비스참조로 받아서 사용하려고 하는데요;;
    클라이언트쪽에 자동생성되는 함수들이 어떤형식으로 생성이 되는건지..
    그리고 자유롭게 사용하기위해서어찌해야 될런지;;
    답을 얻을수있는 공부해볼만한것들좀 조언좀 해주세요
    부탁드립니다.

    • 자동 생성되는 클래스들을 보고 싶다면 프로젝트 폴더 밑에 "Service References" 폴더를 보시면 확인할 수 있으실겁니다.
      그런데 서비스를 자유롭게 사용한다는 말이 어떤 말인지,, 명확하게 말씀을 해주시면 더 자세히 도와드릴 수 있을 것 같습니다. ^^;;