Dart는 싱글 스레드 기반의 언어이지만, 비동기 프로그래밍을 통해 여러 작업을 효율적으로 처리할 수 있다. 비동기 작업을 처리하는 기본 도구는 Future이다. 이 장에서는 Future와 비동기 함수의 동작 원리와 사용법에 대해 깊이 있는 설명을 제공하겠다.

Future의 개념

Future는 미래에 완료될 작업을 나타내는 객체이다. 작업이 완료되면 해당 결과를 반환하거나, 오류가 발생하면 예외를 던진다. Future는 주로 시간이 걸리는 작업(예: 파일 읽기, 네트워크 요청)에서 사용된다.

다음은 Future의 상태 변화를 도식으로 표현한 것이다:

graph LR A[Future] --> B[Pending] B --> C[Completed] C -->|Success| D[Value] C -->|Error| E[Exception]
  1. Pending 상태: Future가 생성되고 작업이 아직 완료되지 않은 상태.
  2. Completed 상태: 작업이 완료되었으며, 성공적으로 값이 반환되거나 오류가 발생함.
  3. Value 또는 Exception: 작업이 성공한 경우 값을 반환하고, 실패한 경우 예외를 발생시킴.

비동기 함수의 정의와 호출

Dart에서 비동기 함수는 async 키워드를 사용하여 정의한다. 비동기 함수는 일반적으로 Future 객체를 반환하며, 이 함수 내부에서 await를 통해 비동기 작업이 완료될 때까지 기다릴 수 있다. 비동기 함수의 핵심 개념은 바로 동기적 코드 흐름과 비동기적 작업을 결합하여 복잡한 비동기 로직을 보다 쉽게 구현할 수 있다는 것이다.

Future<int> fetchData() async {
  return 42;
}

위 예제는 간단한 비동기 함수 fetchData를 정의한 것이다. 이 함수는 42라는 값을 담은 Future를 반환한다.

Future의 사용 방식

Future를 사용하여 비동기 작업을 처리할 때는 두 가지 방식이 있다.

  1. then() 사용: then() 메서드는 Future가 완료된 후 호출된다. 성공적으로 완료된 경우 값을 전달받을 수 있으며, 오류가 발생한 경우 catchError를 사용하여 예외를 처리할 수 있다.
fetchData().then((value) {
  print("Data: $value");
}).catchError((error) {
  print("Error: $error");
});
  1. await 사용: async 함수 내에서 await 키워드를 사용하면, Future가 완료될 때까지 기다리고, 완료된 값을 직접 변수에 할당할 수 있다. await를 사용하면 코드가 마치 동기적으로 실행되는 것처럼 보이지만, 실제로는 비동기 작업이 진행되고 있는 것이다.
void fetchAndPrintData() async {
  int data = await fetchData();
  print("Data: $data");
}

Future의 결합

Dart에서는 여러 Future를 결합하여 동시에 실행하거나, 순차적으로 실행할 수 있다.

  1. Future.wait(): 여러 Future가 모두 완료될 때까지 기다린다. 이 함수는 비동기 작업들을 병렬적으로 처리할 때 유용하다.
Future<List<int>> fetchMultipleData() async {
  List<int> results = await Future.wait([fetchData1(), fetchData2(), fetchData3()]);
  return results;
}
  1. Chaining: then()을 통해 여러 Future 작업을 순차적으로 연결하여 처리할 수 있다.
fetchData()
  .then((value) => fetchMoreData(value))
  .then((moreValue) => print("More Data: $moreValue"));

수학적 비유

비동기 프로그래밍을 수학적으로 비유하자면, 동기적 함수 호출을 일차 방정식으로 표현할 수 있다. 예를 들어, 함수 f(x)는 입력값 x에 대해 즉시 값을 반환한다.

f(x) = x + 2

하지만 비동기 함수는 값이 즉시 반환되지 않으며, 미래에 값을 반환할 것을 약속한다. 이를 수학적으로 표현하면, 다음과 같이 Future의 개념을 이용하여 비동기적으로 값을 처리할 수 있다.

\text{Future}(f(x)) = \text{Pending}

이때, 함수 f(x)의 결과는 값이 준비될 때까지 기다려야 하며, 완료된 이후에는 결과가 반환된다.

graph LR X[x] -->|"f(x)"| Y[Future Pending] Y --> Z[Future Completed] Z -->|Result| A[Value]

비동기 함수는 이러한 과정에서 작업이 완료될 때까지 기다리는 것을 보장하며, 결과 값이 준비되면 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) = \begin{cases} f(x), & \text{if } f(x) \text{ succeeds} \\ \text{Error}, & \text{if } f(x) \text{ fails} \end{cases}

이때, g(x)는 성공적으로 값을 반환하거나 예외를 발생시키는 두 가지 상태로 분기할 수 있다.

graph TD X[x] -->|"f(x)"| A[Future Pending] A -->|Success| B[Future Completed with Value] A -->|Failure| C[Error Handled] C --> D["catchError() 처리"]

이와 같이, 비동기 작업에서는 함수가 정상적으로 완료될 때까지 기다리며, 중간에 발생하는 예외는 catchError() 또는 try-catch로 처리하여 작업의 실패를 관리할 수 있다.