지난 회 차에 여러 가지 문제로 .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

개요

.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

- 취소는 어렵지.

약속은 하는 것 보다, 취소하는 게 어렵습니다. 게다가 취소할 때는 적절한 타이밍을 놓치면, 안 좋은 기억만 남기게 되죠. 그래서 프로그램에서도 취소를 제대로 할 수 있도록 지원하는 게 중요합니다. 누구나 실수 할 수 있거든요.


- TPL과 함께 취소 좀 더 쉽게하기. 어헣.

TPL은 두가지 형태의 병렬성을 지원합니다. 첫 번째는 작업 병렬성(Task Parallel)이고, 두 번째는 데이터 병렬성(Data Parallelism)입니다. 작업 병렬성은 하나 이상의 작업이 동시에 진행되는 것을 말하구요, 데이터 병렬성은 연속적인 데이터에 대해서 동일한 작업이 동시적으로 수행되는 것을 말합니다. 기존까지 Task클래스와 관련해서 살펴봤던게 작업 병렬성을 위한 것이었다면, 이번에는 데이터 병렬성을 지원하는 부분을 살펴보겠습니다.

데이터 병렬성을 매우 손쉽게 지원하기 위해서 System.Threading.Tasks.Parallel클래스에 병렬성을 지원하는 for와 foreach를 추가했습니다. Parallel.For와 Parallel.ForEach가 바로 그 것인데요. 하나씩 살펴보겠습니다.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace Exam13
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] nums = Enumerable.Range(1, 1000).ToArray<int>();
            Parallel.For(0, 1000, (i) =>
            {
                nums[i] = nums[i] * nums[i];
            });

            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(nums[i].ToString());               
            }
        }
    }
}

<코드1> 간단한 병렬 예제.

<코드1>을 보면, 1부터 1000까지의 정수 배열을 만든 뒤에, 각각의 수를 제곱하는 코드입니다. i번째의 숫자를 제곱해서 그 결과를 i번째 인덱스에 넣는 작업과, i+1번째의 숫자를 제곱해서 그 결과를 i+1번째 인덱스에 넣는 작업은 별개의 작업이며, 동시에 수행가능한 작업이죠. 저렇게 for와 거의 비슷한 모양으로 작성하고, for대신에 Parallel.For를 써주는 것 만으로도 남아도는 CPU의 코어를 활용할 수 있다니. 간편하죠?

Parallel.ForEach와 병렬 루프에서 예외를 처리하는 부분은 이미 다룬 부분이기 때문에 건너뛰구영. 바로, 병렬 루프를 취소하는 방법에 대해서 알아보겠습니다. 지난 포스트에서 작업을 취소하는 방법에 대해서 알아봤었는데요. 이번에도 크게 다르지 않습니다. 동일하게 CancellationTokenSource와 CancellationToken클래스를 활용합니다. 다만, 방법이 약간 다른데요, 예제를 보시죠.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

namespace Exam14
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts =
                new CancellationTokenSource();
            ParallelOptions parallelOptions =
                new ParallelOptions
                {
                    CancellationToken = cts.Token
                };
            cts.Token.Register(
                () => Console.WriteLine("Cancelling....")
            );

            Console.WriteLine("끝내려면 엔터키를 누르세용.");

            IEnumerable<string> files =
                Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories);
            List<string> fileList = new List<string>();

            Console.WriteLine("파일 개수 : {0}", files.Count());

            Task task = Task.Factory.StartNew(() =>
                {
                    try
                 {
                        Parallel.ForEach(files, parallelOptions,
                        (file) =>
                        {
                            FileInfo fileInfo = new FileInfo(file);
                            if (fileInfo.Exists)
                            {
                                if (fileInfo.Length >= 10000000)
                                {
                                    fileList.Add(fileInfo.Name);
                                }
                            }
                        });
                 }
                 catch (OperationCanceledException)
                 {
                 }
                });

            Console.Read();

            cts.Cancel();
            task.Wait();
           
            foreach (var file in fileList)
            {
                Console.WriteLine(file);
            }
            Console.WriteLine("총 파일 개수 : {0}",fileList.Count());
        }
    }
}

<코드2> 병렬 루프에서의 작업 취소.

