Async/await

"오늘의AI위키"는 AI 기술로 일관성 있고 체계적인 최신 지식을 제공하는 혁신 플랫폼입니다.
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.

1. 개요

`async/await`는 비동기 프로그래밍을 동기 프로그래밍처럼 보이게 해주는 문법적 설탕으로, 여러 프로그래밍 언어에서 지원한다. C#, 파이썬, 자바스크립트, 러스트, C++, 스위프트 등 다양한 언어에서 구현되었으며, 각 언어별로 `async`와 `await` 키워드의 사용법과 구현 방식에 차이가 있다. `async/await`는 비동기 코드의 가독성을 높이고 유지보수를 용이하게 하지만, "함수 색상" 문제와 같은 비판도 존재한다. 한국 개발 생태계에서는 Node.js, Python, 웹 프론트엔드 개발 등 다양한 분야에서 널리 활용되고 있다.

Async/await
기능 개요
설명비동기 함수를 작성하는 컴퓨터 프로그래밍 구문. 이를 통해 호출 스레드가 차단되지 않고 다른 작업을 수행할 수 있다.
역사 및 구현
최초 구현 언어C# (Microsoft)
최초 구현 시기2012년
특징비동기 프로그래밍의 어려움을 완화하고 코드를 단순화함.
지원 언어
지원 언어 목록C#
자바스크립트
파이썬
스위프트
러스트
스칼라

PHP (8.1 버전부터)
타입스크립트
코틀린
📚 더 읽어볼만한 페이지
  • 프로그래밍 언어 - 다중 패러다임 프로그래밍 언어
    다중 패러다임 프로그래밍 언어는 둘 이상의 프로그래밍 패러다임을 지원하며, 다양한 프로그래밍 스타일을 혼합하여 사용할 수 있도록 설계되었다.
  • 프로그래밍 언어 - 모조 (프로그래밍 언어)
    모조는 모듈러사에서 개발한 파이썬과 유사한 구문의 고성능 프로그래밍 언어로, AI 애플리케이션 개발에 초점을 맞추고 러스트의 영향을 받은 메모리 안전성을 제공하며 향후 오픈 소스로 전환될 예정이다.
  • 제어 흐름 - 프로그램 카운터
    프로그램 카운터는 CPU 내에서 다음에 실행될 명령어의 주소를 저장하는 레지스터로, 명령어 사이클의 fetch 단계에서 사용되어 명령어를 가져오고 실행 후 갱신되며, CPU 성능 향상 기술과 현대 프로그래밍 모델에 영향을 미친다.
  • 제어 흐름 - 예외 처리
    예외 처리는 프로그램 실행 중 예외 발생 시 정상적인 실행 흐름을 유지하거나 안전하게 종료하기 위한 메커니즘으로, 많은 프로그래밍 언어에서 제공하며 예외 안전성을 목표로 한다.

2. 역사

F#은 2007년에 비동기 워크플로우를 도입하여 async/await 개념의 초기 형태를 제시했다. 이는 C#의 async/await 메커니즘에 영향을 주었다.

C#은 2011년에 Async CTP에서 프로토타입 버전이 구현되었고, 2012년 C# 5.0에서 async/await를 정식으로 도입하여 널리 사용되기 시작했다.

사이먼 말로우는 2012년에 async 패키지를 개발했다.

파이썬은 2015년 버전 3.5에 `async`와 `await` 키워드를 추가하여 async/await 지원을 추가했다.

타입스크립트는 2015년 버전 1.7에 async/await 지원을 추가했다.

자바스크립트는 2017년에 ECMAScript 2017 자바스크립트 에디션의 일부로 async/await 지원을 추가했다.

러스트는 2019년 버전 1.39.0에 `async` 키워드와 `.await` 후위 연산자를 사용하여 async/await 지원을 추가했는데, 이는 2018 에디션에 도입되었다.

C++는 2020년 버전 20에 `co_return`, `co_await`, `co_yield` 키워드를 사용하여 async/await 지원을 추가했다.

스위프트는 2021년 [https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html 버전 5.5]에 `async`와 `await` 키워드를 추가하여 async/await 지원을 추가했다. 이는 `actor` 키워드를 사용한 액터 모델의 구체적인 구현과 함께 출시되었으며, async/await를 사용하여 외부에서 각 액터에 대한 접근을 중재한다.

3. 개념

