brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Mar 29. 2020

다트 future, async, await

플러터를 위한 다트 프로그래밍

다트의 비동기 프로그래밍 (2/3)

다트는 future를 지원한다. future는 이미 다른 언어에서도 종종 사용되고 있는 키워드다.


future는 어떤 작업 결괏값을 나중에 받기로 약속하는 것이다. 즉 요청한 작업의 결과를 기다리지 않고 바로 다음 작업으로 넘어간다. 그 후 작업이 완료되면 결과를 받는 방식으로 비동기 처리를 하는 것이다.


작업이 완료될 때까지 기다렸다가 결괏값을 받고 다음 작업으로 넘어갈 수도 있다. 이 경우는 async, await를 사용하면 가능하다.


그럼 future, async, await에 대해서 하나씩 알아보자.


1) future

future는 크게 두 가지 상태를 가지고 세부적으로는 세 가지 상태를 가진다.


1. Uncompleted(미완료) : future 객체를 만들어서 작업을 요청한 상태

2. Completed(완료) : 요청한 작업이 완료된 상태

 2.1) data : 정상적으로 작업을 수행하여 결괏값을 리턴하며 완료

 2.2) error : 작업 처리 중 문제 발생 시 에러와 함께 완료


future는 상태별로 다른 작업과 마찬가지로 event loop에 의해서 순차적으로 처리된다. 처음 future를 생성하여 작업을 시작하면 Uncompleted future가 event queue에 들어간다. 해당 작업이 완료되기 전까지는 다른 작업들이 event queue에 들어가고 event loop에 의해서 꺼내져 처리된다. 그러다가 future가 작업을 끝내면 Completed future가 event queue에 들어가고 event loop에 의해 선택되면 Completed future가 가진 결괏값이나 에러에 대한 처리를 하는 것이다.


다음 그림은 future가 event queue에 어떤 식으로 쌓이는지 보여준다.


위 그림을 바탕으로 예제를 만들어 볼 것이다. 일단 future의 기본 형태를 잠시 살펴보자. 조금이라도 쉽게 이해할 수 있도록 가장 날 것의 형태부터 먼저 본다.


Future<T> 변수명 = new Future( () {

  // do something

 }

 return T;

});


변수명.then((결괏값) {

  // do something

}, onError: (에러) {

 // do something

});


future 객체를 만들 때 타입은 Future<T>와 같이 제네릭을 사용한다. 만약 타입을 Future<String>로 선언했다면 future에서 작업 후 리턴될 결괏값의 타입이 String 타입이라는 의미다. future를 만들어서 작업이 시작된 이 상태가 Uncompleted future이다. 따라서 현재 상태를 event queue에 넣고 다음 작업으로 넘어간다. 이런 식으로 future내의 do something이 모두 처리되기 전에 다음 작업을 진행할 수 다.


future의 작업이 완료되면 then()이 호출된다. 이때는 Completed future인 상태이다. then()의 첫 번째 매개변수는 결괏값을 인자로 가지는 익명 함수이고 두 번째 onError는 에러 처리를 위한 함수이다.



onError의 형태가 잘 이해되지 않는다면 함수 관련 글의 3. 이름 있는 선택 매개변수 부분을 참고하면 된다. onError는 매개변수의 형태가 변수가 아니라 함수라는 점이 조금 다를 뿐이다. 간단한 예를 들면 다음과 같다.



가장 날 것의 모습을 한 코드로 구현하면 다음과 같다.

Line 2 : start를 출력하는 작업이 event queue에 가장 먼저 들어가서 처음으로 실행된다.


Line 4~9 : future를 만든다. myFuture라는 객체를 생성하였다. 이때 타입을 보면 Future<String>으로 되어 있다. 결괏값의 타입이 String 타입이라는 의미다. 현재 상태Uncompleted future이다. 여기서의 작업은 for문을 100억 번 반복하는 것이다. 환경에 따라 다르겠지만 4~7초 정도 걸리는 작업이다. 해당 작업의 시작을 알리는 Uncompleted future가 event queue에 들어간다. 실제 작업은 dart 내부적으로 별도의 스레드를 이용해서 진행할 것이고 바로 다음 event queue에 있는 작업으로 넘어간다.


