# async, await 키워드

#### 비동기 프로그래밍의 배경

Dart에서 비동기 프로그래밍은 시간이 오래 걸리는 작업, 특히 I/O 작업을 수행할 때 애플리케이션의 성능을 최적화하는 중요한 기법이다. 비동기 처리를 통해 메인 쓰레드가 차단되지 않고 계속 실행되도록 보장할 수 있다. 이를 위해 Dart에서는 `async`와 `await`라는 두 가지 키워드를 제공한다. 이 키워드들은 비동기 코드를 동기식 코드처럼 간단하게 작성할 수 있도록 도와준다.

#### async 키워드의 역할

`async` 키워드는 함수 앞에 붙여서 해당 함수가 비동기 함수임을 선언하는 데 사용된다. 일반적으로 함수는 값을 반환하지만, `async` 키워드가 붙으면 해당 함수는 `Future`를 반환하게 된다. `Future`는 나중에 완료될 값의 약속을 나타낸다. `async` 키워드를 사용하면, 함수 내에서 비동기 작업을 포함한 여러 작업을 순차적으로 처리할 수 있다.

다음은 `async` 키워드를 사용한 간단한 예제이다:

```dart
Future<void> fetchData() async {
  print('데이터 가져오는 중...');
  await Future.delayed(Duration(seconds: 2)); // 2초 후에 완료
  print('데이터 가져옴');
}
```

이 코드에서 `fetchData` 함수는 `async` 키워드로 선언되었으며, `Future.delayed`로 2초 동안의 지연을 시뮬레이션한다. 함수는 `await`를 사용하여 비동기 작업이 완료될 때까지 기다린다. 이를 통해 `await` 키워드는 비동기 함수 내부에서 동기식 코드처럼 작성되지만, 실제로는 비동기 작업을 처리한다.

#### await 키워드의 역할

`await`는 `async` 함수 내에서 사용되는 키워드로, 비동기 작업이 완료될 때까지 기다린다. 일반적으로 `Future` 객체를 반환하는 함수 앞에 `await`를 붙여서 그 함수가 반환하는 `Future`의 완료를 기다린다. 이때, Dart는 다른 작업을 계속 수행하다가 해당 비동기 작업이 완료되면 이후의 코드를 실행하게 된다.

다음 예제를 보자:

```dart
Future<void> processData() async {
  print('데이터 처리 시작');
  var result = await fetchData();
  print('처리된 데이터: $result');
}
```

이 코드에서 `processData` 함수는 `await`를 사용하여 `fetchData` 함수의 완료를 기다리고, 완료되면 그 결과를 사용하여 데이터를 처리한다. 이 방식은 마치 동기식 코드처럼 보이지만, `fetchData`는 실제로 비동기적으로 처리된다.

#### Future의 구조와 async, await의 상호작용

비동기 함수가 반환하는 `Future`는 수학적으로도 이해할 수 있다. Future는 상태가 변할 수 있는 상태 머신으로 볼 수 있으며, 시간이 지나면서 그 상태가 "대기 중"에서 "완료"로 변경된다. 이를 수식으로 표현하면 다음과 같다.

$$
\text{Future}(t) = \begin{cases} \text{대기 중} & t\_0 < t < t\_f \ \text{완료} & t = t\_f \end{cases}
$$

여기서 $t\_0$는 비동기 작업의 시작 시간, $t\_f$는 작업이 완료되는 시간이다. `await` 키워드는 이러한 `Future`의 상태가 완료될 때까지 코드 실행을 일시적으로 멈추고 기다리는 역할을 한다.

#### Future와 Promise의 비교

JavaScript의 `Promise`와 Dart의 `Future`는 매우 유사하지만, Dart에서는 `await` 키워드를 통해 비동기 처리에 대한 가독성을 더욱 높일 수 있다. 다만, `Future`는 그 자체로 상태와 값을 가지고 있는 객체이며, Dart는 이 객체를 기반으로 다양한 비동기 처리 작업을 수행한다.

Dart에서의 Future는 주로 다음과 같은 두 가지 상태로 분류된다:

1. **완료 전**: 아직 값이 반환되지 않은 상태
2. **완료 후**: 값이 반환된 상태

