반응형

Dart 의 비동기/동기(async/ sync programming) 표현 방법은 상당히 직관적이면서도 편리한 부분들이 많습니다.

효율적인 동작을 위한 코딩을 하려다 보면 필연적으로 async로 코드 작성이 필요로 하게 되죠.

 

아래 간단한 주제를 가지고 한번 얘기 해보고자 합니다.

 

A라는 데이타 목록을 가지고 B라는 과정을 거쳐서 C라는 결과를 도출하고 D라는 데이타 목록으로 저장을 하려고 합니다.
1. A의 각 항목들은 B라는 과정을 거칠때 1~2 초 정도 시간이 걸린다. 
2. B 과정을 입력 A 데이타에 따라 처리 시간이 달라진다.(1초 또는 2초)
3. D에 저장 되는 순서는 A의 순서와 같아야 한다.

일단 A항목들을 순차적으로 처리하게 되면, A의 개수만큼 시간이 늘어나게 됩니다.

따라서 비동기(async)프로그래밍를 고려하게 될 것입니다.

 

위의 상황에 대해서 코딩을 해보면,  가장먼저 다음과 같이 생각해볼 수 있을 것입니다.

A = srcData, B =Future.delayed() , C =  A와 같은 type , D = dstData 라고 합시다.

import 'dart:async';
import 'dart:math';


List<String> srcData=[];
List<String> dstData=[];

init(){
  for( int i = 0;i<24; i++ ){
    srcData.add('mmm_${i}');
  }
}

Future copyToDst(int i){// test를 위해서 1~2초의 랜덤 수행 순서로, src에서 dstData 에 copy 하는 내용.
  return Future.delayed(Duration(seconds: Random().nextInt(2)),(){
    dstData.add(srcData[i]);
    print('src[$i]=>dst');
  });
}



fillDest() async{
  init();
  print("fillDest: length=${srcData.length}");
  for(int s = 0; s<srcData.length ;s++){
    copyToDst(s);
  }
}

void main() async{
 await fillDest();
 await Future.delayed(Duration(seconds: 5));
 print(dstData);
  
}

1초 또는 2초에 처리가 완료 되면, dstData에 저장을 하게 됩니다.

실행 결과는 아래와 같습니다.

기본적인 동작은 하는것 처럼 보입니다.

 

<실행 결과>

fillDest: length=24
src[3]=>dst
src[4]=>dst
src[5]=>dst
src[8]=>dst
src[10]=>dst
src[11]=>dst
src[15]=>dst
src[16]=>dst
src[20]=>dst
src[21]=>dst
src[22]=>dst
src[0]=>dst
src[1]=>dst
src[2]=>dst
src[6]=>dst
src[7]=>dst
src[9]=>dst
src[12]=>dst
src[13]=>dst
src[14]=>dst
src[17]=>dst
src[18]=>dst
src[19]=>dst
src[23]=>dst
[mmm_3, mmm_4, mmm_5, mmm_8, mmm_10, mmm_11, mmm_15, mmm_16, mmm_20, mmm_21, mmm_22, mmm_0, mmm_1, mmm_2, mmm_6, mmm_7, mmm_9, mmm_12, mmm_13, mmm_14, mmm_17, mmm_18, mmm_19, mmm_23]

 

음... 그런데  결과에서 보듯이 "mmm_숫자" 결과의 순서가 srcData의 순서와 맞지 않는 것을 볼수 있습니다.

 

이제 여기서 몇가지 고민을 하게 되겠죠? 가령 "어떻게 순서를 맞출 것인가? " 와 같은 것 말이죠.

대략 한 3가지 방법을 생각해 볼 수 있을것 같네요..

 

첫번째,  copyToDest를  await 으로 처리하는 방법입니다.

  for(int s = 0; s<srcData.length ;s++){
    await copyToDst(s);
  }

 직접 실행해보면, .. 으악... 이런 최악의 수네요..ㅠ_ㅠ 실행시간이 너무 오래걸려요.

 

 

두번째,  map을 이용하는 방법이 있겠네요. 

map을 이용해서 srcData의 순서를 map의  index로 사용하는 방법이죠.

import 'dart:async';
import 'dart:math';


List<String> srcData=[];
Map<int, String> dstData={};

init(){
  for( int i = 0;i<24; i++ ){
    srcData.add('mmm_${i}');
  }
}