`async`/`await`는 비동기 프로그래밍을 동기 프로그래밍처럼 보이게 하는 문법적 설탕(Syntactic sugar)이다. `async` 키워드는 함수를 비동기 함수로 표시하며, 이 함수는 암시적으로 프로미스(Promise) 또는 퓨처(Future)를 반환한다. `await` 키워드는 비동기 작업의 완료를 기다리는 데 사용되며, 비동기 함수 내에서만 사용할 수 있다. `await`는 프로미스가 완료될 때까지 함수의 실행을 일시 중단하고, 완료되면 결과를 반환하거나 예외를 발생시킨다.

다음은 `async`/`await` 패턴을 사용한 C# 함수의 예시이다. 이 함수는 URI에서 리소스를 다운로드하여 리소스의 길이를 반환한다.

```csharp
public async Task FindSizeOfPageAsync(Uri uri)
{
var client = new HttpClient();
byte[] data = await client.GetByteArrayAsync(uri);
return data.Length;
}
```

* `async` 키워드는 C#에 해당 메서드가 비동기적임을 나타내며, 이는 임의의 수의 `await` 표현식을 사용할 수 있고 그 결과를 프로미스에 바인딩한다는 의미이다.
* 반환 형식인 `Task`는 C#에서 프로미스 개념과 유사하며, 여기서는 `int` 타입의 결과 값을 갖도록 지정된다.
* `await` 키워드가 `Task`에 연결되어 있으면, 이 함수는 즉시 `Task`를 호출자에게 반환하고, 호출자는 필요에 따라 다른 처리를 계속할 수 있다.

C#의 `async`/`await` 패턴은 컴파일 시간에 람다 또는 연속을 사용하여 구현된다. 예를 들어, 위의 코드는 컴파일러에 의해 다음과 유사한 코드로 변환된다.

```csharp
public Task FindSizeOfPageAsync(Uri uri)
{
var client = new HttpClient();
Task dataTask = client.GetByteArrayAsync(uri);
Task afterDataTask = dataTask.ContinueWith((originalTask) => {
return originalTask.Result.Length;
});
return afterDataTask;
}
```

Python 3.5는 `async`/`await` 지원을 추가했다.

```python
import asyncio

async def main():
print("hello")
await asyncio.sleep(1)
print("world")

asyncio.run(main())
```

JavaScript의 `await` 연산자는 비동기 함수 내에서만 사용할 수 있다.

```javascript
async function createNewDoc() {
const response = await db.post({}); // doc을 전송한다
return await db.get(response.id); // id로 검색한다
}

async function main() {
try {
const doc = await createNewDoc();
console.log(doc);
} catch (err) {
console.log(err);
}
}
main();
```

C++에서는 `await`가 정식으로 C++20 드래프트에 병합되어 C++20의 일부로 정식 수리될 예정이다. 다만, 실제 C++ 키워드는 `await`가 아닌 `co_await`라는 이름이 되었다.

```c++
#include
#include

using namespace std;

future add(int a, int b)
{
int c = a + b;
co_return c;
}

future test()
{
int ret = co_await add(1, 2);
cout << "return " << ret << endl;
}

int main()
{
auto fut = test();
fut.wait();

return 0;
}
```

2019년 11월 7일, `async`/`await`가 Rust의 안정 버전에서 사용 가능하게 되었다.

