글 작성자: Doublsb

실무에서 코루틴 말고 UniTask를 쓰는 빈도가 점점 늘어나고 있다.
편하고, 명확해지고, 코루틴과 달리 가비지를 만들지 않아 안 쓸 이유가 없다!

하지만 쉬운 메서드만 사용해보고 본격적으로 공부해 본 적은 없었기에, 이번 글에서는 문서를 읽으며 정리해보고자 한다.

 


 

UniTask는 메모리 할당이 제로?

 

코루틴의 경우, 잦은 새 객체 생성과 메모리 할당으로 불필요한 메모리를 차지하는 단점이 있다.

 

iEnumerator Function(float time)
{
  while(true)
  {
    Debug.Log("Print");
    yield return new WaitForSeconds(1f);
  }
}

 

 

위 코드에서는 1초마다 새로운 WaitForSecond 객체가 생성되며, 해당 객체는 1초가 지나기 전까지 메모리에 남는다.

이를 최적화 하기 위해 객체를 캐싱하여 쓰는 방법이 있지만, UniTask를 알고 난 뒤에는 굳이 그럴 필요가 없다.

 

public async UniTaskVoid Wait()
{
  while(true)
  {
    Debug.Log("Print");
    await UniTask.Delay(TimeSpan.FromSeconds(1));
  }
}

 

문서에 따르면 UniTask는 메모리 할당 제로를 지향한다.
대기를 수행하는 태스크 개체의 풀링을 지원하며 박싱이 발생하지 않도록 신경써서 개발되었기 때문이다.

그런 연유로 메모리 문제에서 자유로우므로, 즐겁게 사용하도록 하자.

 

 

UniTask는 코루틴보다 아름답다

 

코루틴으로 네트워크 로직을 개발해야 한다고 생각해보자.

작업 완료시까지 대기 후, 패킷이 제대로 도착했는지 체크하고, 확인되면 콜백으로 클라이언트에게 알려 줄 것이다.

 

그러나 이건 효율적인가? 아래 예시의 네트워크 코드를 살펴보자.

 

//콜백 함수를 작성하는 코루틴
IEnumerator GetData(Action<string> callBack)
{
  var request = UnityWebRequest.Get(url);
  yield return request.SendWebRequest();
  callBack.Invoke(request.downloadHandler.text);
}
//콜백을 작성하지 않는 명확한 UniTask
async UniTask<string> GetData()
{
  try
  {
    var request = UnityWebRequest.Get(url);
    var response = await request.SendWebRequest();
    return response.downloadHandler.text;
  }
  catch(Exception e)
  {
    Debug.LogError(e);
  }
}

async void Test()
{
  var data = await GetData();
}

 

코루틴은 호출 시마다 매번 콜백을 작성해야 하며, 이 도중에 오류가 발생할 경우 에러 처리가 힘들다.

그러나 UniTask로 작성하면 리턴 타입으로 데이터를 받아올 수도 있고, 에러 처리가 명확해지며 코드가 쉽게 재사용 가능해진다.

 

 

주의사항

 

별개로 주의사항이 있는데, 문서에서는 다음 작업을 수행하면 안 된다고 경고하고 있다.

 

var task = UniTask.DelayFrame(10);
await task;
await task; // NG, throws Exception

 

위의 코드에서 task는 두 번 기다릴 수 없다.

태스크가 1회 대기 후 최적화를 위해 풀링 상태로 돌아가기 때문이다. 자세한 불가능 조건을 서술하자면 아래와 같다.

  • 객체는 여러 번 대기할 수 없다.
  • AsTask를 여러 번 부를 수 없다.
  • 작업이 완료되지 않았는데 .Result나 .GetAwaiter().GetResult()를 사용할 수 없다.

이런 조건에 있는 경우에는 예외가 리턴되므로 주의해야겠다.

 

 

UniTask로 기다리기 : await asyncOperation

 

public async UniTaskVoid LoadManyAsync()
{
    // parallel load.
    var (a, b, c) = await UniTask.WhenAll(
        LoadAsSprite("foo"),
        LoadAsSprite("bar"),
        LoadAsSprite("baz"));
}

async UniTask<Sprite> LoadAsSprite(string path)
{
    var resource = await Resources.LoadAsync<Sprite>(path);
    return (resource as Sprite);
}

 

UniTask에는 다양한 대기 방법이 존재하는데,

NextFrame, WaitUntil, WhenAll, WhenAny 등의 다양하고 유용한 기능들이 정의되어 있다.


예시 코드는 foo, bar, baz 스프라이트를 모두 로딩할 때까지 기다리는 Task이다. 유용함의 극치!

UniTask의 factory 메서드 문서는 여기에서 볼 수 있다. 너무 많아서 모든 메서드를 다 서술하며 정리할 수는 없을 것 같다.

 

 

UniTask 취소하기 : .WithCancellation(CancellationToken)

 

// this CancellationToken lifecycle is same as GameObject.
await UniTask.DelayFrame(1000, cancellationToken: this.GetCancellationTokenOnDestroy());

 

코루틴의 경우 참조 중인 오브젝트가 비활성화되면 코루틴도 함께 꺼진다.

그러나 UniTask는 관련된 오브젝트를 삭제하더라도 자동으로 중단되지 않으므로, 만들 때 조심해야 한다.

 

이 때 사용하는게 CancellationToken이다. 비교적 취소 방법이 간단하다.
위처럼 오브젝트가 사라질 때 같이 태스크를 취소해달라고 예약을 걸어 놓으면, 코루틴처럼 취소되도록 이용할 수 있다.

 

var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 5sec timeout.

try
{
    await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(cts.Token);
}
catch (OperationCanceledException ex)
{
    if (ex.CancellationToken == cts.Token)
    {
        UnityEngine.Debug.Log("Timeout");
    }
}

 

TimeOut도 만들 수 있는데, 코드는 CancelAfterSlim으로 5초의 타임아웃을 걸어 놓았다.

5초가 지나버릴 경우 try-catch문에서 오류가 발생하며, 타임아웃을 알리는 것을 확인할 수 있다.


우선 1부는 이렇게 끝내보고, 실제로 실무에서 사용하다가 문제가 생길 때 다시 돌아오겠다.

반응형