이 두 상태를 수식으로 나타내면 다음과 같다:

$$
\mathbf{Future}*{i} = \begin{bmatrix} \text{상태}*{i} \ \text{값}\_{i} \end{bmatrix}
$$

이때 `await` 키워드는 $\mathbf{Future}\_{i}$에서 상태가 완료될 때까지 기다리는 역할을 한다.

#### async와 await의 에러 처리

비동기 함수에서 발생할 수 있는 중요한 부분 중 하나는 에러 처리이다. `async` 함수는 `Future`를 반환하므로, `Future`에서 발생하는 에러는 `catchError` 메소드나 `try-catch` 구문을 사용하여 처리할 수 있다. 비동기 함수 내에서 `await` 키워드가 사용될 때, `try-catch`를 통해 예외를 처리할 수 있다.

예를 들어:

```dart
Future<void> fetchData() async {
  try {
    print('데이터 가져오는 중...');
    await Future.delayed(Duration(seconds: 2));
    throw Exception('데이터 가져오기 실패'); // 의도적으로 에러 발생
  } catch (e) {
    print('에러 발생: $e');
  }
}
```

이 코드에서는 `await`로 비동기 작업을 처리하는 동안, `Exception`을 의도적으로 발생시켰다. 이를 `try-catch` 블록으로 감싸서, 발생한 예외를 처리한다. 이렇게 하면 비동기 함수 내에서 에러가 발생하더라도 애플리케이션의 흐름이 끊기지 않고 안정적으로 동작할 수 있다.

또한, 비동기 함수에서 `Future`는 에러가 발생할 때 해당 에러를 전달하는 역할도 한다. 이를 수학적으로 표현하면, `Future`는 성공적인 완료와 실패한 완료의 두 가지 상태로 나뉠 수 있다.

$$
\mathbf{Future}\_{i} = \begin{cases} \mathbf{성공} & \text{작업 성공 시} \ \mathbf{실패} & \text{작업 실패 시} \end{cases}
$$

따라서 `await` 키워드를 사용할 때는 항상 에러 처리에 신경 써야 하며, `try-catch`를 적절히 사용하여 비동기 함수에서 발생할 수 있는 예외를 처리해야 한다.

#### await의 병렬 처리

`await` 키워드를 사용할 때 종종 발생하는 문제는 모든 비동기 작업이 순차적으로 실행된다는 점이다. 이 경우 여러 비동기 작업을 병렬로 실행하고 싶을 때, `await`를 나란히 사용할 경우 원하는 성능을 얻을 수 없다. 병렬 처리가 필요한 경우에는 `Future.wait()`를 사용할 수 있다. 이 메소드는 여러 개의 `Future`를 동시에 실행하고, 모든 작업이 완료될 때까지 기다린다.

다음 예제는 여러 비동기 작업을 병렬로 처리하는 방법을 보여준다:

```dart
Future<void> loadData() async {
  var future1 = Future.delayed(Duration(seconds: 2), () => '작업 1 완료');
  var future2 = Future.delayed(Duration(seconds: 3), () => '작업 2 완료');
  
  var results = await Future.wait([future1, future2]);
  print(results); // ['작업 1 완료', '작업 2 완료']
}
```

이 코드는 두 개의 비동기 작업을 동시에 실행하고, 두 작업이 모두 완료되면 그 결과를 리스트로 반환한다. 이 방식은 각 작업이 독립적일 때 매우 유용하며, 성능 최적화에 큰 도움을 준다.

이 원리를 수식으로 표현하면, 각 비동기 작업 $\mathbf{Future}\_{i}$가 병렬로 처리되므로 전체 작업의 완료 시간은 가장 긴 작업의 완료 시간에 의해 결정된다.

$$
t\_{\text{완료}} = \max(t\_{\mathbf{Future}*1}, t*{\mathbf{Future}*2}, \dots, t*{\mathbf{Future}\_n})
$$

병렬 처리를 통해 모든 작업이 빠르게 완료될 수 있으며, `await` 키워드를 사용하는 방식에 비해 성능이 크게 향상된다.

#### await와 반복문