<코드2>를 보면, Parallel.ForEach이용해서, 음악 파일 중에서 10메가가 넘는 파일만 찾아서 리스트에 담고 있습니다. 그리고 루프 취소와 모니터링을 위해서 CancellationTokenSource, CancellationToken클래스를 활용하고 있습니다. 다른점이 있다면, 병렬 루프에 옵션을 주기 위해서 ParallelOptions클래스를 사용하고 있다는 것이죠. 그리고 생성된 ParallelOptions타입의 객체에 Token을 주고, 그 객체를 Parallel.ForEach루프에 매개변수로 넘겨주고 있습니다. 결과를 보면, 늦게 취소를 한 경우에는 리스트가 모두 완성된 반면에, 빨리 취소를 한 경우에는 리스트가 만들어지다가 만 걸 확인할 수 있죠.

끝내려면 엔터키를 누르세용.
파일 개수 : 2746

Cancelling....

(중략...)

05 サンクチュアリ.mp3
06 空のように 海のように.mp3
07 月の虹.mp3
총 파일 개수 : 380
계속하려면 아무 키나 누르십시오 . . .

<결과1> 늦게 취소해서 다 완성된 리스트.

끝내려면 엔터키를 누르세용.
파일 개수 : 2746

Cancelling....

(중략...)

01.mp3
02.mp3
03.mp3
01.うるわしきひと.mp3
총 파일 개수 : 256
계속하려면 아무 키나 누르십시오 . . .

<결과2> 중간에 취소해서 만들어지다 만 리스트.

ParallelOptions를 통해서 CancellationToken을 받은 병렬 루프는 내부적으로, IsCancellationRequested속성을 계속해서 주시하고 있습니다. 그리고, 이 취소 요청이 들어와서 이 속성이 true가 되면, 그 이후로는 새로운 루프가 시작되는 걸 막아버리는 것이죠. 그리고 병렬 루프가 취소되었음을 외부에 알릴 수 있는 유일한 방법이 OperationCanceledException을 통해서 인데요. <코드2>를 보면, catch를 통해서 예외를 잡긴하지만, 무시해버렸습니다. 그래서 Register메서드를 통해서 등록된 "Cancelling...."이라는 메세지가 출력되고 프로그램이 종료된 것이죠.


- 마치면서

역시 병렬처리를 간단하게 만들어 주는 만큼, 병렬처리를 취소하는 방법도 최대한 간단하게 만들어 주네요. TPL만쉐이! 어헣.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
2. http://msdn.microsoft.com/en-us/library/dd537608.aspx
3. http://msdn.microsoft.com/en-us/library/dd537609.aspx

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- 뭐, 미안하다고?

선진국에 가보면, 약간만 부딛힐 듯한 상황이라면, 서로 'Excuse me', '스미마셍'같이 서로를 배려하는 모습을 볼 수 있습니다. 우리나라에서는 아직 길을 걸으면서 뒷 사람에게 담배연기를 선사한다던가, 뭐 그리 급한지 보행자일 때는 운전자를, 운전할 때는 보행자를 씹으면서 급하게 서두르는 모습을 쉽게 볼 수 있습니다. 층간소음이 일어나면 오히려 윗집이 더 큰소리를 치기도 하죠. 시민의식으로 겨루는 한일전에서도 완승을 거뒀으면 좋겠다는 생각을 합니다만, 저 역시 모범시민은 아니기에 같이 노력해야겠죠. 어허허허헣. 오늘은 닷넷이 예절을 얼마나 잘 지키는 지, 한번 살펴보겠습니다.


- Stop it, Now!

위 소제목을 보시고, 잭 바우어를 떠올렸다면, 24시의 팬이시겠군요. 잭 바우어는 너무나도 급한 상황을 많이 만나는데요, 상대방에게는 정말 미안하지만, 상황을 해결하기 위해서 윽박지르고, 때로는 때리고, 아주 때로는 다리를 쏘는 등등등! 의 방법을 사용합니다.

