[Unity - Trace Memory] IsObjectMonoBehaviour can only be called from the main thread

전경진·2024년 8월 23일
0

현재 Unity에서 TraceMemory라는 오프라인 기반 보드 게임을 제작 중이다.
본격적으로 게임 로직에 들어가기 앞서 방 입장을 구성하는 단계에서 문제가 발생했다.

발생한 문제의 바탕은 다음과 같다.

  • 사용자의 이름, 프로필 사진, 성별 등을 입력받고 프로필 사진을 제외한 나머지를 Local Player Custom Properties에 저장한다.
  • 프로필 사진까지 저장하기에는 PhotonNetwork에서 지원하는 저장소가 용량이 부족하므로, 프로필 사진은 Firebase의 Storage에 저장하고 url을 통해 Load하기로 한다.

다음 코드는 Image를 불러오는 ImageLoader 클래스의 일부이다.


	// URL에서 이미지를 로드하는 메서드
    public void LoadImageFromUrl(Image targetImage, string imageUrl)
    {
        try
        {
            StartCoroutine(DownloadImage(targetImage, imageUrl));
        }
        catch (Exception ex)
        {
            Debug.LogError("An error occurred: " + ex.Message);
        }
    }

    // 코루틴을 통해 URL에서 이미지를 다운로드하고 UI에 적용
    private IEnumerator DownloadImage(Image targetImage, string imageUrl)
    {
        UnityWebRequest request = UnityWebRequestTexture.GetTexture(imageUrl);
        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
        {
            Debug.LogError(request.error);
        }
        else
        {
            Texture2D texture = DownloadHandlerTexture.GetContent(request);
            targetImage.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
        }
    }
    
    public void LoadPlayerImage(Image targetImage, string fileName)
    {
        // Firebase에서 URL 가져오기
        imageUploader.GetImageUrl(fileName, (imageUrl) =>
        {
            // Firebase로부터 가져온 URL을 사용하여 이미지를 로드
            LoadImageFromUrl(targetImage, imageUrl);
        });
    }
    

아래는 imageUploader.GetImageUrl이다.

public void GetImageUrl(string fileName, System.Action<string> onUrlReceived)
{
    StorageReference storageRef = storage.RootReference;
    StorageReference imageRef = storageRef.Child("images/" + fileName);

    imageRef.GetDownloadUrlAsync().ContinueWith((Task<System.Uri> task) => {
        if (!task.IsFaulted && !task.IsCanceled)
        {
            System.Uri downloadUrl = task.Result;
            onUrlReceived(downloadUrl.ToString());
        }
        else
        {
            Debug.LogError(task.Exception.ToString());
        }
    });
}

이 코드에서 문제는 LoadImageFromUrl 메서드에서 코루틴에 진입하지 못하고 본 게시글의 제목과 같은
IsObjectMonoBehaviour can only be called from the main thread 오류가 발생한다는 것이다.

이 문제는 비동기 작업과 UI 관련 작업이 서로 다른 스레드에서 실행될 때 발생하는 전형적인 문제이다
기본적으로 Unity에서는 UI 작업은 반드시 메인 스레드에서 수행되어야 하기 때문에, 다른 스레드에서 UI를 조작하려고 하면 오류가 발생할 수 있다. 그것이 이 오류 발생의 원인으로 보인다.

그렇다면 어디서 스레드의 문제가 발생한 것일까?

'GetImageUrl' 메서드의 구조

해당 메서드는 Firebase Storage의 비동기 메서드인 imageRef.GetDownloadUrlAsync()를 사용하여 이미지를 저장한 URL을 가져온다. 비동기 메서드는 요청이 완료되기까지 메인 스레드에서 실행이 멈추지 않고 계속 진행되도록 설계되어 있다. 이로 인해 Firebase SDK에서 사용하는 네트워크 요청이나 파일 다운로드 등의 작업이 메인 스레드를 차단하지 않는다.

메서드의 흐름은 다음과 같다 :

  1. imageRef.GetDownloadUrlAsync() 호출: Firebase Storage에서 해당 파일의 다운로드 URL을 가져온다. 이 작업은 비동기적으로 이루어지며, 작업이 완료될 때까지 메인 스레드를 차단하지 않는다.

  2. .ContinueWith() 사용: Firebase SDK의 비동기 작업이 완료되면 ContinueWith()가 실행된다. 이 콜백은 비동기 작업이 끝난 후 호출되며, task를 통해 작업 결과를 처리한다.

  3. 콜백 내에서 URL 반환: 비동기 작업이 성공하면 task.Result를 통해 downloadUrl을 얻고, 이 값을 onUrlReceived 콜백에 전달한다.

