Dart는 싱글 스레드 기반의 언어이지만, 비동기 프로그래밍을 통해 여러 작업을 효율적으로 처리할 수 있다. 비동기 작업을 처리하는 기본 도구는 Future
이다. 이 장에서는 Future
와 비동기 함수의 동작 원리와 사용법에 대해 깊이 있는 설명을 제공하겠다.
Future의 개념
Future
는 미래에 완료될 작업을 나타내는 객체이다. 작업이 완료되면 해당 결과를 반환하거나, 오류가 발생하면 예외를 던진다. Future
는 주로 시간이 걸리는 작업(예: 파일 읽기, 네트워크 요청)에서 사용된다.
다음은 Future
의 상태 변화를 도식으로 표현한 것이다:
- Pending 상태:
Future
가 생성되고 작업이 아직 완료되지 않은 상태. - Completed 상태: 작업이 완료되었으며, 성공적으로 값이 반환되거나 오류가 발생함.
- Value 또는 Exception: 작업이 성공한 경우 값을 반환하고, 실패한 경우 예외를 발생시킴.
비동기 함수의 정의와 호출
Dart에서 비동기 함수는 async
키워드를 사용하여 정의한다. 비동기 함수는 일반적으로 Future
객체를 반환하며, 이 함수 내부에서 await
를 통해 비동기 작업이 완료될 때까지 기다릴 수 있다. 비동기 함수의 핵심 개념은 바로 동기적 코드 흐름과 비동기적 작업을 결합하여 복잡한 비동기 로직을 보다 쉽게 구현할 수 있다는 것이다.
Future<int> fetchData() async {
return 42;
}
위 예제는 간단한 비동기 함수 fetchData
를 정의한 것이다. 이 함수는 42
라는 값을 담은 Future
를 반환한다.
Future의 사용 방식
Future
를 사용하여 비동기 작업을 처리할 때는 두 가지 방식이 있다.
- then() 사용:
then()
메서드는Future
가 완료된 후 호출된다. 성공적으로 완료된 경우 값을 전달받을 수 있으며, 오류가 발생한 경우catchError
를 사용하여 예외를 처리할 수 있다.
fetchData().then((value) {
print("Data: $value");
}).catchError((error) {
print("Error: $error");
});
- await 사용:
async
함수 내에서await
키워드를 사용하면,Future
가 완료될 때까지 기다리고, 완료된 값을 직접 변수에 할당할 수 있다.await
를 사용하면 코드가 마치 동기적으로 실행되는 것처럼 보이지만, 실제로는 비동기 작업이 진행되고 있는 것이다.
void fetchAndPrintData() async {
int data = await fetchData();
print("Data: $data");
}
Future의 결합
Dart에서는 여러 Future
를 결합하여 동시에 실행하거나, 순차적으로 실행할 수 있다.
- Future.wait(): 여러
Future
가 모두 완료될 때까지 기다린다. 이 함수는 비동기 작업들을 병렬적으로 처리할 때 유용하다.
Future<List<int>> fetchMultipleData() async {
List<int> results = await Future.wait([fetchData1(), fetchData2(), fetchData3()]);
return results;
}
- Chaining:
then()
을 통해 여러Future
작업을 순차적으로 연결하여 처리할 수 있다.
fetchData()
.then((value) => fetchMoreData(value))
.then((moreValue) => print("More Data: $moreValue"));
수학적 비유
비동기 프로그래밍을 수학적으로 비유하자면, 동기적 함수 호출을 일차 방정식으로 표현할 수 있다. 예를 들어, 함수 f(x)는 입력값 x에 대해 즉시 값을 반환한다.
하지만 비동기 함수는 값이 즉시 반환되지 않으며, 미래에 값을 반환할 것을 약속한다. 이를 수학적으로 표현하면, 다음과 같이 Future
의 개념을 이용하여 비동기적으로 값을 처리할 수 있다.
이때, 함수 f(x)의 결과는 값이 준비될 때까지 기다려야 하며, 완료된 이후에는 결과가 반환된다.
비동기 함수는 이러한 과정에서 작업이 완료될 때까지 기다리는 것을 보장하며, 결과 값이 준비되면 Value
또는 Error
상태로 전환된다.
비동기 작업에서의 예외 처리
비동기 함수에서 예외 처리는 일반 함수와 다르다. Future
가 반환될 때, 예외는 then()
또는 await
을 사용하는 코드에서 비동기적으로 처리해야 한다. Dart에서는 비동기 작업 중 발생한 예외를 처리하기 위해 try-catch
를 사용할 수 있다.
try-catch를 통한 예외 처리
비동기 함수 내부에서 발생하는 예외는 try-catch
블록을 사용하여 처리할 수 있다. 일반 함수와 동일한 방식으로 예외를 처리하되, 비동기 함수에서는 await
키워드와 함께 예외 처리가 가능한다.
Future<void> fetchData() async {
try {
int data = await getDataFromServer();
print("Data: $data");
} catch (e) {
print("An error occurred: $e");
}
}
위 코드는 서버에서 데이터를 가져오는 비동기 함수에서 발생할 수 있는 예외를 try-catch
로 처리하는 예시이다. await
는 비동기 작업이 완료될 때까지 기다리며, 이때 예외가 발생하면 catch
블록에서 처리된다.
catchError를 통한 예외 처리
Future
객체는 catchError()
메서드를 사용하여 비동기 작업에서 발생한 예외를 처리할 수 있다. 이는 then()
과 함께 사용될 때 유용하다.
getDataFromServer().then((data) {
print("Data: $data");
}).catchError((error) {
print("An error occurred: $error");
});
이 코드는 then()
과 catchError()
를 사용하여 예외를 처리하는 예시이다. 비동기 작업이 성공적으로 완료되면 then()
블록이 실행되고, 예외가 발생하면 catchError()
블록에서 처리된다.
Future의 체인에서의 예외 처리
비동기 작업을 여러 단계로 나누어 체인 형태로 연결할 때, 각 단계에서 예외를 처리할 수 있다. 만약 하나의 Future
가 실패하면, 해당 오류는 체인 전체로 전파되며, 이후의 작업은 실행되지 않는다.
fetchData()
.then((value) => fetchMoreData(value))
.then((moreValue) => print("More Data: $moreValue"))
.catchError((error) {
print("An error occurred: $error");
});
위 예제에서 첫 번째 fetchData()
가 실패하면, 체인의 나머지 부분은 실행되지 않고 즉시 catchError()
로 이동하여 오류를 처리한다.
수학적 비유 - 예외 처리
예외 처리의 개념을 수학적으로 설명하자면, 비동기 작업이 진행되면서 예외가 발생할 확률이 존재하는 함수로 비유할 수 있다. 함수 f(x)가 입력 x에 대해 예외를 발생시킬 가능성이 있는 경우, 해당 함수는 예외를 처리하는 새로운 함수 g(x)로 대체될 수 있다.
이때, g(x)는 성공적으로 값을 반환하거나 예외를 발생시키는 두 가지 상태로 분기할 수 있다.
이와 같이, 비동기 작업에서는 함수가 정상적으로 완료될 때까지 기다리며, 중간에 발생하는 예외는 catchError()
또는 try-catch
로 처리하여 작업의 실패를 관리할 수 있다.