async와 await 키워드를 사용하면 현재 코드가 어떤 작업을 대기하고 있는지 깔끔하고 명확하게 표현할 수 있습니다. Callback Hell도 피할 수 있으며, 비동기 작업이 반환한 결과값을 await을 통해 바로 전달받을 수 있습니다.
유니티는 이전 버전에서도 async/await 키워드를 지원했습니다. 다만 다음 문제가 있어 자주 사용되지 않았습니다.
· 유니티 게임 오브젝트의 라이프사이클이나, 플레이어 루프의 각 이벤트 타이밍을 감지할 수 없었습니다.
· async Task 메서드를 호출할 때마다 메모리 할당이 발생합니다.
· AsyncOperation들을 await 할수 없었습니다.
이러한 문제는 Awaitable 타입을 통해 해결할 수 있습니다. 같은 코드에서 다음과 같이 Task 타입 대신 Awaitable 타입을 사용하여 async 메서드를 작성함으로써 위 문제를 해결하면서도 async, await 키워드를 사용할 수 있습니다.
public async Awaitable GetAssetAsync(string assetName) { var assetLoadOperation = Addressables.LoadAssetAsync(assetName); await assetLoadOperation.Task; return loadedAsset = assetLoadOperation.Result; }
public async void Start() { var loadedAsset = await GetAssetRoutine("boat"); await DoSomethingAsync1(loadedAsset); await DoSomethingAsync2(loadedAsset); await DoSomethingAsync3(loadedAsset); } |
Awaitable 타입 소개
Awaitable 타입은 유니티에서 Task를 대체하여 비동기 작업을 표현하는 타입으로, 유니티가 추적할 수 있는 타입입니다. Awaitable은 Task와 비교하여 유니티 개발 환경에 최적화되어 설계되었습니다.
Awaitable 풀링
Awaitable 타입은 기존 Task 타입과 달리 풀로 관리됩니다. 따라서 async 메서드를 실행할 때마다 Task 타입을 위한 메모리 할당이 발생하는 문제를 해결합니다. 따라서 더 나은 성능으로 async 메서드를 사용할 수 있습니다.
단 동일한 Awaitable 인스턴스를 여러 번 await 해서는 안된다는 것에 주의해야 합니다. 예를 들어 Task 타입을 사용한 다음 타입을 살펴봅시다.
async Task TaskExample() { Debug.Log("Starting Task..."); await Task.Delay(1000); Debug.Log("Continuing Task..."); }
async Task StartTaskExample() { var taskInstance = TaskExample(); await taskInstance; // 첫 번째 await await taskInstance; // 두 번째 await, 정상 동작 } |
StartTaskExample()을 실행했을때 같은 Task 인스턴스를 두번 await해도 코드는 정상 동작합니다. 첫번째 await에서 해당 Task가 이미 완료된 상태이므로 두번째 await은 즉시 실행이 완료됩니다.
이번에는 Awaitable을 사용한 경우를 살펴봅시다.
async Awaitable AwaitableExample() { Console.WriteLine("Starting Awaitable..."); await Awaitable.WaitForSecondsAsync(1f); Console.WriteLine("Continuing Awaitable..."); }
async Awaitable StartAwaitableExample() { var awaitableInstance = AwaitableExample(); await awaitableInstance; // 첫 번째 await
// 이 시점에서 awaitableInstance는 풀로 반환되었을 수 있습니다. await awaitableInstance; // 두 번째 await, 비정상 동작 (풀에서 재사용된 인스턴스 가능성) } |
StartAwaitableExample()을 실행했을 때, 처음 await으로 완료된 awaitableInstance는 풀로 돌아가게 됩니다.
이미 await한 awaitableInstance를 다시 await하려고 할 때, 해당 Awaitable 인스턴스가 사실 다른 곳에 실행된 async 메서드의 반환 값으로 이미 사용 중일 수 있습니다.
이로 인해 예상하지 못한 동작이 일어날 수 있습니다. 예를 들어 awaitableInstance가 본래 표현하던 작업이 완료가 되었음에도, 두 번째 await에서는 awaitableInstance가 다른 비동기 메서드로 인한 작업을 표현하고 있는 상황을 고려해야 합니다.
따라서 본래라면 두 번째 await 코드 실행은 즉시 완료되어야 하지만, 그러지 못하고 다른 작업을 대기하게 될 수 있습니다.
이는 정상적인 동작이 아니며, 최악의 경우 데드락 상태를 유발할 수 있습니다. 이 동작을 좀 더 자세히 살펴봅시다. 다음과 같은 코드 작성을 가정해보겠습니다.