Future copyToDst(int i){// test를 위해서 1~2초의 랜덤 수행 순서로, src에서 dstData 에 copy 하는 내용.
  return Future.delayed(Duration(seconds: Random().nextInt(2)),(){
    dstData[i] = srcData[i];
    print('src[$i]=>dst');
  });
}



fillDest() async{
  init();
  print("fillDest: length=${srcData.length}");
  for(int s = 0; s<srcData.length ;s++){
     copyToDst(s);
  }
}

void main() async{
 await fillDest();
 await Future.delayed(Duration(seconds: 5));
 print(dstData);
  
}

 

<실행결과>

fillDest: length=24
src[0]=>dst
src[1]=>dst
src[2]=>dst
src[3]=>dst
src[5]=>dst
src[6]=>dst
src[7]=>dst
src[11]=>dst
src[12]=>dst
src[16]=>dst
src[18]=>dst
src[21]=>dst
src[22]=>dst
src[4]=>dst
src[8]=>dst
src[9]=>dst
src[10]=>dst
src[13]=>dst
src[14]=>dst
src[15]=>dst
src[17]=>dst
src[19]=>dst
src[20]=>dst
src[23]=>dst
{0: mmm_0, 1: mmm_1, 2: mmm_2, 3: mmm_3, 5: mmm_5, 6: mmm_6, 7: mmm_7, 11: mmm_11, 12: mmm_12, 16: mmm_16, 18: mmm_18, 21: mmm_21, 22: mmm_22, 4: mmm_4, 8: mmm_8, 9: mmm_9, 10: mmm_10, 13: mmm_13, 14: mmm_14, 15: mmm_15, 17: mmm_17, 19: mmm_19, 20: mmm_20, 23: mmm_23}

실행결과를 보시면 데이타의 순서는 처리속도에 따라 순서가 달라졌지만, 우리는 map의 index를 사용할 수 있기 때문에 index 순서로 결과를 뽑아볼수 있습니다. ^^ 매우 좋은 결과네요. 

속도도 거의 차이가 없고, 결과도 좋고요.

한가지 걸리는 것은 , dstData를 얻기까지 1~2초가 걸리는데, data가 정상적으로 들어왔는지, 언제 사용해야 하는지가 약간은 애매하네요.

 

 

 

세번째, dstData를 Future List로 변경하는 방법인데요. 

dstData 의 형태를 List<String> 에서 List<Future<String>> 변경하고 copyToDst의 형태도 dstData에 Future<String>을 추가하도록 변경되었습니다.

이렇게 하면 dstData 에는 실행 결과가 들어가는 것이 아니라 실행 명령 순서인 Future<String> 을 저장하는 것을 의도 합니다.

import 'dart:async';
import 'dart:math';


List<String> srcData=[];
List<Future<String>> dstData=[];

init(){
  for( int i = 0;i<24; i++ ){
    srcData.add('mmm_${i}');
  }
}


Future copyToDst2(int i)async{// test를 위해서 1~2초의 랜덤 수행 순서로, src에서 dstData 에 copy 하는 내용.
  return dstData.add(Future<String>.delayed(Duration(seconds: Random().nextInt(2)),(){
    print('src[$i]=>dst');
    return srcData[i];
  }));
}


fillDest() async{
  init();
  print("fillDest: length=${srcData.length}");
  for(int s = 0; s<srcData.length ;s++){
    copyToDst2(s);
  }
}

void main() async{
 await fillDest();
 await Future.delayed(Duration(seconds: 5));
  
  
 List<String> l=[];
 for (var d in dstData){
   l.add(await d);
 }
  
  
 print(l);
  
}

 

추후 Future<String>의 결과를 얻기 위해서는 await dstData[i] 형태로 사용하면 String을 얻어올 수 있게 됩니다.

 

<실행 결과>

fillDest: length=24
src[0]=>dst
src[1]=>dst
src[2]=>dst
src[3]=>dst
src[5]=>dst
src[6]=>dst
src[7]=>dst
src[10]=>dst
src[11]=>dst
src[12]=>dst
src[16]=>dst
src[20]=>dst
src[23]=>dst
src[4]=>dst
src[8]=>dst
src[9]=>dst
src[13]=>dst
src[14]=>dst
src[15]=>dst
src[17]=>dst
src[18]=>dst
src[19]=>dst
src[21]=>dst
src[22]=>dst
[mmm_0, mmm_1, mmm_2, mmm_3, mmm_4, mmm_5, mmm_6, mmm_7, mmm_8, mmm_9, mmm_10, mmm_11, mmm_12, mmm_13, mmm_14, mmm_15, mmm_16, mmm_17, mmm_18, mmm_19, mmm_20, mmm_21, mmm_22, mmm_23]

 

 

추가로, 

map을 사용했을때 " dstData 언제 사용해야 하는지가 약간은 애매하네요." 라고 했던 부분에 대해서 추가로 생각해보자면,

List<Future<String>> 을 사용했던것 처럼, map<int , Future<String>> 으로 사용하게 되면, 필요한 데이타를 사용할때 await을 활용하여 data 결과가 채워지기를 기다릴 수 있겠는데요 ? 

 

아무튼 async 프로그래밍을 다루기에 편리한 언어인 dart 인데 이를 활용 안할 이유가 없을것 같죠? 그렇죠?

 

다들 dart의  async, async* sync, sync* 에 대해서 찾아보시고 알아두시면 즐거운 코딩 생활에 도움이 될것 같습니다.

 

 

반응형

좋은 언어와 좋은 프래임워크란 ?

언어와 framework의 발전사가 이렇다고 하고, 그러면 제목에서 적은 좋은 언어와 좋은 프래임워크란 무엇일까요?

 

가장 기본적인 기준이 되는 것이 2가지 일것 입니다.

첫번째로 프래임워크에서 제공하는 사용성과 정책이 프로그래밍 언어에서 제공하는 것과 일관되어야 합니다.

두번째는 일반적일 것 같은 동작은 그렇게 동작하도록 해야 합니다.

 

위에서 예기한 것들을 좀 살펴봅시다.

우선 첫 번째 "프래임워크와 언어의 일관성"에 대한  예를 들면, 

프로그래밍 언어에서  var A =  a; 라는 코드가 A에 a를 copy하는 것이라고 정의하고 있다면,  이 언어로 작성된 프래임워크의 MyObject obj = OldObj; 의 코드 역시 obj 에 OldObj의 내용들을 copy하는 것이어야 합니다. 그런데 만약 여기서, obj에 copy 하는 것이 아니라 OldObj의 내용을 가리키도록 동작하도록 작성되었다고 한다면,

obj의 내용을 변경했을때 왜 OldObj의 내용이 바뀌는지등에 대한 의문을 만들게 됩니다.

또는 실제 obj가 쓰일때 copy를 한다거나, obj.sync() 를 불렀을때 실제 복사가 일어난다거나 한다면, 사용자 입장에서 '=' 가 불렸을때 copy가 될 것을 기대하고 퍼포먼스를 높이기 위해 튜닝을 했는데, 실제로는 다른 timing에서 data가 copy 되기 때문에 제대로 된 튜닝을 못하게 되는 상황이 발생합니다.

이는 사용자에게  framework에서 제공하는 "=" 동작에 대해 혼란을 주는 결과가 됩니다.

이는 실수를 유발하거나 직관성을 떨어뜨리게 됩니다.

 

두번째 "일반적일것 같은 동작" 대한 예를 들어보자면,

ImageViewer class 가 있고, ImageViewer에는 Draw()라는 함수가 있습니다. 이 Draw() 라는 함수는 ImageViewer의 image를 화면에 그려주는 기능을 하는 함수입니다.

Draw()기능을 좀더 세밀하게 정의 해보자면, 

1. Draw()는 ImageViewer가 들고있는 display용 context에 image를 drawing하는 함수라고 합시다.

2. Draw()가 불렸을때 화면에 보이지는 않고 framework의 rendering 시점에 이 display context의 내용이 화면에 출력되는 방식입니다.

그런데 이 class 개발자가 Draw()가 불렸을때 image를 직접 display context에 drawing하는 것보다, rendering 시점에 display context에 drawing 하도록 하는것이 성능상 이득이라고 판단하여, Draw()함수가 나중에 drawing이 필요하다는 flag만 설정하고 나오는 것으로 정의했습니다.

Draw() 함수에 대한 정의가 좋은 방향으로 결정 된것 일까요? 옳다 그르다의 표현은 좋지 못하고, API의 직관성을 흐리는 방향으로 간것 같다는 표현이 맞죠.

 

만약 위와 같은 경우에 사용자는 ImagViewer의 Draw()호출한 다음에 Display context에서 draw된 image를 꺼내와서, 그위에 text를 더 그리려는 작업을 하려 했다면, 전혀 엉뚱한 결과를 얻게 될것입니다.

