티스토리 뷰

UWP & Windows App

Reactive Extension part5

kaki104 2014. 9. 19. 14:18
반응형

Reactive Extension part5

 

페이스북 그룹에 등록한 part4 포스트에 반응이 뜨거워서, 월요일 저녁임에도 불구하고, part5를 포스트 작성을 바로 시작했다....로 시작한 포스트 작성이 금요일까지 이어지고 있다..쿨럭..오늘은 마무리를 해야징

 

페이스북 그룹 이야기를 나왔으니, 광고를 한번 하도록 하겠다.

 

윈도우 & 윈도우폰 앱 개발 모임

https://www.facebook.com/groups/vapps/

 

윈도우와 윈도우폰 앱 개발을 하는 개발자(다수), 기획(0), 디자이너(1-2)가 활동 중인 그룹으로 한달에 2번 오프라인 모임을 통해서 서로 개발에 관한 의견을 나누고, 저녁도 먹으며, 친목을 도모하고 있다.

자세한 사항은 http://vapps.github.io/ 이 곳을 참고한다.

 

 

"포스트에 광고 막 넣는 거야?"

 

"문제 없다. 내 포스트니"

 

"그래서 사기 광고 잖아?"

 

"ㅇㅇ?"

 

"모임에 언제 디자이너(1-2)가 참석 했지?"

 

"전에는 매주 왔다! 사진찍기 신공이 무서운 아이였지!! 하지만, 선택과 집중에서 앱 개발 모임이 밀렸을 뿐이다. 마음은 항상 참가하고 있다."

 

"그걸 어떻게 알아?"

 

"알고 있지..지구인이 아닌 것을.. 내 사진들은 다른 행성에 팔려 갔을지도.."

 

퍼퍼퍽!@#@#@

 

 

1. Bridging the asynchronous method pattern with Rx

 

Rx에서 웹 서비스를 어떻게 사용하는지 알아 보자. 먼저, part4에서 이야기 했듯이 웹 서비스를 이용한 간단한 사전 단어 제안 어플리케이션을 만들어 보도록 할 것인데, 여기서 사용하려는 서비스와 중요 정보 몇가지를 먼저 살펴 보자.

 

http://www.dict.org

 

이 사이트는 사전 검색을 하는 곳인데, 개발자가 이 사이트에 코드로 조회를 요청하거나, 사용하기가 어려운 어렵다. 그 이유는 http 프로토콜이 아닌 Dictionary Server Protocol (RFC 2229)을 사용하기 때문이다. 그래서 개발자가 쉽게 이 사이트를 이용하기 위해 아래의 http://services.aonaware.com 사이트에서 사전 서비스의 asmx 정보를 제공하고 있다.

 

 

http://services.aonaware.com/DictService/DictService.asmx

 

 

위의 이미지에 DictService에서 제공중인 웹 메소드 목록이 표시되는데, 그 중에서 MatchInDict를 사용할 것이기 때문에 어떤 파라메터를 입력해야하는지 확인하기 위해 클릭해본다.

 

 

dictId, word, strategy 3개의 파라메터가 존재하는데, dictId는 wn(WordNet 2.0), word는 검색할 단어 일부, strategy는 prefix를 사용하도록 한다.

 

내용 확인이 끝났으니, 사전 서비스를 프로젝트에 추가하도록 한다.

 

 

Add Service Reference...을 선택한다.

 

 

위와 같이 입력하고, Advanced 버튼을 눌러서 내용을 확인해 본다. Visual Studio 2013에서는 task-base가 기본으로 선택되어 있음으로 pdf에서 async를 선택하라는 내용은 무시해도 된다.

 

몇 가지 웹 메소드에 대해서 간단히 살펴 보면 MatchInDict는 사용자가 지정한 사전에서 입력한 단어로 찾아주는 서비스이고, DictionaryList는 사전 목록 조회, StrategyList는 검색어 찾는 방법에 대한 설정 값 목록을 조회할 수 있다.

 

정상적으로 서비스가 추가가 되었다면, Service References에 DictionarySuggestService가 추가되어 있을 것이다. 이 서비스를 사용하기 위해 program.cs 파일에 static 프로퍼티로 아래 내용을 추가해 준다.

 

        static readonly DictServiceSoapClient Service = new DictServiceSoapClient("DictServiceSoap");

 