아흙. 닷넷의 멀티 스레드 환경을 한번 생각해보죠. 여러개의 스레드가 작업을 처리하는 동안, 하나의 스레드는 사용자의 UI에서 입력을 기다립니다. 그리고 사용자가 취소버튼을 누르면, 사용자의 의지를 이어받아서 다른 스레드들을 취소해야 하는데요. 기존의 .NET 3.5까지는 작업 중인 스레드를 취소하는 게 매우 무례했습니다. 취소해야 할 때는 기냥 바로 끼어들어서 취소해버렸기 때문이죠. 그렇게 하면, 데이터 업데이트가 이뤄지는 도중에 취소되어서 부분적으로만 데이터가 업데이트 된다든지, 자원해제가 제대로 안 된다든지 하는 부작용의 위험이 항상 존재합니다. 그래서 가능하면, 다른 방법이 전혀 없을 때, 이렇게 하는 것이 좋겠죠?

물론 기존의 방식도 여전히 활용가능하지만, 이젠 닷넷이 많이 예의를 갖췄습니다. 닷넷 4.0에 새롭게 추가된 PLINQ나 TPL을 사용하는 경우에는 취소 요청 접근법(cancellation request approach)만 사용할 수 있는데요, 이런 방식을 협력적인 취소(cooperative cancellation)이라고 합니다. 즉, 한 스레드가 다른 스레드를 강제로 종료시키는 게 아니라, 작업 취소 API를 통해서 작업을 취소해줄 것을 요청하는 것이죠. 취소 플래그를 통해서 취소요청을 받은 작업은 취소요청에 어떻게 응답할 것인지 선택할 수 있습니다. 예제를 하나 보시죠.

using System;
using System.Threading.Tasks;
using System.Threading;

namespace Exam11
{
    class Program
    {
        static void PrintDash(CancellationToken cancellationToken)
        {
            cancellationToken.Register(Canceled);

            while (!cancellationToken.IsCancellationRequested)
            {
                Console.Write("-");
            }
        }

        static void Canceled()
        {
            Console.WriteLine("작업이 취소되었군요!!");
        }

        static void Main(string[] args)
        {
            string stars = "*".PadRight(Console.WindowWidth - 1, '*');

            CancellationTokenSource cancellationTokenSource =
                new CancellationTokenSource();

            Task task = Task.Factory.StartNew(
                () => PrintDash(cancellationTokenSource.Token));
           
            Console.ReadLine();

            cancellationTokenSource.Cancel();
            Console.WriteLine(stars);
            task.Wait();
            Console.WriteLine("작업의 완료상태 : {0}", task.Status);
            Console.WriteLine();
        }
    }
}

<코드1> 취소 요청 접근법.

<코드1>은 그냥 평범하게 '-'를 출력하는 예제입니다. 하지만, 새로운 클래스가 몇개 보이는데요. CancellationTokenSource, CancellationToken클래스말이죠.

namespace System.Threading
{
    [ComVisible(false)]
    [DebuggerDisplay("IsCancellationRequested = {IsCancellationRequested}")]
    public struct CancellationToken
    {
        public CancellationToken(bool canceled);
       
        public static bool operator !=(CancellationToken left, CancellationToken right);

        public static bool operator ==(CancellationToken left, CancellationToken right);

        public bool CanBeCanceled { get; }

        public bool IsCancellationRequested { get; }

        public static CancellationToken None { get; }

        public WaitHandle WaitHandle { get; }

        public bool Equals(CancellationToken other);

        public override bool Equals(object other);

        public override int GetHashCode();

        public CancellationTokenRegistration Register(Action callback);

        public CancellationTokenRegistration Register(Action<object> callback, object state);

        public CancellationTokenRegistration Register(Action callback, bool useSynchronizationContext);

        public CancellationTokenRegistration Register(Action<object> callback, object state, bool useSynchronizationContext);

        public void ThrowIfCancellationRequested();
    }
}

<코드2> 구조체 CancellationToken.

CancellationToken클래스는 말 그대로, 현재 이 토큰이 어떤 상태에 있는지 모니터링 하기 위한 정보를 갖고 있습니다. 이 토큰이 현재 취소 요청을 받았는지, 취소요청을 받으면 어떤 행동을 취할 것인지 등을 확인하고, 설정할 수 있습니다.

namespace System.Threading
{
    [ComVisible(false)]
    public sealed class CancellationTokenSource : IDisposable
    {
        public CancellationTokenSource();

        public bool IsCancellationRequested { get; }

        public CancellationToken Token { get; }

        public void Cancel();

        public void Cancel(bool throwOnFirstException);

        public static CancellationTokenSource CreateLinkedTokenSource(params CancellationToken[] tokens);