Line 11~15 :  future가 작업을 완료하면 then()이 호출된다. 결괏값을 받아서 출력하도록 했다. 만약 에러가 발생하면 에러를 출력하도록 한다. (에러 출력 예제는 다음 예제에서 다룬다.)


Line 17 : do something을 출력한다.


출력 결과 : main이 시작하면 start를 가장 먼저 출력한다. 그 후 main isolate의 유일한 스레드는 Uncompleted future 처리를 요청받을 것이다. 해당 작업은 별도의 어디선가 작업을 진행하도록 하고 바로 다음 작업인 do something을 출력한다. 그리고 future가 완료되면 Completed future를 받아서 then()의 익명 함수에서 구현된 내용인 결괏값을 출력한다.


Completed future가 결괏값 대신에 에러를 가졌을 때는 onError를 통해서 처리된다. 확인해 보기 위해서 다음과 같이 future 내에서 의도적으로 에러를 던져준다.

출력 결과를 보면 Exception을 받아서 출력한 것을 확인할 수 있다.


이제 위 코드를 좀 더 보기 좋게 정리해 보자. for문이 데이터를 얻는 과정이라고 가정하여 해당 부분을 별도 함수로 분리하고 then()의 익명 함수도 람다를 사용할 것이다. 에러 처리를 하는 onError도 builder pattern으로 변경할 것이다. 이때는 catchError() 라는 함수를 사용한다.

onError와 catchError는 에러를 처리한다는 관점에서 역할은 동일하지만 차이점이 있다. onError는 future에서 발생한 에러만 처리할 수 있다. 대신 catchError는 then()의 첫 번째 인자인 익명 함수 내부에서 발생한 에러까지 처리할 수 있다. 코드로 보자.

기존 익명 함수에서 바로 print() 함수를 호출하던 것을 test()라는 함수로 변경했다. test() 함수는 print() 함수를 호출한 후 에러를 발생하도록 했다. 이때 catchError는 test() 함수에서 발생한 에러를 처리하여 Exception을 출력했다. 하지만 onError는 에러를 처리하지 못했다.


2) async, await

async와 await는 한 쌍으로 사용한다. await가 비동기(async) 함수 내에서만 사용할 수 있는 키워드이기 때문이다. 비동기 함수를 만드는 방법은 함수명 뒤에 async 키워드를 붙이는 것이다.

기본 형태는 다음과 같다.


함수명() async {

 await 작업함수();

}


비동기 함수 내에서 await가 붙은 작업해당 작업이 끝날 때까지 다음 작업으로 넘어가지 않고 기다린다.


어떤 경우에 async, await를 사용하는지 예제를 보자. 다음 예제는 await를 적용하지 않은 경우이다.

Line 4~5 : getData()의 작업이 끝나기 전에 리턴 값을 가져와서 출력하고 있다. 따라서 아직 결괏값이 없기 때문에 리턴 변수의 타입이 출력되었다.


위 예제에 await를 적용해 보자.

main() 함수를 비동기 함수로 만들고 getData()의 작업이 끝날 때까지 다음 작업으로 진행하지 않도록 await를 사용했다. 그러면 getData() 내부의 future가 작업을 마치고 결괏값을 제공하기 때문에 출력 결과가 이전과 다르게 결괏값을 보여준다. 또한 실제 실행 시 Line 2의 start가 출력이 되고 Line 4에서 getData() 내부 for문이 처리되는 동안 프로그램이 멈춘 것처럼 있다가 완료되는 순간 Line 5가 실행되고 곧이어 Line 7이 실행되는 것을 확인할 수 있다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari