[Unity] GameObject.Find()의 성능 이슈

Arthur·2023년 9월 26일
0
post-thumbnail

작성하게된 계기


유니티 게임 팀 프로젝트를 진행하면서 PR을 날리고 서로 피드백을 주는 시스템을 진행하고 있습니다.
팀 프로젝트를 진행하던 중 팀원분이 GameObject.Find()에 대한 피드백을 해줬습니다.

작성한 코드에 대해서 한번 더 생각해보고 제가 처음에 의도했던 것이 무엇인지 정리하기 위해 이렇게 글을 작성하게 되었습니다.



팀원에게 받은 피드백


팀원분이 위와 같이 피드백을 해줬습니다.(PR 원본 링크)

우선 피드백을 받은 개선이 필요한 코드는 아래와 같습니다.


using UnityEngine;

public class Managers : MonoBehaviour
{
    private static Managers s_instance;
    
    // Managers 객체를 전역(static)으로 처리하고 한 번 생성 되었던 s_instance 리턴
    public static Managers Instance { get { Init(); return s_instance; } }

    #region Managers
    private UIManager _ui = new UIManager();
    
    public static UIManager UI { get { return Instance._ui; } }
    #endregion


    void Start()
    {
        Init();
    }

    static void Init()
    {
    	// s_instance가 null이면 객체 생성 로직 진행
        if (s_instance == null)
        {
        	// 피드백을 받은 GameObject.Find() 코드
            GameObject gameObject = GameObject.Find("@Managers");
            if (gameObject == null)
            {
                gameObject = new GameObject { name = "@Managers" };
                gameObject.AddComponent<Managers>();
            }
			
            // Managers의 GaemObject가 Destroy 되지 않도록 처리
            DontDestroyOnLoad(gameObject);
            s_instance = gameObject.GetComponent<Managers>();
            
            // Managers에 포함된 매니저들 초기화(Initialization)
            // 각 매니저들의 초기 코드를 호출합니다.
            s_instance._ui.Init();
        }
    }

	// 씬 변경 시 각 매니저에서 삭제가 필요한 객체 혹은 GameObject를 정의해놓은 함수
    public static void Clear()
    {
    	// ex) UIManager에서 씬 이동 시 삭제할 GameObjcet들을 Clear()
		s_instance._ui.Clear();
    }
}

모든 매니저(Manager) 객체들을 생성해 싱글톤으로 관리하는 Managers라는 클래스입니다.


코드의 최초 의도는 아래와 같이 정리해봤습니다.

  • 메인 씬이 로드될 때 Managers의 GameObject만 생성되면, Managers에 생성된 매니저들이 같이 객체가 생성되는 구조입니다.
    • 코드에 UIManager가 같이 생성되는 매니저들의 예시 중 하나입니다.
  • DontDestroyOnLoad()를 사용해서 게임 진행 중에는 Destory() 되지 않도록 구현
  • 싱글톤을 사용해서 하나의 Managers 객체만 전역적으로 사용하도록 구현

Managers.Instance.UI.ShowPopupUI();

전역적으로 처리되기 때문에 각 매니저에 정의된 메서드를 위와 같이 호출 할 수 있습니다.


기존 코드의 장점

  • 매니저들이 Managers에 정리되어 있기 때문에 어떤 매니저가 있는지 확인하기 편합니다.
  • Managers의 싱글톤 객체가 생성되면서 다른 매니저들의 초기화 코드를 실행할 수 있습니다.
  • 매니저들의 GameObject를 직접 생성해서 인스텍터 참조를 넣어주는 소요가 줄어듭니다.

정리하면 Managers 싱글톤 객체가 생성되면 다른 매니저들의 객체 생성과 초기화 코드가 연속적으로 진행되는 것이 장점입니다.

하지만 당연히 코드가 적고 소규모의 게임에는 괜찮겠지만,
매니저(Manager), GameObject들이 많아지면 문제가 발생할 수 있는 코드입니다.



기존 코드 개선이 필요한 것


개선해야 할 점과 우려되는 것들을 하나씩 정리해봤습니다.

1) GameObject.Find()의 성능 이슈

GameObject.Find()는 상당히 직관적인 역할을 하는 함수입니다.
하지만 마이크로소프트 공식 문서(MS Docs)에도 해당 함수는 비용이 많이 드는 함수라고 정의되어 있습니다.

C#을 언어를 만든 회사에서도 권유하지 않는 방식이라 문제가 되는 것은 쉽게 인지했습니다.

하지만 실직적으로 어떤 부분이 문제가 되는지 좀 더 찾아봤습니다.

그래서 찾은 문제점이 잘 정리되어 있는 자료의 링크입니다.
(영문으로 된 자료입니다.)

핵심이 되는 부분을 변역기를 사용한 후 캡처한 사진입니다.

정리하면

  • GameObject.Find는 유니티의 씬에 있는 모든 GameObject를 크롤링해서 매칭되는 이름을 찾습니다.
  • 문자열로 입력하기 때문에 대소문자를 잘못 입력하거나 오타가 있으면 오류가 발생합니다.
  • 중복된 이름의 GameObject가 있으면 어떤 GameObject를 가져올지 몰라 신뢰성이 떨어집니다.
  • GameObject.Find() 함수의 오류는 런타임 환경에서 발생합니다.
    • 컴파일 과정에서 오류를 확인하지 못하고 런타임 환경에서 발생해서 더욱 버그를 잡기 힘들어질 수 있습니다.
  • 문자열로 입력하기 때문에 IDE를 사용해서 자동 완성 기능을 사용할 수 없습니다.

