본문 바로가기

Unity Engine/기초 테크닉

[Unity] 유니티 오브젝트의 null 확인

728x90
728x90

Preface: UnityEngine.Object

유니티에서의 모든 게임 오브젝트는 UnityEngine.Object로부터 상속을 받는다.

 

UnityEngine.Object는 유니티 엔진에서 생성된 C++ 내부 구현 객체와 C#에서 해당 객체의 포인터를 참조하는 인터페이스 클래스로 구성되어 있다.

 

여기서 C++은 unmanaged 언어라 프로그래머가 객체의 메모리 해제 시점을 결정할 수 있지만 C#은 managed 언어이기 때문에 메모리 해제를 GC에 맡겨야 하는 차이가 있다.

UnityEngine.Object의 파괴

유니티로 코딩을 하면서 UnityEngine.Object(혹은 그를 상속하는)를 파괴하려고 하면 Destroy() 또는 DestroyImmediate()를 호출하게 된다.

 

이때 위에 언급한 두 언어의 특성 차이로 인해 유니티 엔진에서는 C++ 구현 객체만 직접 해제하도록 하고 C# 인터페이스는 GC가 언젠간 해제한다는 믿음으로 방치하게 된다.

UnityEngine.Object의 null 확인

유니티에서의 모든 게임 오브젝트는 UnityEngine.Object로부터 상속을 받는다. 그리고 "일단은" 이러한 게임 오브젝트들의 파괴 여부를 확인할 수 있도록 되어있다.

 

MonoBehaviour를 상속하는 Box 클래스를 가진 BoxBox 오브젝트가 있을 때 null 확인을 해보겠다.

 

using UnityEngine;

public class TestNull : MonoBehaviour
{
    Box m_Box;

    void Start()
    {
        m_Box = FindObjectOfType<Box>();

        Debug.Log(m_Box == null);
        DestroyImmediate(m_Box);
        Debug.Log(m_Box == null);
    }
}

 

해당 클래스를 가진 오브젝트를 만들고 플레이 모드로 진입하면 아래와 같은 로그를 볼 수 있다.

 

 

이 코드는 적절히 큰 수만큼 실행해도 항상 같은 결과를 볼 수 있다. 위에서 배운 이론대로라면 C# 인터페이스는 당장 해제될 수 없으므로 아직 살아있는 상태일 수 있기 때문에 True가 나올 수 없을 것이다. 하지만 True가 나온다. 왜?

== 연산자 오버로딩을 통한 Fake Null

사실 유니티 엔진은 개발의 편의성을 위해 일단 Destroy를 하면 null이 될 수 있도록 처리를 했다. 만약 그렇지 않더라면 오브젝트의 파괴 여부를 확인하기 위해 더 귀찮은 과정이 생겼을 수도 있다. 그리고 이를 fake null이라고 부른다.

 

Fake null은 UnityEngine.Object에서 == 연산자를 오버로딩하여 구현한다.

 

 

추적을 통해 UnityEngine.Object의 코드를 살펴보면 == 와 != 를 오버로딩한 것을 볼 수 있다.  오버로딩된 ==,!= 연산자는 null과 비교할 때 다음 연산을 거치는 것으로 알려져 있다.

 

  1. C# 클래스가 해제되었는지 아닌지 살펴본다. 해제되어 있으면 null인 것이다.
  2. 만약 C# 클래스가 해제되지 않았다면 C++ 내부 구현 객체가 해제되어 있는지 살펴본다. 해당 객체가 해제되었으면 null이다.

이 구현으로 인해 실제 개발에서 몇몇 제약이 생긴다.

따라서 UnityEngine.Object의 null 확인은 상당히 무겁다.

기존과 비교해 한 단계 더 추가되었으므로 UnityEngine.Object의 null 확인은 무거운 작업이다. 따라서 가능하면 null 확인을 하지 않을 것을 권고하고 있다.

C#의 일부 문법을 사용할 수 없다.

UnityEngine.Object의 인터페이스를 둘러보면 알 수 있지만 is,?? 와 같은 연산자는 오버로딩을 하지 않았다. 따라서 ==,!= 외의 연산자로 null 확인을 제대로 할 수 없다는 의미가 된다. UnityEngine.Object를 상속하는 오브젝트에서는 가급적 ==,!=를 제외한 null 관련 연산자를 사용하면 안 된다. 일부 포스트에서는 is 연산자가 빠르다는 말이 있지만 당연히 오버로딩이 되지 않은 거라 쓸모가 없고 버그가 생길 요인이 된다.

Null 비교를 조금 더 빠르게 하는 법

하지만 실제 개발을 하다 보면 null 비교는 안 할 수가 없기 마련이다. 이를 극복하는 방법들이 인터넷에서 전해지고 있는데 이 글에서는 몇 가지를 소개한다.

OnDestroy()가 호출될 때 표시하고 is null과 같이 쓰기

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Box : Observer
{
    public bool IsNull
    {
        get => m_IsNull;
    }

    bool m_IsNull = true;

    void Awake()
    {
        m_IsNull = false;
    }

    void OnDetroy()
    {
        m_IsNull = true;
    }
}

// example: (obj is null || obj.IsNull)

 

아이디어는 Destroy 되었다는 표시를 멤버로 남기고 C# 클래스만 확인하는 is null과 같이 쓰면 C++ 객체까지 갈 볼 필요가 없다는 것이다. 하지만 OnDestroy()는 프레임이 끝난 후 호출되므로 Destroy 하는 프레임에서 null 체크를 같이 해야 한다면 이는 무용지물이다.

Destroy() 호출 후 명시적으로 null을 대입하고 is null 쓰기

Destroy(go)
go = null;

if (go is null)
{
    Debug.Log("is null");
}

파괴를 명시적으로 할 수 있다면 이처럼 파괴 후 직접 null을 대입하면 is null을 활용할 수 있다.

결론

이번 포스트에서는 UnityEngine.Object의 구성을 간단히 알아보고 fake null과 그로부터 오는 성능 저하, 이를 극복하기 위한 트릭을 소개했다. Fake null의 성능 저하를 막는 방법은 특정한 상황으로만 한정되기 때문에 그 사용이 굉장히 어려울 수 있다. 하지만 적절한 규칙이 정해져 있거나 상황을 통제할 수 있으면 최적화에 도움이 될 것이다.

728x90
728x90