이제 사용할 준비가 되었으니, 서비스가 정상적으로 동작하는지 확인을 위한 테스트 코드를 아래와 같이 추가하고 실행해보자

 

            var inputSubscription = input
                                    .ObserveOn(SynchronizationContext.Current)
                                    .Subscribe(async data =>
                {
                    Console.WriteLine(data);
                    lbl2.Text = data;

                    var result = await Service.MatchInDictAsync("wn", data, "prefix");
                    if (result != null)
                    {
                        Console.WriteLine("Results of '{0}'", data);
                        foreach (var dictionaryWord in result)
                        {
                            Console.WriteLine(dictionaryWord.Word);
                        }
                    }
                });

 

Service.MatchInDictAsync라는 메소드가 자동으로 생성된 비동기 메소드이다.

 

 

 

2. SelectMany : the Zen of composition

 

위에서 사용자가 입력한 단어로 시작하는 여러 단어를 반환하는 사전 서비스를 테스트 했다.

 

웹 서비스에는 한가지 이슈가 있는데, 내가 호출한 순서대로 결과를 반환해 주지 않는다는 것이다. 즉, 내가 rea -> reac -> react -> reacti -> reactiv -> reactive ...순서로 웹 서비스를 여러번 비동기 호출했을 때, 결과가 동일한 순서로 도착하는 것이 아니라, 뒤죽 박죽 순서로 돌아온다는 것이다. 그렇기 때문에 내가 rea에 대한 호출을 해서 처음으로 반환되는 결과가 rea에 대한 결과라는 것을 확신할 수도, 마지막에 호출한 reactive의 결과가 제일 마지막에 들어온다는 보장이 없다. (pdf 31page 참조)

 

물론 async, awiat 환경에서는 하나의 비동기 메소드에서 호출하고, 결과가 올때까지 대기를 하고 있다가 처리할 수 있다. 그렇다면, Rx에서 이 이슈를 어떻게 해결하는 알아 보도록 하자.

 

Rx에 SelectMany라는 오퍼레이터는 2개의 서로다른 observable sequence를 연결하는 기능을 가지고 있다. 아래 인텔리 센스에 나오는 설명을 확인하도록 하자.

 

 

* 위의 화면은 Resharper(3rd party tool)에서 제공해 주는 화면으로 기본 화면과는 약간 다름

 

Projects each element of the source observable sequence to the other observable sequence and merges the resulting observable sequences into one observable sequence.

 

원본 observable sequence와 다른 observable sequence를 merge해서 하나의 결과 observable sequence로 만들어 준다는 내용이다. 다시 설명하면, 내가 입력한 문자열과 그 문자열을 이용해서 호출한 웹 서비스를 하나로 연결하는 새로운 observable sequence를 만들 수 있다는 것이다.

 

먼저, 화면에 리스트 박스를 추가한다.

 

            var lbl = new Label();
            var lbl1 = new Label{ Left = 100};
            var txt = new TextBox{ Top = 30, Width = 100, Height = 30 };
            var lst = new ListBox { Top = txt.Top + txt.Height + 10, Height = 200};
            var frm = new Form
            {
                Controls = { lbl, lbl1, txt, lst }
            };

 

* lbl2를 삭제했다. 소스코드 아래에 있는 lbl2.Text = data 부분은 주석/삭제를 한다.

 

            var input = (from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
                        select ((TextBox) evtPattern.Sender).Text)
                        .Throttle(TimeSpan.FromSeconds(0.2))
                        .DistinctUntilChanged()
                        .Do(x => Debug.WriteLine("Start web service of " + x));

 

위에 Do를 추가해서 push가 발생하는 문자열이 어떤 것인지 확인 할 수 있도록 한다.

 

* pdf에 나오는 FromAsyncPattern 오퍼레이터는 더이상 지원하지 않는다.

 

이제 사용자가 입력한 문자열과 그 문자열을 이용해 조회한 웹 서비스를 서로 연결하는 작업을 한다.

 

            var res = (from inp in input
                from words in Service.MatchInDictAsync("wn", inp, "prefix")
                select new {KeyWord = inp, Result = words});

 

            res.Subscribe(results =>
            {

                Debug.WriteLine("Start results of " + results.KeyWord);
                foreach (var result in results)
                {
                    Debug.WriteLine(result.Word);
                }
                Debug.WriteLine("End results of " + results.KeyWord);
            });

 

이 코드의 결과가 정말 입력값과 웹 서비스의 결과가 연결되어서 제대로 나올까?? 궁금하면 실행해서 확인해본다.

 

Start ... rea        
  Start ... react      
    Start ... reacti    
Start results of rea        
Rea Silvia        
…..        
readapt        
End results of rea        
  Start results of react      
  react      
  …..      
  reactor      
  End results of react      
      Start ... reactiv  
    Start results of reacti    
    reaction    
    …..    
    reactive schizophrenia    
        Start ... reactive
    reactivity    
    End results of reacti    
        Start results of reactive
        reactive
        reactive depression
        reactive schizophrenia
        End results of reactive
      Start results of reactiv  
      reactivate  
      reactive  
      reactive depression  
      reactive schizophrenia  
      reactivity  
      End results of reactiv  

 

 

출력된 결과를 보기 쉽게 입력한 문자와 출력된 결과를 같은 컬럼에 배치했고, 아래의 도식화된 내용 처럼 서로 SelectMany로 연결되어 있다는 것을 확인 할 수 있다.

 

 

...마음에 드는 가?

 

 

"별로"

 

"뭐가 문젠가?"

 

"그냥 비동기 메소드에서 웹 서비스 호출하고, await로 기다리고, 도착하면 처리하는 것이랑 차이가 없는 거잖아?"

 

"결과적으로는 그렇치만,..음...음..."

 

"뭐? 왜그래?"

 

"너무 좋은데 딱히 표현할 방법이 없다!"

 

"컹...." 퍽퍽 퍼퍼벅

 

 

잠시 쉬어가는 의미로 다루지 않고 넘어갔던 부분에 대해 알아보고 처리하는 코드를 추가하자.

 

바로 웹 서비스의 오류 발생 상황이다. 웹 서비스의 특성상 네트워크의 문제가 발생하거나, 서비스 자체의 오류가 발생하면, 프로그램이 중지 되기 때문에, 반드시 오류 처리를 하고 넘어가야 한다. 또한, 지금 사용중인 서비스는 한 글자를 조회 요청하면, 서비스 오류를 발생 시키는데, 이 오류를 방지하기 위해 최소한 2글자 이상이 되도록 수정한다.

 

            var input = (from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
                        select ((TextBox) evtPattern.Sender).Text)
                        .Throttle(TimeSpan.FromSeconds(0.5))
                        .DistinctUntilChanged()
                        .Where(x => x.Length >= 3)
                        .Do(x => Debug.WriteLine("Start web service of " + x));

 

* Where 문이 저 곳이 아닌 다른 곳에 위치하면 약간 다른 결과가 나올 수 있다. 테스트 해보자

 

            res.Subscribe(results =>
            {
                Debug.WriteLine("Start results of " + results.KeyWord);
                foreach (var result in results.Result)
                {
                    Debug.WriteLine(result.Word);
                }
                Debug.WriteLine("End results of " + results.KeyWord);
            }, ex => Debug.WriteLine("Exception of web service : " + ex.Message));

 

출력결과가 정상적일 것이라고 생각한다. 다시 실행해서 결과 확인을 하는 것은 각자 하도록 하고, 이제 리스트박스에 결과를 출력 하도록 수정해 보자.

 

            res.ObserveOn(SynchronizationContext.Current)
                .Subscribe(results =>
            {
                lst.Items.Clear();
                lst.Items.AddRange(results.Result.Select(p => p.Word).ToArray());
            }, ex => Debug.WriteLine("Exception of web service : " + ex.Message));

 

실행 결과 아래와 같이 입력한 글자에 대한 유사 단어들의 목록을 리스트 박스에서 볼 수 있다.

 

 

 

휴~ 숨한번 돌리고, 이제 마지막 끝 판왕만 남았다.

 

방금 SelectMany를 이용해서 입력 값과 출력 값을 merge 시켜서 하나의 observable sequence를 만드는데 성공을 했다. 하지만, 여기에도 문제가 존재하는데....

 

rea -> react -> reacti -> reactiv -> reactive 순으로 입력을 진행하고 서비스를 호출했는데, 결과가 도착한 순서로 보자면 rea -> react -> reacti -> reactive -> reactiv 이다. 이렇게 도착 순서가 뒤바뀐 경우 리스트에 출력되는 내용은 어떻게 되는 것일까?

 

 

위의 일러스트에서 보듯이 reactive의 결과가 리스트에 출력이 된 후에 react가 도착이 되면, react의 결과가 리스트에 출력이 되기 때문에 오류 상황이 되는 것이다.

 

혹시 이러한 이슈를 해결하는 방법에 대해서 알고 있는 것이 있나? 아마도 일러스트에 나온 내용일 것이라고 생각한다.

 

 

일러스트의 내용으로 보면 react에 대한 웹 서비스 조회를 하는 중에 reactive를 다시 조회 할 때, 조회 중인 웹 서비스를 취소해서, 결과가 반환되지 못하도록 하는 것이다. 이렇게 처리하는 것을 "crossing out" 혹은 "muting"이라고 한다.

 