문제의 스레드

GetDownloadUrlAsync()와 같은 비동기 메서드는 Unity의 메인 스레드가 아닌 별도의 스레드에서 실행될 수 있다. Firebase SDK는 내부적으로 작업을 비동기적으로 처리하는데, 이 작업은 Unity의 메인 스레드와는 별도로 운영된다. 따라서 ContinueWith 블록은 기본적으로 메인 스레드가 아닌 비동기 작업을 처리하는 스레드에서 실행된다.

문제가 발생한 부분은 다음과 같다 :

  • 비동기 콜백 스레드: ContinueWith() 안의 코드(즉, onUrlReceived(downloadUrl.ToString()))는 Firebase의 비동기 작업이 완료된 후 실행되는데, 이 시점에서 해당 코드가 메인 스레드가 아닌 별도의 스레드에서 실행될 수 있다.

  • onUrlReceived 콜백 실행: onUrlReceived는 LoadPlayerImage 메서드에서 정의된 익명 함수로, 이 함수가 호출되면 LoadImageFromUrl 메서드를 호출하게 된다. 하지만 이때 onUrlReceived가 실행되는 스레드가 메인 스레드가 아니면, LoadImageFromUrl도 메인 스레드가 아닌 스레드에서 실행된다.

  • UI 업데이트 문제: LoadImageFromUrl가 비동기 콜백을 통해 호출되면서, UI 작업이 메인 스레드가 아닌 스레드에서 실행되려고 시도되었고, 이로 인해 UI 관련 작업에서 스레드 충돌이 발생했다. Unity는 UI 업데이트 작업을 반드시 메인 스레드에서 수행해야 하기 때문에 이 문제가 발생한 것이다.

해결 방법

본 문제의 원인을 요약하자면, 다음과 같다.
Unity에서는 UI 업데이트 작업을 반드시 메인 스레드에서 수행해야 한다. 그러나, Firebase SDK의 메서드가 비동기적으로 실행되었고, 그 안에서 onUrlReceived 콜백이 실행되었다. 콜백의 내부에는 UI의 업데이트를 포함한 LoadImageFromUrl가 있으며, 이 또한 메인 스레드가 아닌 스레드에서 실행되게 된다.
이는 UI 업데이트 작업을 메인 스레드에서가 아닌 스레드에서 실행하려는 시도이기 때문에 오류가 발생한다.

그렇다면 문제는 다음과 같은 방법으로 해결할 수 있다 :

  • onUrlReceived 콜백 내부에서 실행되는 함수인 LoadImageFromUrl를 메인 스레드에서 실행될 수 있도록 한다.
  • 이때, UnityMainThreadDispatcher.Enqueue()가 사용되었으며, UnityMainThreadDispatcher 클래스는 아래의 코드와 같다.
using System;
using System.Collections.Generic;
using UnityEngine;

public class UnityMainThreadDispatcher : MonoBehaviour
{
    private static readonly Queue<Action> _executionQueue = new Queue<Action>();

    public void Update()
    {
        lock (_executionQueue)
        {
            while (_executionQueue.Count > 0)
            {
                _executionQueue.Dequeue().Invoke();
            }
        }
    }

    public static void Enqueue(Action action)
    {
        if (action == null)
            throw new ArgumentNullException("action");

        lock (_executionQueue)
        {
            _executionQueue.Enqueue(action);
        }
    }
}

요약

  • Unity에서 UI 업데이트는 메인스레드에서 실행되어야 한다. 그렇기 때문에 비동기 메서드 내부에서 실행되려는 시도가 있다면 오류가 발생할 수 있다.

  • 본 게시글에서는 Firebase에서 URL을 비동기적으로 가져오는 GetDownloadUrlAsync와 이후에 실행해야 할 일을 정의하는 ContinueWith 콜백에서 UI 업데이트를 이끌어냈기 때문에 문제가 발생하였다.

  • 해결: Firebase 비동기 작업의 콜백이 메인 스레드에서 실행되도록 보장하기 위해 UnityMainThreadDispatcher.Enqueue()를 사용하여 LoadImageFromUrl 메서드를 메인 스레드에서 안전하게 호출했다.

profile
전경진입니다.

0개의 댓글