```rust
// futures 크레이트를 사용하기 위해, 크레이트의 Cargo.toml 의존성 섹션에 `futures = "0.3.0"`을 정의해야 한다.

extern crate futures; // 현재, `std` 라이브러리에는 executor 가 존재하지 않는다.

// 다음과 같이 디슈가링된다.
// `fn async_add_one(num: u32) -> impl Future`
async fn async_add_one(num: u32) -> u32 {
num + 1
}

async fn example_task() {
let number = async_add_one(5).await;
println!("5 + 1 = {}", number);
}

fn main() {
// future는, 생성된 시점에서는 태스크가 시작되지 않는다.
let future = example_task(5);

// JavaScript와 달리, future는 폴링되어 처음 시작된다.
futures::executor::block_on(future);
}

4. 구현

Scala-async에서 `async`는 매크로를 사용하여 구현된다. 컴파일러는 이를 통해 유한 상태 머신 구현을 생성하는데, 이는 모나드 구현보다 효율적이지만 직접 작성하기에는 불편하다.

Scala-async는 다양한 비동기 구현을 지원할 계획도 가지고 있다.

4.1. C#

C#에서 비동기 프로그래밍은 `async` 및 `await` 키워드를 사용하여 구현된다. 이를 통해 개발자는 백그라운드 스레드를 차단하지 않고도 I/O 바운드 또는 CPU 바운드 작업을 수행할 수 있다.

* `async` 키워드는 메서드, 람다 식 또는 무명 메서드를 비동기 한정자로 표시한다. `async` 메서드는 `await` 연산자를 사용하여 호출자에게 제어를 반환하고, await된 작업이 완료될 때까지 대기한다.
* `await` 키워드는 비동기 작업(`Task` 또는 `Task`) 앞에 붙여 해당 작업이 완료될 때까지 메서드 실행을 일시 중단시킨다. 이때 스레드를 차단하지 않고 제어권을 호출자에게 반환한다.
* `Task` 또는 `Task`는 비동기 작업을 나타내는 객체이다. `Task`는 `T` 타입의 결과를 반환하는 비동기 작업을 나타낸다.

다음은 `async` 및 `await`를 사용하여 URI에서 데이터를 다운로드하고 그 길이를 반환하는 C# 메서드(함수)의 예시이다.

```csharp
public async Task FindSizeOfPageAsync(Uri uri)
{
var client = new HttpClient();
byte[] data = await client.GetByteArrayAsync(uri);
return data.Length;
}
```

* `FindSizeOfPageAsync` 메서드는 `async`로 선언되어 비동기 메서드임을 나타낸다.
* `await client.GetByteArrayAsync(uri)`는 `GetByteArrayAsync` 메서드가 완료될 때까지 기다리는 동안 호출자에게 제어권을 반환한다.
* `GetByteArrayAsync`가 완료되면 `data` 변수에 결과(바이트 배열)가 할당되고, 메서드 실행이 계속된다.
* 메서드는 다운로드한 데이터의 길이를 반환하며, 컴파일러는 이 값을 `Task`로 래핑한다.

`async` 메서드는 `void`, `Task`, `Task`, `ValueTask`, `ValueTask` 등을 반환할 수 있다. `void` 반환 형식은 주로 이벤트 처리기에 사용되며, 일반적인 경우에는 `Task`를 반환하는 것이 더 나은 예외 처리를 제공한다.

C#의 async/await 패턴은 컴파일 시간에 람다 또는 연속을 사용하여 구현되므로, 실제로는 런타임의 핵심 부분이 아니다. 컴파일러는 async/await 코드를 상태 머신으로 변환하여 비동기 작업을 효율적으로 처리한다.

4.2. F#

F#은 2007년 버전 2.0에서 비동기 워크플로우를 추가했다. 비동기 워크플로우는 CE(계산식)로 구현된다. C#의 `async`와 같은 특별한 컨텍스트를 지정하지 않고 정의할 수 있다. F# 비동기 워크플로우는 비동기 작업을 시작하기 위해 키워드에 느낌표(!)를 추가한다.

다음은 비동기 워크플로우를 사용하여 URL에서 데이터를 다운로드하는 비동기 함수 예시이다.

```f#
let asyncSumPageSizes (uris: #seq) : Async = async {
use httpClient = new HttpClient()
let! pages =
uris
|> Seq.map(httpClient.GetStringAsync >> Async.AwaitTask)
|> Async.Parallel
return pages |> Seq.fold (fun accumulator current -> current.Length + accumulator) 0
}

4.3. 파이썬

파이썬 3.5(2015)는 유리 셀리바노프(Yury Selivanov)가 작성하고 구현한 PEP 492에 설명된 async/await에 대한 지원을 추가했다.

```python
import asyncio

async def main():
print("hello")
await asyncio.sleep(1)
print("world")

asyncio.run(main())
```