위에서 이야기한, 호출 중인 서비스를 취소하는 작업은 SelectMany의 특징 중 하나인 cancellation을 이용하면 쉽게 처리할 수 있다. 이 기능은 source가 웹 서비스를 호출 하고, 결과를 수신하기 전에, 새로운 키워드가 입력이 되어서 웹 서비스를 호출해야 한다면, 그 전에 존재하던 수신 대기 observable을 unsubscribe을 하게 된다. 마치 아래 일러스트 처럼 송유관에 벨브에 비유하기도 하는데, source는 웹 서비스 호출하는 부분이고, 그 뒤에 TakeUntil(value)가 붙어서 value에 값이 채워지기 전까지..라는 의미가 된다.

 

코드를 수정해 보자.

 

            var res = (from inp in input
                from words in Service.MatchInDictAsync("wn", inp, "prefix")
                            .ToObservable()
                            .Finally(() => Debug.WriteLine("Disposed request for " + inp))
                            .TakeUntil(input)
                select new {KeyWord = inp, Result = words});

 

Service.MatchInDictAsync 메소드는 observable sequence가 아니기 때문에 ToObservable() 메소드를 이용해서 observable로 변경해 주고, unsubscirbe가되는지 확인을 하기 위해 Finally를 추가하고, 마지막으로 TakeUntil을 사용한다. 실행 후 결과를 확인해 보자

 

Start web service of reacti
Start web service of reacti
Start web service of reactiv
Start web service of reactiv
Disposed request for reacti
Start web service of reactive
Disposed request for reactiv
Start web service of reactive
Disposed request for reactive
Start results of reactive
reactive
reactive depression
reactive schizophrenia
End results of reactive

 

Debug창에 출력된 log로, 새로운 단어로 웹 서비스를 호출하기 전에 호출 중인 웹 서비스를 unsubscribe하는 것을 확인 할 수 있다. 그런데, 약간 눈에 거슬리는 것이 있는데, 동일한 문장이 보이는 것이다. 중복 문장은 Do로 출력한 Start web service of xxx라는 내용인데, 그 이유는, SelectMany와 TakeUntil 2개의 Subscribe이 존재하기 때문이다.

 

* pdf에 Switch에 대한 사항은 소스에서 Switch를 사용할 수 없어서 일단 제외 한다. Switch가 제거 된 것은 아닌데, 사용하지 못하는 이유는 찾게되면 다시 포스트를 작성하도록 하겠다.

 

 

지금까지 part2~part5까지 4회에 걸쳐서 Rx HOL .NET.pdf의 내용을 Visua Studio 2013와 Rx 2.2.x 버전으로 구현하고, 설명하였다. 이제 Rx에 대한 사용법에 대해서 약간은 느낌이 왔을 것이라고 생각하고, 다음에도 더 좋은 자료가 있으면 포스트를 하도록 하겠다.

 

 

이제 part5 포스트가 일주일이나 걸린 이유를 공개한다. 짜잔~

 

*********************************************************************************

 

pdf에 나온 것과 같은 서비스를 이용해서 작업을 진행할려고 했는데, 갑자기 서비스에 문제가 발생한 것을 알게되었다. 그래서, 부득이하게 다른 예를 이용하는 포스트를 작성 한다. 순식간에 내용이 끊어지고 다른 형태로 한다고 만이 놀랐을 것 같다. 나도 셈플 프로젝트를 만드는데, 서비스 오류가 발생해서, 내가 뭘 잘 못했는 줄 알고, 이것 저것 찾아본다고 고생했다. 혹시라도, 포스트 작성 중 서비스가 다시 정상으로 돌아오면 그 때 계속하도록 하겠다.

 

9월 18일 오후 정말 파란 만장하다고 해야하나;; 새로운 프로젝트 만들어서 열심히 이것 저것 해보고 있었는데 지금 서비스가 정상화 되었다. 하하하..그렇다면 다시 pdf내용으로 작성한다.

 

*********************************************************************************

 

 

2. 새로운 프로젝트로..

 

pdf를 따라하지 못하니 새로운 유니버셜 프로젝트를 만들어서 작업을 진행하도록 하자.

 

포스트 작성 중에 한통의 메일을 확인 했다. 두달전에 물에 빠진 루미아 925를 홍콩으로 a/s를 보냈었는데, 회생 불능 판정을 받았다는 내용이였다. 모든 기능은 정상적으로 동작하는 듯 했는데..(컴퓨터와 연결하면 동기화까지도 했었는데..) lcd에 아무것도 표시가 되지 않는 것으로 보아서, lcd만 갈아 끼우면 될 것 같았는데... 앞으로 윈폰8을 언제 다시 만질 수 있을지..그래서, 이번에는 루미아 925를 추모(?)하는 마음에서 윈폰에서 작업을 하도록 하겠다.

 

새로운 Blank 유니버셜앱 프로젝트를 생성 후 Nuget package에서 rx를 선택해서 Windows store app, Windows Phone app 2개의 프로젝트에 모두 추가하도록 한다.

 

윈도우 폰 프로젝트를 선택 후 Set as Start up Project를 선택해서 시작 프로젝트로 만들고, MainPage.xaml에 다음과 같이 코딩한다.

 

2.1 MainPage.xaml

 

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="70"/>
            <RowDefinition Height="60"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>

 

        <TextBlock Text="Rx part5" FontSize="80"/>

        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <TextBlock Text="Search text :" FontSize="30" VerticalAlignment="Center"/>
            <TextBox x:Name="tb" MinWidth="100"/>
        </StackPanel>
       
        <WebView x:Name="wv" Grid.Row="2" Source="
http://kaki104.tistory.com/m"/>
    </Grid>

 

실행 결과

 

 

 

2.2 MainPage.xaml.cs

 

        public MainPage()
        {
            this.InitializeComponent();

            this.NavigationCacheMode = NavigationCacheMode.Required;

 

      tb.Text = string.Empty;

            var input = (from evtPattern in Observable.FromEventPattern<TextChangedEventArgs>(tb, "TextChanged")
                         select ((TextBox)evtPattern.Sender).Text)
                        .Throttle(TimeSpan.FromSeconds(1))
                        .DistinctUntilChanged();

 

            input.Subscribe(
                txt => Debug.WriteLine("Input text is {0}", txt));

        }

 

part4에서 사용했던 TextChanged 이벤트를 이용하는 부분을 복사해서 붙여 넣은 후 입력 문자를 debug 창에 출력되도록 수정하였다. 추가로 EventArgs도 TextChangedEventArgs로 수정 했다.

 

Input text is react
Input text is reactive

 

정상적으로 결과가 되는 것을 확인했다.

이제 검색어를 입력하면 wikipedia에서 검색을 하여 결과를 화면에 출력하도록 소스를 수정해보자. 

 

            input.ObserveOn(SynchronizationContext.Current)
                .Subscribe(
                inputText =>
                {
                    Debug.WriteLine("Search text : {0}", inputText);

                    wv.Navigate(new Uri("http://en.wikipedia.org/wiki/" + inputText));
                });

 

실행 후 reactive를 입력하면 아래와 같은 결과가 표시 된다.

 

 

 

.ObserveOn(SynchronizationContext.Current)

part4에서 ObserveOn에 사용되는 IScheduler가 조금씩 다르다고 했는데, 이전에 사용하던 스케줄러 중에 몇가지가 사용을 할 수 없도록 삭제되어 있었다. 그래서 정확하게 어떤 스케줄러를 사용해야하는지 찾아보았지만, 검색에 실패! 결국 part4에서 사용한 것을 그대로 사용하기로 했다. 추후에 더 정확한 스케줄러를 찾게 되면 수정 하도록 하겠다.

 

어떻게 생각하면 각 플랫폼마다 여러가지 스케줄러를 혼용함으로 인해 발생하는 복잡함을 감소하기 위해서 SynchronizationContext.Current로 대동 단결 하도록 만들었는지도 모르겠다. 과거 버전에 있던 여러가지 스케줄러를 왜 삭제했는지는 히스토리를 찾아보거나, Rx 팀에 문의를 해보아야 정확하게 알 수 있을 것 같다.

 

...

 

 

"음? 왜 갑자기 끝난거야?"

 

"너무 길면, 읽기 싫어지고, 손가락이 아프다."

 

"힘들어서가 아니고?"

 

"험험..그럼 이만.." =3 =3 =3

 

 

 

9. End

 

여기까지가 웹 서비스가 고장이난 기간 동안 작성한 내용이다. 정신 없기는 하지만, 2개의 내용 모두를 그대로 포함하기로 하고, 소스도 모두 포함한다. 페이스북에 좋아요 눌러주신 한별님과 영재님 큐~

 

콘솔작업 프로젝 소스

 

RxSample.Part5.Console.zip
다운로드

 

 

유니버셜 프로젝 소스

 

RxSample.Part5.Universal.zip
다운로드

 

 

 

 

반응형

'UWP & Windows App' 카테고리의 다른 글

ReactiveUI part2  (0) 2014.09.30
ReactiveUI part1  (0) 2014.09.29
Reactive Extension part4  (0) 2014.09.14
Reactive Extension part 3  (2) 2014.09.13
Reactive Extension part2  (0) 2014.08.25
댓글