Dart에서 `await`를 반복문과 함께 사용할 수 있다. 반복문 내에서 비동기 작업을 처리할 때 각 반복이 순차적으로 실행되기 때문에, 반복문이 끝나기 전까지 다음 작업이 시작되지 않는다. 이는 때때로 성능에 영향을 줄 수 있다. 하지만 때로는 이러한 순차적인 처리가 필요할 수도 있다.

다음은 반복문에서 `await`를 사용하는 간단한 예시이다:

```dart
Future<void> processTasks() async {
  var tasks = [1, 2, 3, 4, 5];
  for (var task in tasks) {
    await Future.delayed(Duration(seconds: 1));
    print('작업 $task 완료');
  }
}
```

이 코드에서는 작업 목록을 순차적으로 처리하며, 각 작업이 완료될 때까지 1초 동안 대기한다. 이 경우, 모든 작업이 순차적으로 처리되므로 총 5초가 소요된다.

이를 수식으로 나타내면, 각 작업의 완료 시간 $t\_{\text{완료}}$은 각 대기 시간의 합계로 나타난다.

$$
t\_{\text{완료}} = \sum\_{i=1}^{n} t\_{\mathbf{Future}\_i}
$$

여기서 $n$은 반복 횟수이고, 각 $t\_{\mathbf{Future}\_i}$는 개별 비동기 작업의 소요 시간이다.

#### 병렬 반복 처리

비동기 작업이 독립적일 때, 반복문 내에서 순차적으로 `await`를 사용하는 대신 병렬로 처리하는 것이 더 효율적이다. 이를 위해 `Future.wait()`를 사용할 수 있다. 이 방법을 사용하면 모든 작업이 동시에 시작되고, 가장 늦게 완료되는 작업이 끝나면 전체 작업이 완료된다.

다음은 병렬 처리를 통해 반복문 내 비동기 작업을 최적화한 예제이다:

```dart
Future<void> processTasks() async {
  var tasks = [1, 2, 3, 4, 5];
  var futures = tasks.map((task) => Future.delayed(Duration(seconds: task), () => '작업 $task 완료'));
  var results = await Future.wait(futures);
  print(results);
}
```

이 코드에서는 각 작업이 병렬로 실행되며, 작업 시간이 $task$ 값에 따라 다르다. `Future.wait()`는 모든 작업이 완료될 때까지 기다린 후 그 결과를 반환한다.

수식으로 표현하면, 이 경우 각 작업의 완료 시간 $t\_{\text{완료}}$은 가장 긴 작업의 시간에 의해 결정된다.

$$
t\_{\text{완료}} = \max(t\_{\mathbf{Future}*1}, t*{\mathbf{Future}*2}, \dots, t*{\mathbf{Future}\_n})
$$

따라서 병렬 처리를 통해 작업을 효율적으로 완료할 수 있으며, 반복문 내에서의 순차 처리보다 성능이 크게 향상된다.

#### Future와 `await`의 성능 고려 사항

`await`는 매우 유용하지만, 비동기 작업의 순차적 실행으로 인해 성능 저하가 발생할 수 있다. 특히 반복적인 비동기 작업이 있는 경우, 모든 작업이 완료될 때까지 기다리는 시간이 문제될 수 있다. 따라서 이러한 상황에서는 병렬 처리를 적극 활용하거나, 비동기 작업의 특성에 맞게 설계를 최적화해야 한다.

예를 들어, 비동기 작업 간에 의존성이 없고 병렬 처리가 가능한 경우, 순차적으로 `await`를 사용하는 대신 `Future.wait()`를 사용하는 것이 성능적으로 더 유리하다. 반면, 작업 간에 의존성이 있어 순차 처리가 필요한 경우, `await`를 반복문 내에서 사용하는 것이 적절하다.

수식을 통해 비동기 작업의 총 소요 시간을 다시 정리하면, 순차 처리의 경우:

$$
t\_{\text{완료}} = \sum\_{i=1}^{n} t\_{\mathbf{Future}\_i}
$$

병렬 처리의 경우:

$$
t\_{\text{완료}} = \max(t\_{\mathbf{Future}*1}, t*{\mathbf{Future}*2}, \dots, t*{\mathbf{Future}\_n})
$$

각 비동기 작업의 특성을 고려하여 적절한 방식을 선택하는 것이 중요하다.