        public static CancellationTokenSource CreateLinkedTokenSource(CancellationToken token1, CancellationToken token2);

        public void Dispose();
    }
}

<코드3> CancellationTokenSource 클래스

그리고 CancellationTokenSource는 CancellationToken의 기반이 되는 클래스로(Source라는 이름이 붙어있죠), CancellationTokenSource에서 생성된 각각의 Token에 대해서 취소를 요청하는 역할을 합니다. CancellationTokenSource에서 Cancel메서드로 취소요청을 하면, 같은CancellationTokenSource에서 생성된 Token들은 전부 취소요청을 받는 셈이죠.


한가지 주목해서 보실 점은 CancellationToken가 클래스가 아니라 구조체라는 것입니다. 즉, Token을 매번 다른 객체에 넘겨줄 때마다 새로운 복사본이 생성된다는 것이죠. 그래서 각각의 스레드에 넘겨진 Token은 각각 독립적인 복사본 이므로, Cancel메서드는 스레드 안전성(thread-safe)을 확보할 수 있습니다. 만약에 참조가 그냥 복사된다면, 각각의 스레드가 Token에 손을 대면, 다른 스레드가 참조하는 Token에도 동일한 변화가 생겨서 예측불가능한 일이 벌어지겠죠.

<코드1>을 보면, 병렬적으로 수행되는 작업에서 취소 요청을 모니터링하기 위해서, CancellationToken을 인자로 넘겨주는 것을 볼 수 있습니다. 그래서 PrintDash메서드 내부에서 IsCancellationRequested속성을 통해서 작업 취소 요청이 들어왔는지 계속 해서 확인하게 되죠. 그럼 <코드1>을 실행 해볼까요?

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--작업이 취소되었군요!!
*******************************************************************************
작업의 완료상태 : RanToCompletion

계속하려면 아무 키나 누르십시오 . . .

<결과1> <코드1>의 실행결과.

<결과1>을 보면, 작업의 완료상태를 출력하는 부분이 있는데요, 이 부분에서 RanToCompletion이 출력되고 있습니다. 그래서 만약, ContinueWith메서드로 연쇄 작업을 연결하고, 옵션을 OnlyOnCanceled로 설정해준다고 하더라도, 연쇄작업은 실행되지 않습니다. 작업은 완료된 상태이기 때문에, 연쇄 작업이 취소되었다는 에러메세지만 확인할 수 있을 뿐이죠. 그렇다면, 연쇄작업을 이용해서 <코드1>과 동일한 결과를 내려면 어떻게 해야 할까요?

using System;
using System.Threading.Tasks;
using System.Threading;

namespace Exam12
{
    class Program
    {
        static void PrintDash(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                Console.Write("-");
            }

            if (cancellationToken.IsCancellationRequested)
            {
                cancellationToken.ThrowIfCancellationRequested();
            }
        }

        static void Main(string[] args)
        {
            string stars = "*".PadRight(Console.WindowWidth - 1, '*');

            CancellationTokenSource cancellationTokenSource =
                new CancellationTokenSource();

            Task task = Task.Factory.StartNew(
                () => PrintDash(cancellationTokenSource.Token),
                cancellationTokenSource.Token);

            Task canceledTask = task.ContinueWith(
                (antecedentTask) => Console.WriteLine("작업이 취소되었군요!!"),
                TaskContinuationOptions.OnlyOnCanceled);

            Console.ReadLine();

            cancellationTokenSource.Cancel();
            Console.WriteLine(stars);
            canceledTask.Wait();
            Console.WriteLine("작업의 완료상태 : {0}", task.Status);
            Console.WriteLine();
        }
    }
}

<코드4> <코드1>과 동일하지만, 연쇄작업을 사용하는 코드.

<코드4>가 바로, <코드1>과 동일한 결과를 내는 코드입니다.(사실 완전히 같지는 않습니다. 실행해보면, '작업이 취소되었군요!'라는 멘트가 출력되는 위치가 다르지요.) 이런식으로 연쇄작업을 연결해놓고, 병렬로 실행되는 메서드 안에서, ThrowIfCancellationRequested()메서드를 통해서, 취소되었을 때, 취소되었다는 표시를 하도록 한 것이죠. 그러면, 연쇄 작업이 바톤을 이어받아서 실행을 계속하게 됩니다. 그리고 또 한가지 차이점은 작업을 생성할 때, 인자로 Token을 넘겨준다는 것이지요.