Python영어 3.5는 async/await 지원을 추가했다. 더 자세한 내용은 PEP0492 (https://www.python.org/dev/peps/pep-0492/)를 참조하라.

```python
import asyncio

async def main():
print("hello")
await asyncio.sleep(1)
print("world")

asyncio.run(main())

4.4. 자바스크립트

JavaScript에서 `await` 연산자는 비동기 함수 내부 또는 모듈의 최상위 레벨에서만 사용할 수 있다. 매개변수가 프라미스인 경우, 프라미스가 해결되면 비동기 함수의 실행이 재개된다. (프라미스가 거부된 경우에는 일반적인 JavaScript 예외 처리로 처리할 수 있는 오류가 발생한다). 매개변수가 프라미스가 아닌 경우, 매개변수 자체가 즉시 반환된다.

많은 라이브러리에서 `await`와 함께 사용할 수 있는 프라미스 객체를 제공하며, 네이티브 JavaScript 프라미스에 대한 사양과 일치하는 경우이다. 그러나 jQuery 라이브러리의 프라미스는 jQuery 3.0까지 Promises/A+ 호환이 되지 않았다.

다음은 예시이다. (이 기사에서 수정됨):

```javascript
async function createNewDoc() {
let response = await db.post({}); // 새 문서를 게시합니다.
return db.get(response.id); // ID로 찾기
}

async function main() {
try {
let doc = await createNewDoc();
console.log(doc);
} catch (err) {
console.log(err);
}
}
main();
```

Node.js 버전 8에는 표준 라이브러리 콜백 기반 메서드를 프라미스로 사용할 수 있게 해주는 유틸리티가 포함되어 있다.

아래 코드는 위와 동일한 예시이다. (이 문서에서 일부 변경):

```javascript
async function createNewDoc() {
const response = await db.post({}); // doc을 전송한다
return await db.get(response.id); // id로 검색한다
}

async function main() {
try {
const doc = await createNewDoc();
console.log(doc);
} catch (err) {
console.log(err);
}
}
main();
```

👆
좌우로 밀어서 보기
(예시) 전통적인 콜백 코딩async/await 패턴 코딩

4.5. 러스트

rust
// futures 크레이트를 사용하기 위해, 크레이트의 Cargo.toml 의존성 섹션에 `futures = "0.3.0"`을 정의해야 한다.

extern crate futures; // 현재, `std` 라이브러리에는 executor 가 존재하지 않는다.

// 다음과 같이 디슈가링된다.
// `fn async_add_one(num: u32) -> impl Future`
async fn async_add_one(num: u32) -> u32 {
num + 1
}

async fn example_task() {
let number = async_add_one(5).await;
println!("5 + 1 = {}", number);
}

fn main() {
// future는, 생성된 시점에서는 태스크가 시작되지 않는다.
let future = example_task();

// JavaScript와 달리, future는 폴링되어 처음 시작된다.
futures::executor::block_on(future);
}
```
2019년 11월 7일, 안정적인 Rust 버전에서 async/await가 릴리즈되었다. Rust의 비동기 일반 함수는 `async/await`패턴에서 강력한 문법 설탕 기능으로 동기화된 특성의 반환값을 갖는 Future를 구현시켜준다. 현재 유한 상태 기계로 구현되어 있다. `async` 키워드를 사용하여 비동기 함수를 정의하고, `.await`를 사용하여 Future의 완료를 기다린다. Future 트레이트를 사용하여 비동기 작업을 나타낸다.

4.6. C++

C++에서 await (C++에서는 `co_await`로 명명됨)는 C++20에 병합되었다. `co_await`와 같은 키워드, 코루틴에 대한 지원은 GCC 및 MSVC 컴파일러에서 사용할 수 있으며, Clang은 부분적으로 지원한다.

`std::promise`와 `std::future`는 awaitable 객체처럼 보이지만, 코루틴에서 반환되고 `co_await`를 사용하여 await될 수 있도록 필요한 메커니즘을 구현하지 않는다는 점에 유의해야 한다. 프로그래머는 형식이 await될 수 있도록 반환 형식에 `await_ready`, `await_suspend` 및 `await_resume`과 같은 여러 개의 public 멤버 함수를 구현해야 한다. 자세한 내용은 cppreference에서 확인할 수 있다.

C++에서 await는 정식으로 C++20 드래프트에 병합되어 C++20의 일부로 정식으로 수리될 예정이다. 다만, 실제 C++ 키워드는 `await`가 아닌 `co_await`라는 이름이 되었다. MSVC 컴파일러와 Clang 컴파일러는 적어도 어떤 형태의 `co_await`를 이미 지원하고 있으며, GCC는 아직 지원하지 않는다.

4.7. 스위프트

SE-0296에서 설명하는 대로 스위프트 5.5 (2021)에 async/await 지원이 추가되었다.

```swift
func getNumber() async throws -> Int {
try await Task.sleep(nanoseconds: 1_000_000_000)
return 42
}

Task {
let first = try await getNumber()
let second = try await getNumber()
print(first + second)
}

5. 장점 및 비판

`async`/`await` 패턴은 자체 런타임을 가지고 있지 않거나 제어하지 않는 언어의 언어 설계자들에게 특히 매력적인데, 컴파일러에서 상태 머신으로 변환하는 것만으로 구현될 수 있기 때문이다.

`async`/`await` 패턴을 지원하는 언어의 큰 장점은 비동기, 논블로킹 코드를 최소한의 오버헤드로 작성할 수 있으며, 기존의 동기 블로킹 코드와 거의 동일하게 보인다는 것이다. 이러한 애플리케이션은 그래픽 사용자 인터페이스를 제공하는 프로그램부터 게임, 금융 애플리케이션 등 매우 확장 가능한 상태 저장 서버 측 프로그램에 이르기까지 다양하다.

5.1. 장점

`async`/`await`는 비동기 코드를 마치 동기 코드처럼 작성할 수 있게 해주어 코드의 가독성과 유지보수성을 크게 향상시킨다. 이는 복잡한 콜백 함수(Callback Hell)의 사용을 줄여 코드의 복잡성을 낮추고, 개발자가 비동기 프로그래밍을 더 쉽게 이해하고 사용할 수 있도록 돕는다.

특히, `await`는 메시지 전달 프로그램에서 비동기 코드를 작성하는 가장 좋은 방법으로 꼽히며, 기존의 동기 블로킹 코드와 유사한 형태로 작성할 수 있다는 점, 가독성이 높고, 코드의 양을 최소화할 수 있다는 장점이 있다.

C#, JavaScript 등 많은 프로그래밍 언어에서 `async`/`await` 패턴을 지원하며, 이를 통해 개발자는 비동기 프로그래밍을 더욱 효율적으로 수행할 수 있다. 예를 들어, C#에서는 `async` 키워드를 사용하여 메서드를 비동기적으로 선언하고, `await` 키워드를 사용하여 비동기 작업의 완료를 기다릴 수 있다.

```csharp
public async Task FindSizeOfPageAsync(Uri uri)
{
var client = new HttpClient();
byte[] data = await client.GetByteArrayAsync(uri);
return data.Length;
}
```

위 코드에서 `await` 키워드는 `GetByteArrayAsync` 메서드가 완료될 때까지 기다린 후, 다음 코드를 실행한다. 이처럼 `async`/`await`를 사용하면 비동기 코드를 동기 코드처럼 작성할 수 있어 코드의 가독성이 높아진다.

결과적으로, `async`/`await`는 프로그램의 성능을 향상시키는 동시에, 개발자가 코드를 더 쉽게 이해하고 관리할 수 있도록 돕는 강력한 도구이다.

5.2. 비판

`async`/`await` 비판자들은 이 패턴이 주변 코드도 비동기적으로 만드는 경향이 있고, 그 전염성으로 인해 언어의 라이브러리 생태계가 동기 및 비동기 라이브러리와 API로 분리되는 경향이 있다고 지적하며, 이 문제를 종종 "함수 색상"이라고 부른다. 이 문제를 겪지 않는 `async`/`await`의 대안을 "무색"이라고 부른다. 무색 설계의 예로는 Go의 고루틴과 Java의 가상 스레드가 있다.

`await`가 비판받을 때에는, `await`가 주변 코드도 비동기적으로 만드는 경향이 있다는 점이 종종 지적된다. 한편, 이 코드의 전염성("좀비 바이러스"에 비유되기도 함)은 모든 종류의 비동기 프로그래밍에 고유하다고 주장되어 왔기 때문에, 이 점에 관해서는 `await`에만 특유한 것은 아니다.

6. 한국 개발 생태계

async/await 패턴은 Node.js를 이용한 백엔드 개발에서 활발하게 사용되고 있다. 자바스크립트를 기반으로 한 Node.js 환경에서는 비동기 처리가 필수적이기 때문이다.

Python의 asyncio를 이용한 비동기 프로그래밍도 점차 증가하는 추세이다. 웹 프론트엔드 개발에서도 API 호출 등 비동기 처리를 위해 async/await를 적극적으로 활용하고 있다.

다음은 전통적인 콜백 코딩과 async/await 패턴 코딩을 비교한 예시이다.

👆
좌우로 밀어서 보기
전통적인 콜백 코딩async/await 패턴 코딩