여기에서 과연 Draw()라는 함수이름에서 보여주는 직관적인 동작은 무엇인가요? 바로 drawing입니다.

즉, 만약 ImageViewer의 성능을 높이기 위해서 flag 설정을 하여 Lazy update을 구현 할 생각이었다면, 다른 이름을 사용하는게 옳습니다.

많은 방법이 있죠. requestDraw나, Invalidate(windows에서 많이 사용하는 용어죠), SetRenderingTimeDrawEnabled() 등 API이름에서 해당 목적이 명확하게 나오도록 하는 것이 더 낳았을것 입니다.

 

framework은 직관성을 유지 하면서, 일반적인으로 예상할 수 있는 동작을 기본 동작으로 하고, 다양한 옵션을 제공하는 방향으로 되어야 한다고 생각합니다.

 

다음과 같은 항목들이 대표적인 사항들일 것입니다.

- Sync call와 Async call의 구분 

- lazy update와 immediate update 

- resource 소유권에 대한 규칙 

 

이와 같은 부분들이 아마도 platform에서 일관되게 동작하도록 정의하고 정리하기 어려운 부분일 것입니다.

 

특히 Resource 라고 표현한 객체의 lifecycle 관리 즉, 객체를 생성한 주체와 삭제하는 주체에 대한 것은 아주 자주 접하는 케이스이기 때문에, 명확히 하고 일반적으로 방법(개발자들이 흔히 예상할 수 있는 )으로 제공하는 것이 맞습니다.

 

 

아주 특별하고 획기적인 기능이라 하더라도, 그게 일반적인 상식이나 생각에서 벗어난다면, 좋지 못하겠죠.

 

몇가지 예를 들어보겠습니다.

 

DDD::CreateObj()

{

   ABC* obj = new ABC;

   Dock.Set(obj);

}

 

라는 아주 간단한 code가 있다고 합시다. 이 경우 obj는 누가 관리하는 것이 맞을까요?

1. DDD class에서 해야한다.

2. Dock에서 하는 것이 맞다.

3. 가비지 컬랙터(Garbage Collector)에 맡긴다.

 

 

또 다른 예를 들어보자면, array와 list가 있다고 합시다.

C++로  SoCool이라는 프래임워크 를 개발하고 있는 탐 이라는 사람이 있다고 합시다.

탐은 Array 와 List 를 만들때, "왜 다른 언어들은 index를 0 부터 시작하지? 그래서 개수가 10개 이면 0~9까지 카운트 되게 했지? 이건 일반인들은 상상도 할 수 없는 일이야" 라고 생각했습니다. 그래서 array의 index를 0이 아닌 1부터 시작하도록 했습니다.

어떻게 생각하시나요?

 

이 생각을 바탕으로 하나씩 짚어봅시다.

 

Array의 index는 프로그래밍 세계에서는  일반적으로 0부터 시작하는 반 열린 집합 형태로 표기 됩니다.

즉 개수가 10개인 array는 0<= index<10 으로 표현하는 것이 일반적인 표현 방식이죠. 이유는 memory와 관련 있는데, array 의 변수명은 그 변수가  잡고있는 memory의 시작 위치를 표시합니다. 따라서 Array[0]의 표시는 address + 0의 위치 시작 위치로 표시되는 것이죠.

이를 실생활에서 처럼 첫번째 item이니까 Array[1] 로 표현 한다면, address + 1 -1 과 보정을 하거나 아니면, 메모리상 address +1  위치 부터 data를 사용하도록 해야 합니다. 

이 두가지 모두 Computer 구조랑 잘 맞지 않는 표현 방법이 되죠.

(생각하기 나름이긴 하지만 시스템 이나 메모리구조 등을 이해하고 있는 많은 프로그래머들의 일반적인 생각은 이렇다는 것입니다.)

 

이러한 이유에 의해서 인지는 모르지만, 결과적으로 보면, 아래와 같은 의미가 되었습니다.

개념 상 뭐 어떻게 생각하느냐에 따라 다를 수 있습니다. 하지만 수학의 집합 형태로 생각 해봅시다.

우선 수학에서 어떤 집합을 표현 할때 열린 집합, 닫힌 집합, 반 열린 집합 등의 명칭으로 분류를 하기도 합니다.

무슨 의미냐 하면, 1 부터 9까지의 숫자들의 모음(1과 9을 포함하는)을 위에서 예기한 집합으로 표현하자면 다음과 같습니다.

1. [열린집합] 0<집합<10   즉, 경계인 0을 포함하지 않고, 10을 포함하지 않는 집합.

2. [닫힌집합] 1<=집합<=9  즉, 경계인 1을 포함하고 , 9는 포함하는 집합.

3. [반 열린 집합] 1<=집합<10  즉, 경계인 1을 포함하고 , 10을 포함하지 않는 집합.

4. [반 열린 집합] 0<집합<=9   즉, 경계인 0을 포함하지 않고 , 9는 포함하는 집합.

 

Computer programming 에서는 memory addressing 방식 때문에 자연스럽게 3번 형태를 띄게 되었습니다.

Computer에서는 시작을 1부터가 아닌 0부터 시작하고 있기 때문에, 10개 라는 의미가 0~9까지를 의미하는 것이 되었기 때문이죠.

 
탐은 array에 대해서는 이해를 했고, 리스트는 고치고 싶었습니다. "그래 . array는 메모리 상에 존재하는 특정 블럭을 표현 한것이니가 그렇다 치고, 그럼 list는 논리적으로 head 와 tail을 따로 구성할 수 있고, 가변적인 데이타 구조이니까, 앞으로 우리는 list에서는 index가 1부터 시작하도록 할거야."

라고 생각하고 list의 index를 1부터 시작하는 것으로 제공하기로 하였습니다.

 

만약 list 의 index를 1부터 시작하겠다고 하는 것은, "2번 닫힌 집합으로 표현 해라." 라는 의미가 되는 것입니다. 즉, 10개가 있다는 의미가 1~10까지 닫힌 집합으로 표현해야 한다가 되는것이고,  code로는   1<= index <=10 이라는 의미가 되는 것이죠.

array에서는 0<=index<10 이고, list에서는 1<=index<=10 이라고 표현 하면, 결국 data의 집합을 표현 하는 자료 구조인데 한 언어나 프래임워크 내에서 서로 다른 규격을 제시 하는 것이 됩니다. 

 

이런 비슷한 기능을 제공하는 class들이 각각 다른 기준의 동작을 유도하는 것은, 어플리케이션 개발에 수많은 혼선을 줄 가능성이 있습니다.

단순히 list와 array의 차이가 아니라, 어떤 API들도 직관적으로 접근 할 수가 없고, 숨은 의도들을 파악해야 하는 상황이 발생하죠. 

위의 경우, 어플리케이션 개발자는  "아 array는 memory 블럭을 잡는 것이니까 index는 0 부터 사용해야 하고, list는 동적이기 때문에 메모리 블럭과 관련이 없어 그러니까 프로그래머가 아닌 일반적인 사람 입장에서 생각해서  index가  1부터 인거야, 이건 꼭 외워야 해. 나중에 index array와 list간의 index변환도 고려 해야 하니까. 이런게 또 뭐가 있을까? Stack ? Stack은 어떻게 동작하지?" 라는 생각을 하게 할겁니다.

 

 

전반적인 S/W 도메인 별로 보면 여러 주제들 예를 들면 Synchronous와 Asynchronous, mutable, thread, lock, ipc, data binding, runtime, reflection 등등, 이러한 요소들을 프레임워크와 언어에 대해서 일관성, 과 일반적이고 상식적인 기본 동작과 컨셉을 가져가야 할 것인지를 잘 정리하고 제공되어야 좋은 플랫폼 또는 프래임워크 라고 할 수 있을 것입니다.

 

이런 이유에서 규모가 큰 S/W 회사들은 , 프래임워크 구축하려 할때, 기존의 언어로 새로운 패러다임을 반영하기 힘들거나 , 프래임워크에서 제공하려는 기능이 기존의 언어로 표현이 힘들 경우, 새로운 언어들을 만들기도 하는 것입니다. (C#, Java가 대표적이랄까요?)

 

 

'개발 Note > UI Framework 개발하기' 카테고리의 다른 글

Core Image Concepts  (0) 2011.09.20
stdafx.h 를 없애보자!  (0) 2011.08.18
CoreAnimation in iOS  (0) 2011.07.13
key - value 관리  (0) 2011.06.08
[애자일] 릴리즈 계획 과정에서 가장 중요한 것들  (0) 2010.10.29

+ Recent posts