본문 바로가기

프로그래밍 언어 기초/Dart

[Dart] Dart 기본 문법4 [비동기, async, await, Future, Stream]

개요

오늘은 프로그래밍에서 중요한 동기(synchronous), 비동기(asynchronous) 에 대해서 알아보도록 하겠습니다.

우선, 동기와 비동기가 무엇인지 알아보고 dart에서는 어떻게 비동기 프로그래밍을 할 수 있는지 살펴보겠습니다.

오늘의 포스팅에서 소개할 내용은 아래와 같습니다.

  • 동기, 비동기
  • Future
  • Async Await
  • Stream
  • BroadCastStream
  • 비동기 함수의 Starem 변환

동기(synchronous) vs 비동기(asynchronous)

기본적으로 대부분의 프로그래밍 언어는 코드를 작성한 '순서'대로 작업을 수행합니다.

난 포스팅에서 지금까지 dart를 통해 작성한 프로그램도 엄밀히 구분하자면 '동기'적으로 실행되었습니다.

동기, 비동기 프로그램의 동작 흐름을 그림으로 표현해보자면 아래와 같습니다.

 



왜 프로그렘에서 '비동기' 작업이 필요할까요?

오늘날의 프로그램은 성능 좋은 하드웨어와 함께 기능이 구현되므로 그 '속도'가 매우 빠릅니다.

하지만, 모든 작업이 항상 순차적으로 처리해도 될 정도로 빠르게 동작하진 않습니다.

 

그렇기 때문에 모든 프로그램이 '순차적'으로만 즉, 동기적으로만 작동한다면

프로그램이 수행해야할 작업 중 시간이 오래 걸리는 작업이 실행되면

사용자는 기약 없이 오랜 시간 기다려야 할 수도 있습니다.

그리고 운영체제에서 다루는 개념으로 더 설명을 보충하자면, 

하나의 실행중인 프로그램, 즉 프로세스가 컴퓨터 자원을 '오래' 점유하는

기아 현상(starvation phenomenon)이 발생할 수 있습니다.
이는 컴퓨터 자원의 효율적, 합리적 사용을 저해하고 전체적 성능을 낮게하는 부정적인 영향을 끼칩니다.

그렇기 때문에 지난 시간 컴퓨터 엔지니어들은 이를 해결하기 위해 방법을 모색하게 되었고,

이를 위한 해법 중 하나로 바로 비동기 프로그래밍이 등장하게 되었습니다.

비동기 프로그램은 동기적 프로그램과 달리 프로그램을 호출한 '시점'과 동작하는 '시점'이 다릅니다.

예를 들자면, 은행에서 번호표를 뽑아 직원에게 업무를 봐줄 것을 '요청' 했는데,

경우에 따라서는 나보다 늦게 번호표를 뽑은 손님이 '먼저' 업무가 처리되는 상황이라고 비유할 수 았을 것 같습니다.

직관적으로 생각해보면, 다소 불공정한 방식이라고 느껴질 수도 있겠지만

전체적인 프로그램의 동작을 생각해본다면, 이는 단위 시간당 최대의 효율을 낼 수 있는 전략입니다.

다만, 근본적으로 비동기 프로그래밍은 '순차적' 동작을 보장하지 않기 때문에,

이에 따른 작업의 유효성 검증 등을 별도로 수행해야할 수도 있기에, 모든 요건을 만족하는 완벽한 기술은 아니라고 볼 수 있습니다.

그렇기에 오늘날에 프로그래밍에서는 이 두 가지 전략을 적절히 혼합하여 사용합니다.


Future, 미래에 받아올 데이터

dart에서는 Future라는 클래스를 제공합니다.

단어의 뜻에서 유추할 수 있듯이, 이 클래스는 미래에 받아올 값을 다룰 수 있는 자료형입니다.

즉, 비동기 프로그래밍에서 사용 가능한 데이터 타입입니다.

dart에서 Future는 아래와 같이 사용할 수 있습니다.

Future<String> name;
Future<int> age;
Future<bool> isOpen;

 

 


dart의 비동기 프로그래밍

dart에서는 아래와 같이 비동기 프로그램을 작성할 수 있습니다.

비동기 프로그램은 대체로 서버와의 통신 또는 데이터베이스 작업 등