위 글에는 GameObject.Find()를 Start() 함수에도 사용하지 말라고 당부합니다.
성능에 문제가 되고, 런타임 오류가 발생할 수 있는 코드를 한 번 호출한다고 안심하지 말라는 것 같습니다.


GameObject.Find()를 사용하지 말아야 할 이유를 명확하게 알게 되었습니다.
그러면 어떤 것을 대신 사용해야 할까요?

  • GameObject.FindWithTag
  • Inspector를 통한 공개 멤버 할당
  • transform.Find

위 글에 어떤 대안 방법이 좋은 지 예제 코드와 함께 설명이 포함되어 있습니다.
꽤 많은 내용이 있어서 링크에 들어가서 확인해 보시는 것을 추천드립니다.



2) Managers와 다른 매니저들간의 결합도 이슈

제일 큰 문제는 프로젝트의 규모가 커지면 Managers에 추가되는 매니저들도 점점 늘어날 것입니다.
그러면 Managers 싱글톤 객체 생성 시 초기로 실행되는 매니저들의 코드도 많아지게 됩니다.

다른 매니저들의 객체 생성과 초기 코드 실행에 대한 책임이 Managers에 너무 종속적인 것이 문제입니다.

Managers는 각 매니저들의 Init 코드를 전부 호출해줘야 합니다.

    static void Init()
    {
        if (s_instance == null)
        {	
        	// ... Managers 생성 로직 생략 ...
            
            s_instance._ui.Init();
            s_instance._map.Init();
            s_instance._sound.Init();
            s_instance._resource.Init();
            s_instance._data.Init();
            // ... 계속 추가되는 Init() 함수 호출 ...
        }
    }

Managers와 다른 매니저들의 결합도로 인해 씬에서 필요 없는 매니저의 Init() 함수가 실행되는 문제도 발생할 수 있겠다고 생각했습니다.

각 매니저들의 Init() 혹은 Clear()는 각자의 책임으로 실행하게 하고,
매니저 객체의 생성도 필요한 부분에서 호출하는 방식으로 진행하는 방식으로 결정하게 되었습니다.



3) 매니저들의 MonoBehaviour 사용에 대한 제한

문제가 되는 코드는 아래와 같습니다.

    #region Managers
    private UIManager _ui = new UIManager();
    
    public static UIManager UI { get { return Instance._ui; } }
    #endregion

최초에 위와 같이 코드를 작성한 이유는 각 매니저들이 MonoBehaviour의 사용에 대한 필요성을 느끼지 못했기 때문입니다.
Managers에서 Start()함수를 통해 각 매니저들의 Init()을 진행하기 때문입니다.

사실 Managers에 매니저 객체를 생성하고 각 Manager에 MonoBeHaviour를 사용해도 작동은 합니다.

하지만 유니티 상에서 경고(Warnning)문구가 뜨게 됩니다.
그리고 MonoBehaviour가 필요한 매니저들은 제한사항이 생기기 때문에 개선해보기로 했습니다.

위 경고 문구와 MonoBehaviour 사용 제한에 대한 답변이 잘 작성된 링크입니다.
(영문으로 작성되어 있습니다.)

C#에서는 문제가 되지 않지만 유니티에서는 제한이 있어서 경고가 뜨는 것을 알 수 있습니다.

MyScript script = obj.AddComponent<MyScript>();

new를 사용해 직접 객체를 생성하는 것이 아닌 AddComponent를 사용하는 것을 권장하고 있습니다.



개선된 코드


public class GameManager : MonoBehaviour
{
    private static GameManager _instance;
    public static GameManager Instance { get { Init(); return _instance; } }


    void Start()
    {
        Init();
    }

    static void Init()
    {
        if (_instance == null)
        {
            _instance = (GameManager)FindObjectOfType(typeof(GameManager));

            if (_instance == null)
            {
                GameObject gameObject = new GameObject { name = "@GameManager" };
                _instance = gameObject.AddComponent<GameManager>();
                DontDestroyOnLoad(gameObject);
            }
        }
    }
}
  • Managers를 GameManager로 변경했습니다.
  • GameObject.Find()를 FindObjectOfType() 함수로 변경했습니다.
  • Managers에서 다른 매니저의 객체의 생성을 관리하던 코드를 제거했습니다.

제거하고 보니까 Managers가 담당했던 책임이 대부분 사라졌습니다.
현재는 작성된 코드가 거의 없지만 GameManager에서 유저의 게임 시간과 같은 것들을 관리할 예정입니다.



작성하면서 느낀점


쉽게 생각하고 사용성이 좋다고 적용했던 코드들이 문제의 원인이 된다는 것을 알게되었습니다.

어떤 함수를 사용할 때 다른 대안 방법이 없는지 알아보고 비교해 봐야 한다는 것을 알게되었습니다.

하지만 실제 해당 부분이 발생하는 문제를 직접 경험해보지 못했다는 것이 아쉽습니다.
다음에 기회가 된다면 실제 부하 테스트나 게임 오브젝트를 많이 늘려서 확인을 해보고 싶습니다.



참고 자료


  • MS Docs - Unity에 대한 성능 추천 사항 => 링크
  • [Unity] C# MonoBehaviour Singleton 유니티 싱글톤 만드는 방법 => 링크
  • [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 => 링크
  • Regarding GameObject.Find => 링크
profile
기술에 대한 고민과 배운 것을 회고하는 게임 서버 개발자의 블로그입니다.

0개의 댓글