비동기 프로그래밍: Future와 async/await
비동기 프로그래밍: Future와 async/await
Dart의 라이브러리는 Future나 Stream 객체를 돌려주는 함수로 가득 차 있어요. 이런 함수를 비동기(asynchronous) 함수라 부르는데, 파일 입출력 같은 시간이 걸리는 작업을 완료를 기다리지 않고 시작해 놓고 바로 반환합니다. async와 await 키워드를 쓰면 이런 비동기 코드를 동기 코드처럼 읽히는 모양으로 작성할 수 있어요. 이 글에서는 Future를 다루는 방법과 async 함수 선언, Stream을 받아오는 방법을 차례로 살펴볼게요.
Future 다루기
완료된 Future의 결과가 필요할 때는 두 가지 방법을 써요. 하나는 여기서 설명할 async/await이고, 다른 하나는 dart:async 문서에 나오는 Future API를 쓰는 방법이에요.
async와 await로 작성한 코드는 비동기지만 겉보기에는 동기 코드와 아주 비슷해요. 예를 들어 비동기 함수의 결과를 await로 기다리는 코드를 볼까요.
await lookUpVersion();
await를 쓰려면 그 코드가 async 함수, 즉 async로 표시된 함수 안에 있어야 해요.
Future<void> checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}
async 함수는 시간이 오래 걸리는 작업을 할 수는 있어도, 그 작업이 끝날 때까지 기다리지는 않아요. 대신 첫 await 표현식을 만날 때까지 실행하다가, 거기서 Future 객체를 반환하고, await 표현식이 완료된 뒤에야 실행을 이어갑니다.
await를 쓰는 코드에서 에러와 정리를 다룰 때는 try, catch, finally를 사용해요.
try {
version = await lookUpVersion();
} catch (e) {
// React to inability to look up the version
}
async 함수 안에서는 await를 여러 번 쓸 수 있어요. 다음 코드는 세 함수의 결과를 순서대로 기다립니다.
var entrypoint = await findEntryPoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
await _expression_에서 _expression_의 값은 보통 Future예요. 만약 Future가 아니라면 그 값은 자동으로 Future 안에 감싸집니다. 이 Future는 어떤 객체를 돌려주겠다는 약속을 나타내고, await _expression_의 값은 바로 그 돌려받은 객체예요. await 표현식은 그 객체를 사용할 수 있을 때까지 실행을 멈추게 합니다.
await를 쓸 때 컴파일 타임 에러가 난다면, await가 async 함수 안에 있는지부터 확인해 보세요. 예를 들어 앱의 main() 함수에서 await를 쓰려면 main()의 본문이 async로 표시되어 있어야 해요.
void main() async {
checkVersion();
print('In main: version is ${await lookUpVersion()}');
}
위 예시는 checkVersion()이라는 async 함수를 결과를 기다리지 않고 호출하는데, 코드가 함수가 끝났다고 가정하면 문제가 생길 수 있어요. 이런 문제를 피하려면 unawaited_futures 린터 규칙을 쓰면 됩니다.
async 함수 선언하기
async 함수는 본문이 async 수정자로 표시된 함수예요. 함수에 async 키워드를 붙이면 그 함수는 Future를 반환하게 됩니다.
예를 들어 String을 반환하는 아래 동기 함수를 보겠습니다.
String lookUpVersion() => '1.0.0';
이 함수를 async 함수로 바꾸면 — 예를 들어 나중에 구현이 시간이 걸리게 될 때 — 반환값은 Future가 돼요.
Future<String> lookUpVersion() async => '1.0.0';
함수 본문에서 Future API를 직접 쓸 필요는 없어요. 필요하면 Dart가 Future 객체를 알아서 만들어 줍니다. 함수가 유용한 값을 반환하지 않는다면 반환 타입을 Future<void>로 만들어 주는 게 좋아요.
Stream 다루기
Stream에서 값을 받아올 때도 두 가지 방법이 있어요. 하나는 async와 비동기 for 루프(await for)를 쓰는 것이고, 다른 하나는 dart:async 문서에 나오는 Stream API를 쓰는 방법이에요.
await for를 쓰기 전에, 그것이 코드를 더 명확하게 하고 정말로 스트림의 모든 결과를 기다리려는 것인지 확인해야 해요. 예를 들어 UI 이벤트 리스너에는 보통 await for를 쓰지 않는 게 좋아요. UI 프레임워크가 끝없는 이벤트 스트림을 보내기 때문이에요.
비동기 for 루프의 형태는 다음과 같아요.
await for (varOrType identifier in expression) {
// Executes each time the stream emits a value.
}
_expression_의 값은 Stream 타입이어야 해요. 실행은 다음과 같이 진행됩니다.
- 스트림이 값을 내보낼 때까지 기다린다.
- 변수에 그 값이 들어간 상태로 for 루프의 본문을 실행한다.
- 스트림이 닫힐 때까지 1과 2를 반복한다.
스트림 듣기를 멈추려면 break나 return 문을 쓰면 돼요. 이들은 for 루프를 빠져나가면서 스트림 구독도 해제합니다.
비동기 for 루프를 만들 때 컴파일 타임 에러가 난다면 await for가 async 함수 안에 있는지 확인해 보세요. 예를 들어 main() 함수에서 비동기 for 루프를 쓰려면 main()의 본문이 async로 표시되어 있어야 해요.
void main() async {
// ...
await for (final request in requestServer) {
handleRequest(request);
}
// ...
}
더 알아보기
- dart:async 라이브러리 문서 — 스트림 API 레퍼런스
- 비동기 프로그래밍 튜토리얼 — Future와 async/await 대화형 입문
- 스트림 사용하기 — 단일 구독·브로드캐스트 스트림 소비