- 마치면서.

요즘 월드컵을 보면 16강이 가능할 것 같다는 생각이 들기도 하는데요. 꼭! 갔으면 좋겠네요!! ......이게 마무리 멘트라니-_-!! 어헣.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
오늘도 하루가 밝았어요.
상쾌한 기분으로 기지개를 켜고, 커튼을 젖혔어요.

오~ 마이~갓.
바깥에 왠 검은 외투를 입은 사람들이 망원경 뒤집어쓰고 이쪽을 째려보고 있어요.
아침부터 기분이 좋지 않아요.
자고일어나니 스타가 된걸까요, 왜 이쪽을 째려보는 건지 도무지 모르겠어요.
집밖을 나섰어요.
대놓고 따라와요.
집밖을 나왔는데도 왜 망원경은 계속 뒤집어 쓰고 있는건지 모르겠어요.
- [감시당하는 자]의 탐구생활


- 시작부터 탐구생활드립이냐.

그냥 한번 해보고 싶었을 뿐이니 노여워 마시길 바랍니다. 오늘은 감시하는 자와 감시당하는 자의 이야기를 해보려고 합니다. 즉, Observer패턴을 말하는 건데요. 뭔가 변화가 있을때 그 변화를 바로 알아차려서 어떤 동작을 수행해야 할때. 변화의 대상을 유심히 관찰하고 있어야 겠죠. 근데, 유심히 관찰하고 있는거 보다 변화가 일어났음을 알려주는 친구가 있다면 더 편리하겠죠. 이벤트 모델이 그중의 한 예 입니다. 이벤트가 일어났을때 통지를 받겠다고 등록을 해두면, 이벤트 발생시에 이벤트를 기다리는 모든 객체들에게 이벤트가 발생했음을 통지해줘서 적절한 조치를 취하도록 해주는 것 처럼 말이죠.

이런 건, 기존에는 직접 패턴을 통해 작성을 해야 했지만 닷넷 프레임워크 4.0에 이런 Observer패턴을 지원하기 위한 새로운 인터페이스가 추가 되었습니다. 거기에 대해서 설명을 드려볼까 합니다. 기존의 포스트와 마찬가지로 이번 포스트 역시 눈을 조심하시고 언제든지 OME를 외칠 준비를 하시길 바랍니다.



- 기존의 방식으로.

닷넷 프레임워크 4.0이전에는 어떻게 만들어야 했는지 한번 알아보겠습니다. 제 이해가 부족해서 적절치 못한 예제와 적절치 못한 수준일 수도 있으니, 마음의 준비 하시구요. 우선, UML 다이어그램을 한번 보시죠.

- 그림1. 예제의 UML 다이어그램.(인터페이스가 클래스모양을 하고 있는 것 같지만, 착시현상입니다. 이해를 위해서 부족한 실력이지만 그려봤습니다-_-)

네, 그림에서 느낌이 오듯이 라디오방송국과 청취자를 모델링해봤습니다. 제가 만드는거 중에 쓸모있는 건 별로 안나오죠-_-. 우선 RadioBroadcaster라는 인터페이스가 있구요, KBRRadioBroadcaster가 그걸 상속해서 방송국의 역할을 합니다. 그리고 각각의 Listener가 방송국에 주파수를 맞추고, 전달되는 전파를 수신하는 형태이구요.

enum BroadcastStatus
{
    StandBy = 0,
    OnAir = 1,
    End = 2
}

interface RadioBroadcaster//지켜볼 대상
{
    bool ListenTo(Listener listener);
    bool QuitFrom(Listener listener);
    void DoBroadcasting();
    void StartBroadcast();
}


네, 일단 방송의 상태를 알려주는 열거자와 지켜볼 대상이 되는 인터페이스입니다.


class KBRRadioBroadcaster : RadioBroadcaster//정보의 공급자
{
    private List<Listener> listeners = new List<Listener>();
    public BroadcastStatus broadcastStatus = BroadcastStatus.StandBy;

    private List<string> scripts = new List<string>
    {
        "안녕하십니까. KBR방송국의 김병만 기자입니다.",
        "요즘 살림살이 초큼 어찌 나아지셨슴까?",
        "안 나아지셨다구요? 그럼 이 방송에서 취업 필살전략을 알려드립니다.",
        "요즘같이 취업이 어려운때 3개 국어가 가능하면 취업 직빵이겠죠.",
        "따라 해보시져, Handle いぱい 꺽어.(핸들 이빠이 꺽어).",
        "3개국어 정말 쉽져? 이것만 한다면 당신도 취업계의 슈퍼스타!"
    };

    private int currentScriptIndex = 0;

    public bool ListenTo(Listener listener)
    {
        try
        {
            listeners.Add(listener);
        }
        catch (Exception ex)
        {
            return false;
        }

        return true;
    }

    public bool QuitFrom(Listener listener)
    {
        if (listeners.Contains(listener))
        {
            listeners.Remove(listener);
            return true;
        }

        return false;
    }

    public void DoBroadcasting()
    {
        if (scripts.Count.Equals(currentScriptIndex))
        {
            Console.WriteLine("방송끗.");
            broadcastStatus = BroadcastStatus.End;
        }
        else
        {
            foreach (Listener listener in listeners)
            {
                listener.Listening(scripts[currentScriptIndex]);
            }
            currentScriptIndex++;
        }
    }

    public void StartBroadcast()
    {
        broadcastStatus = BroadcastStatus.OnAir;
    }
}


그리고 방송국입니다. 듣고 싶은 청취자는 ListenTo메서드를 통해 주파수를 맞추고 QuitFrom을 통해 라디오를 끕니다. 그리고 DoBroadcasting을 통해 정해진 대사가 한번에 하나씩 전파를 통해 나가구요.


class Listener//지켜보는 행위의 주체
{
    private string name;

    public Listener(string name)
    {
        this.name = name;
        Console.WriteLine("나는야 {0}. 라디오를 가진 남자지.",this.name);
    }

    public void ListenToKBRRadio(RadioBroadcaster radioBroadcaster)
    {
        bool status = radioBroadcaster.ListenTo(this);

        if (status)
        {
            Console.WriteLine("{0} : 수신감도 조쿠나. ㅎㅎㅎㅎ", this.name);
        }
        else
        {
            Console.WriteLine("{0} : 아놕. 라디오도 못듣능구나", this.name);
        }
    }

    public void QuitFromKBRRadio(RadioBroadcaster radioBroadcaster)
    {
        bool status = radioBroadcaster.QuitFrom(this);

        if (status)
        {
            Console.WriteLine("{0} : 아놔. 완전 재미없엉", this.name);
        }
        else
        {
            Console.WriteLine("{0} : 아. 듣고 있던거 아니구낭", this.name);
        }
    }

    public void Listening(string script)
    {
        Console.WriteLine("{0} : {1}", this.name,script);
    }
}


그리고 이번엔 지켜보는 자, 청취자입니다. 라디오를 청취하고, 끊을 수 있으며, Listening을 통해서 매번 날아오는 전파를 수신합니다.

class Program
{
    static void Main(string[] args)
    {
        Listener listener1 = new Listener("마논개");
        Listener listener2 = new Listener("코턱용");
        Listener listener3 = new Listener("송순신");

        KBRRadioBroadcaster kbrRadioBroadcaster = new KBRRadioBroadcaster();
        listener1.ListenToKBRRadio(kbrRadioBroadcaster);//마논개 라디오 청취시작.
        int count = 0;
        do
        {
            if (count == 0)
            {
                kbrRadioBroadcaster.StartBroadcast();
            }
            if (count == 2)
            {
                listener2.ListenToKBRRadio(kbrRadioBroadcaster);//코턱용 라디오 청취시작
            }
            if (count == 3)
            {
                listener1.QuitFromKBRRadio(kbrRadioBroadcaster);//마논개 라디오 끔
                listener3.ListenToKBRRadio(kbrRadioBroadcaster);//송순신 라디오 청취시작
            }
            kbrRadioBroadcaster.DoBroadcasting();
            count++;
            Thread.Sleep(500);
        } while (kbrRadioBroadcaster.broadcastStatus != BroadcastStatus.End);
       
    }
}


그리고 프로그램 코드지요. 마논개, 코턱용, 송순신 세명이 라디오를 청취하는데요. 처음에는 마논개 혼자 듣다가, 두번이후에 코턱용이 라디오를 듣기 시작합니다. 그리고 세번째가 되면, 마논개가 라디오가 지겨워서 끄구요, 송순신이 라디오를 듣기 시작합니다. 방송이 끝날때까지, 0.5초당 한번씩 전파를 타고 방송이 날아오구요. 아웃사이더쯤되야 가능한 속도겠네요. 실행화면을 보시면 아래와 같습니다.

-그림2. 실행한 모습!

넵, 의도했던대로 방송이 전파를 타고 나오면서 사람들이 방송을 들었다가 빠집니다. 대사가 다 나온후에 방송이 종료되구요. 제 내공의 문제로 정확한 Observer패턴의 형태나, 사용방법이라고 볼 수는 없겠지만 뭐 이해에는 문제가 없을 것 같습니다. 하하하-_-;;;


- 이제 새로운 걸 내놔봐.

넵. 이젠 닷넷 4.0에 추가됐다는 새로운 인터페이스 IObservable<T>와 IObserver<T>에 대해서 말씀드려보도록 하겠습니다. IObserable<T>는 지켜볼 대상이 되는 클래스가 구현을 할 인터페이스입니다. 위에서 보자면 방송국이 되겠죠. 그리고 IObserver<T>는 지켜보는 행위를 하는 클래스가 구현을 할 인터페이스입니다. 마찬가지로 청취자가 되겠죠. 즉 IObservable<T>를 구현하는 클래스는 말그대로 observable한(지켜볼 수 있는) 클래스를 말하는 거구요, IObserver<T>를 구현하는 클래스도 말 그대로 observer클래스(지켜보는)가 되는 거죠. 그럼, 위의 예제를 여기에 맞춰서 옮겨 볼까요~~~~! 그전에, 일단 다이어그램을 한번 보시져.

-그림3. 새로운 인터페이스를 활용한 다이어그램


enum BroadcastStatus
{
    StandBy = 0,
    OnAir = 1,
    End = 2
}

interface RadioBroadcaster : IObservable<string>//지켜볼 대상
{       
    void DoBroadcasting();
    void StartBroadcast();
}


RadioBroadcaster가 IObservable<string>을 상속하고 있죠? 지켜볼 대상이 되기 때문이죠. 그리고 string은 지켜보는 observer가 전달받는 데이터를 말하는데요, 방송국에서 string타입의 전파를 뿌렸으므로 string이 됩니다.

class KBRRadioBroadcaster : RadioBroadcaster//정보의 공급자
{
    private List<IObserver<string>> listeners = new List<IObserver<string>>();
    public BroadcastStatus broadcastStatus = BroadcastStatus.StandBy;

    private List<string> scripts = new List<string>
    {
        "안녕하십니까. KBR방송국의 김병만 기자입니다.",
        "요즘 살림살이 초큼 어찌 나아지셨슴까?",
        "안 나아지셨다구요? 그럼 이 방송에서 취업 필살전략을 알려드립니다.",
        "요즘같이 취업이 어려운때 3개 국어가 가능하면 취업 직빵이겠죠.",
        "따라 해보시져, Handle いぱい 꺽어.(핸들 이빠이 꺽어).",
        "3개국어 정말 쉽져? 이것만 한다면 당신도 취업계의 슈퍼스타!"
    };

    private int currentScriptIndex = 0;

    public void DoBroadcasting()
    {
        if (scripts.Count.Equals(currentScriptIndex))
        {
            Console.WriteLine("방송끗.");
            broadcastStatus = BroadcastStatus.End;
        }
        else
        {
            foreach (IObserver<string> listener in listeners)
            {
                listener.OnNext(scripts[currentScriptIndex]);
               
                // Assume that we've arrived at location of Latitude has changed by 4.
                if (broadcastStatus == BroadcastStatus.End)
                {
                    listener.OnCompleted();
                }
            }
            currentScriptIndex++;
        }
    }

    public void StartBroadcast()
    {
        broadcastStatus = BroadcastStatus.OnAir;
    }

    #region IObservable<Listener> Members

    public IDisposable Subscribe(IObserver<string> listener)
    {
        listeners.Add(listener);
        // Announce current location to new observer.
        Console.WriteLine("{0} : 수신감도 조쿠나. ㅎㅎㅎㅎ", (listener as Listener).Name );

        return listener as IDisposable;
    }

    #endregion