기본적으로 오랜 시간을 필요로 하는 작업에 대응하기 위한 기술입니다.

그러나, 본 예제에서는 Duration 클래스를 통해 아래와 같이 임의로 시간차이를 두어 비동기 환경을 구현해보겠습니다.

// async task
void addNumber(int x, int y){
    print('$x + $y = ?');

    Future.delayed(Duration(seconds:3), (){ // Duration Class를 통해 간격을 줌
        print('$x + $y is ${x+y}');

    });

    print('function is end...');
}

void main() {
    addNumber(1,2);
    /*
        // output
        1 + 2 = ?
        function is end...
        1 + 2 is 3
    */
}

 



async, aswait

비동기 프로그램은 프로그램의 코드가 작성된 '순서'대로 동작하지 않습니다.

하지만, 경우에 따라서는 비동기 작업의 결과를 기다린 후 다음 작업을 수행해야하는 것도 필요할 수 있습니다.

이때, 비동기 프로그램을 중첩하여 조건에 대응할 수도 있겠지만,

이러한 구조가 중첩된다면 결국에는 화살표 함수(Arrow fuction)라는 권장되지 않는 코드 패턴이 될 수도 있습니다.

 

따라서, 비동기 프로그램의 '동기적' 실행을 보장하기 위해서 async와 await이 등장하게 되었습니다.

dart에서 async, await은 아래와 같이 사용할 수 있습니다.

//async await
void addAsyncNumber(int x, int y) async {
    print('$x + $y = ?');

    await Future.delayed(Duration(seconds:3), (){ // Duration Class를 통해 간격을 줌
        print('$x + $y is ${x+y}');

    });

    print('function is end...');
}

void main() {
    // async-await
    addAsyncNumber(1, 2);
    addAsyncNumber(3, 4);
    /*
        // ouptut
        1 + 2 = ?
        3 + 4 = ?
        1 + 2 is 3
        function is end...
        3 + 4 is 7
        function is end...
    */

}



async와 await을 사용했을 경우, 비동기 작업이 동기적으로 실행되지만,

비동기 작업이 동기적 작업이 되는 것은 아닙니다.

 

위에서 작성한 함수 addAsyncNumber는 asyn-await 구문을 통해

동기적으로 실행하도록 설계하였지만, 함수가 출력한 데이터를 확인해보면

함수의 상단에 print문은 먼저 실행 되고, 그 다음에 await이 키워드가 붙은 작업이 실행됩니다.

즉, async-await을 통해 비동기 작업에 대한 동기적 제어는 가능하지만,

비동기 함수 자체를 동기 함수로 바꾸는 것은 아니라는 의미입니다.

 


비동기 함수의 동기적 실행

그렇다면, 비동기 함수 자체를 동기적으로 제어할 순 없을까요?

이 또한, async-await 구문을 통해 해결할 수 있습니다.

 

위의 예제 코드를 살펴보면 함수의 몸체(바디, {})부분 앞,

함수 이름 뒤 쪽에 async 키워드를 붙여 함수의 흐름을 제어했는데요.

 

이를 응용하면, 메인 함수 역시 함수이므로 async-await 키워드를 붙여서 흐름을 제어할 수 있습니다.

여기에 리턴 타입을 void에서 Future<void>로 바꿔주면 이제 addAsyncNumber 함수 전체를 동기적으로 제어할 수 있습니다.

//async await
Future<void> addAsyncNumber(int x, int y) async {
    print('$x + $y = ?');

    await Future.delayed(Duration(seconds:3), (){ // Duration Class를 통해 간격을 줌
        print('$x + $y is ${x+y}');

    });

    print('function is end...');
}

void main() async {
    // async-await2
    await addAsyncNumber(1, 2);
    await addAsyncNumber(3, 4);
    /*
        // ouptut
        1 + 2 = ?
        1 + 2 is 3
        function is end...
        3 + 4 = ?
        3 + 4 is 7
        function is end...
    */    
}

 


비동기 작업의 결과값 리턴

지금까지는 비동기 프로그램의 반환 타입이 없는(void) 사례를 살펴보았습니다.

그렇다면, 비동기 작업의 결과 값을 반환 받아 또 다른 작업을 수행하려면 어떻게 해야 할까요?

이 경우에는 Future를 사용해서 간단히 해결할 수 있습니다.

dart에서 비동기 작업의 결과를 받을 떄에는 Futrure를 사용할 수 있고 아래와 같아 활용할 수 있습니다.

// return async-await
Future<int> getAddAsyncNumber(int x, int y) async {
    print('$x + $y = ?');

    await Future.delayed(Duration(seconds:3), (){ // Duration Class를 통해 간격을 줌
        print('$x + $y is ${x+y}');

    });

    print('function is end...');

    return x + y;
}

void main() async {
    // future
    final result1 = await getAddAsyncNumber(1,2);
    print('reult1 is $result1');
    final result2 = await getAddAsyncNumber(result1, 3);
    print('reult2 is $result2');
    /*
        //output
        1 + 2 = ?
        1 + 2 is 3
        function is end...
        reult1 is 3
        3 + 3 = ?
        3 + 3 is 6
        function is end...
        reult2 is 6
    */
}

 

 


Stream

Future는 비동기 작업에 대한 결과 값을 받을 수 있는 자료형입니다.

그러나, Future는 딱 한 번 결과 값을 받을 수 있기 때문에 한계가 존재합니다.

 

따라서, dart에서는 Stream이라는 형태를 통해 비동기 작업의 결과를 받을 수 있고

리슨(listen)이라는 과정을 통해 Stream에 주입되는 모든 값들을 지속적으로 받아올 수 있습니다.

import "dart:async"; // 스트림을 사용하기 위한 도구 추가!

void main() {

    // stream
    final controller = StreamController();
    final stream = controller.stream;

    // stream의 리스너를 설정
    final streamListener1 = stream.listen((value){
        print('listen data is : $value');
    });

    // 데이터 추가
    controller.sink.add(1);
    controller.sink.add(2);
    controller.sink.add(3);
    controller.sink.add(4);
    /*
        //output
        listen data is : 1
        listen data is : 2
        listen data is : 3
        listen data is : 4
    */

}

BroadCastStream

Stream은 listen()이라는 함수를 통해 지속적으로 추가되는 값에 대해 감지할 수 있습니다.

하지만, Stream의 listen()은 오직 한 번만 사용할 수 있기 때문에

한 곳에서 작성한 Stream을 다른 곳에서도 listen()하는 것이 필요할 수 있습니다.

이 경우에 사용할 수 있는 것이 BoradCastStream이고, Stream과 달리 여러번 수신(listen)할 수 있습니다.

 

import "dart:async";

void main() {
    // BroadCastStream
    final controller = StreamController();

    final stream = controller.stream.asBroadcastStream(); // cast to BroadCastStream!

    // stream의 리스너 1
    final streamListener1 = stream.listen((value){
        print('listen1 data is : $value');
    });

    // stream의 리스너 2
    final streamListener2 = stream.listen((value){
        print('listen2 data is : $value');
    });

    // 데이터 추가
    controller.sink.add(1);
    controller.sink.add(2);
    controller.sink.add(3);
    controller.sink.add(4);

    /*
        // output
        listen1 data is : 1
        listen2 data is : 1
        listen1 data is : 2
        listen2 data is : 2
        listen1 data is : 3
        listen2 data is : 3
        listen1 data is : 4
        listen2 data is : 4
    */
}

 

 


비동기 함수를 Starem으로 변환하기

지금까지 Stream과 BroadcastStream은 controller를 통해 스트림을 수신했습니다.

그런데, dart에서는 비동기 함수 자체를 Stream으로 변환하여 수신할 수 있습니다.

간단히 시작 번호부터 5개의 연속된 숫자를 출력하는 함수

countNumber를 스트림(Stream)으로 수신한다면 아래와 같이 작성할 수 있습니다.

 

import "dart:async";

Stream<int> countNumber(int start) async*{
    for(int i = start ; i < start + 5 ; i ++){
        yield i;
    }
    await Future.delayed(Duration(seconds:1));
}

void main() {
  	// function cast to stream
    countNumber(1).listen((value){
        print('countNumber : $value');
    });

    /*
        // output
        countNumber : 1
        countNumber : 2
        countNumber : 3
        countNumber : 4
        countNumber : 5
    */
}