    public void UnSubscribe(IObserver<string> listener)
    {
        listeners.Remove(listener);
        Console.WriteLine("{0} : 아놔. 완전 재미없엉", (listener as Listener).Name);
    }
}


변화가 있다면, IObservable의 메서드인 Subscribe를 구현했다는 것과 QuitFrom이 이름을 맞추기 위해서 UnSubscribe로 바뀐건데요. Subscribe는 ListenTo가 그랬듯이 변화가 생기면, 그걸 알려달라고 등록하는 통로가 됩니다.

class Listener : IObserver<string>//지켜보는 행위를 하는 주체
{
    private string name;

    public string Name
    {
        get
        {
            return name;
        }
    }

    public Listener(string name)
    {
        this.name = name;
        Console.WriteLine("나는야 {0}. 라디오를 가진 남자지.", this.name);
    }

    #region IObserver<string> Members

    public void OnCompleted()
    {
        Console.WriteLine("{0} : 음 재밌네 ㅋㅋㅋㅋㅋㅋㅋ", name);
    }

    public void OnError(Exception error)
    {
        Console.WriteLine("{0} : 아놔 감도 완전 좌절이네.", this.name);
    }

    public void OnNext(string script)
    {
        Console.WriteLine("{0} : {1}", this.name, script);
    }

    #endregion
}


그리고 요건 청취자죠. 지켜봐야 하기 때문에 IObserver<string>을 구현한게 보이시죠? 그리고 인터페이스의 메서드를 구현했는데요. 각각 완료시에 할 동작을 담을 OnCompleted와 에러발생시에 처리할 OnError, 그리고 기존의 Listening과 같이 변화를 전달받을때 할 동작을 담을 OnNext로 나눠집니다.


class Program
{
    static void Main(string[] args)
    {
        Listener listener1 = new Listener("마논개");
        Listener listener2 = new Listener("코턱용");
        Listener listener3 = new Listener("송순신");

        KBRRadioBroadcaster kbrRadioBroadcaster = new KBRRadioBroadcaster();
        kbrRadioBroadcaster.Subscribe(listener1);//마논개 라디오 청취시작.
        int count = 0;
        do
        {
            if (count == 0)//방송시작
            {
                kbrRadioBroadcaster.StartBroadcast();
            }
            if (count == 2)//잠시후 코턱용 라디오 청취시작
            {
                kbrRadioBroadcaster.Subscribe(listener2);
            }
            if (count == 3)//잠시후
            {
                kbrRadioBroadcaster.UnSubscribe(listener1);//마논개 라디오 끔
                kbrRadioBroadcaster.Subscribe(listener3); //송순신 라디오 청취시작
            }
            kbrRadioBroadcaster.DoBroadcasting();
            count++;
            Thread.Sleep(500);
        } while (kbrRadioBroadcaster.broadcastStatus != BroadcastStatus.End);
    }
}


그리고 프로그램 코드죠. 동작은 위와 동일합니다. 결과를 보시죠.

-그림4. 동일한 결과


- 마무리.

의의를 찾자면, 프레임워크 레벨에서 Observer패턴을 지원하면서 좀 더 정돈된 형태로 구현을 할 수 있다는 것과 그렇게 구현을 했을때 나중에 프레임워크 레벨에서 성능 개선이 있던지 하면, 그런 혜택을 코드의 변경없이 누릴 수 있다는 점이 있겠구요. 또 한가지 점은 프레임워크나 다른 기술에서 내부적으로도 이 인터페이스를 활용하게 될거고, 그리고 새로운 형태의 프레임워크도 등장을 기대할 수 있다는 점일 것 같습니다.

실제로, 이 인터페이스를 통해서
Reactive Extensions for .NET(줄여서 Rx) 라는 프로젝트가 Devlab에 공개되었습니다. 스크린캐스트를 살짝 들여다보니, 흥미로운 응용이 가능하더군요. 가능하면 다음번에는 Rx를 가지고 이야기를 한번 해보고 싶네요. 그럼, 시력을 떨어뜨리는 글을 열심히 읽어주셔서 감사합니다. 날도 추운데 피드백은 따쓰하게... 아시져? ㅋㅋㅋㅋ 그리고! 소스는 첨부해드립니다.


- 참고자료

1. http://msdn.microsoft.com/en-us/library/dd990377(VS.100).aspx




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


 

티스토리 툴바