[Unity] 유니티 Fake Null

0시0분·2024년 2월 2일
0

Unity

목록 보기
12/24

타 프로젝트를 받아서 코드 최적화를 진행한 적이 있는데
해당 프로젝트에서는 모든 오브젝트의 null 검사를 IsNull() 이라는 함수로 하고 있었다.

public static bool IsNull(this object o)
{
    return o == null;
}

그런데 이상한 점이 있었다.

if (a == null)

if (a.IsNull())

두 값이 다르게 나오는 경우가 발생했다.


다른점이 뭔지 알수가 없어서 한참 헤맸는데
알고보니 유니티에는 "null"로 표시되는 fake Null 이라는 값이 존재했다.

1) System.Object와 UnityEngine.Object

System.Object
C# 모든 클래스의 조상
UnityEngine.Object
유니티에 존재하는 모든 오브젝트 및 컴포넌트의 조상
System.Object ⊃ UnityEngine.Object
System.Object가 UnityEngine.Object의 상위 클래스

각 오브젝트 생성시

System.Object sObj = new System.Object();
Debug.Log($"* sObj = {sObj}");
UnityEngine.Object uObj = new UnityEngine.Object();
Debug.Log($"* uObj = {uObj}");

UnityEngine.Object의 경우 new 로 새 인스턴스를 할당했음에도 불구하고 null이 출력됨을 알 수 있다.
이는 조사식에서 확인할수 있듯 "null", 즉 fake null을 의미한다.

// ✏️ UnityEngine.Object 내부 코드
//
// Summary:
//     Returns the name of the object.
//
// Returns:
//     The name returned by ToString.
public override string ToString()
{
    return ToString(this);
}

아마 오브젝트의 이름이 null로 지정되는 것 같다.

➕ public 과 private 의 차이

public GameObject publicObj = null;
private GameObject privateObj = null;


public으로 선언될 경우 새 인스턴스가 할당됨을 추측할 수 있다.

이렇게 되는 이유

유니티의 내부 구조는 C++로 만들어져 있다.

👉 Unity의 코어 엔진은 C++로 작성되어 있으며, 이는 게임 실행의 핵심을 담당합니다. 그 외에도 Unity의 에디터 및 일부 기능은 C#으로 작성되어 있습니다.
C++은 주로 게임 엔진의 핵심 부분을 구현하는 데 사용되며, 이는 게임의 성능과 최적화에 중점을 두고 있습니다. 반면에 C#은 Unity의 사용자 인터페이스 및 게임 로직과 같은 부가적인 기능을 담당합니다.
이런 분리된 언어 구조는 게임 엔진의 핵심 부분을 효율적으로 최적화하면서도, 게임 개발자에게는 상대적으로 사용하기 쉬운 언어를 제공할 수 있도록 합니다. Unity 스크립팅 API를 통해 게임 로직을 작성할 때 주로 C#을 사용하게 됩니다. (챗GPT)
👉 https://discussions.unity.com/t/is-unity-engine-written-in-mono-c-or-c/867

따라서 실제 객체는 C++로 구현되어있고, 유니티 상에서 보이는 객체는 C#으로 래핑되어 있다. ▶️ System.Object


2) Destroy(Object)

C#의 객체는 GC(가비지 콜렉터)가 동작해야만 실제 메모리에서 해제가 이루어진다.

따라서 오브젝트를 Destroy 할 때 어떤 상황이 발생할지 예상해볼수 있다. C++로 구현된 실제 객체는 즉시 해제가 이루어지지만, C#으로 래핑된 부분(System.Object)은 GC가 동작할때까지 해제되지 않을 것이다.

(👀 참고한 블로그)

BoxCollider box = gameObject.AddComponent<BoxCollider>();
yield return null;

Destroy(box);
yield return null;

IsUnityNull(box);
IsSystemNull(box);


예상대로 System.Object는 null이 아니라는 것을 알 수 있다.

그렇다면 UnityEngine.Object는 왜 null일까?

// ✏️ UnityEngine.Object 내부 코드
public static bool operator ==(Object x, Object y)
{
    return CompareBaseObjects(x, y);
}

public static bool operator !=(Object x, Object y)
{
    return !CompareBaseObjects(x, y);
}

private static bool CompareBaseObjects(Object lhs, Object rhs)
{
	// 👉 System.Object 형으로 변환 후 null 체크
    bool flag = (object)lhs == null;
    bool flag2 = (object)rhs == null;
    if (flag2 && flag)
    {
        return true;
    }
	
    // 👉 실제 Native 객체의 null 체크
    if (flag2)
    {
        return !IsNativeObjectAlive(lhs);
    }

    if (flag)
    {
        return !IsNativeObjectAlive(rhs);
    }

    return lhs.m_InstanceID == rhs.m_InstanceID;
}

private static bool IsNativeObjectAlive(Object o)
{
    // 👉 포인터 값이 유효한지 검사 (C++ Native 객체에 대한 주소 정보)
    if (o.GetCachedPtr() != IntPtr.Zero)
    {
        return true;
    }

    if (o is MonoBehaviour || o is ScriptableObject)
    {
        return false;
    }

    return DoesObjectWithInstanceIDExist(o.GetInstanceID());
}

이처럼 (세부적인 정확한 로직은 알수 없지만) UnityEngine.Object의 경우 C++ 실제 객체의 존재 유무까지 검사하는것을 알 수 있다.

따라서 아직 메모리상에는 C# 형태의 객체가 존재하지만, C++ 객체는 파괴되어있는,
실제 null은 아니지만 null로 처리되는,
"null", fake null의 형태를 가지게 된다.

3) IsNull()?

코드를 다시 살펴보면,

public static bool IsNull(this object o)
{
    return o == null;
}

Object (=UnityEngine.Object) 형이 아닌
object (=System.Object) 형으로 검사 하고 있음을 알수 있다.

즉, System.Object형이 null인 상태,
'fake null이 아닌 GC가 발생해 메모리상에서까지 깔끔하게 지워진 상태의 null' 을
검사하고자 함인것을 알 수 있다.

위 코드를 재활용해 다시 로그를 찍어보면

BoxCollider box = gameObject.AddComponent<BoxCollider>();
yield return null;

Destroy(box);
yield return null;

Debug.Log($"box == null ? {box == null}");	// UnityEngine.Object
Debug.Log($"box.IsNull() ? {box.IsNull()}");	// System.Object

같은 결과가 나옴을 알 수 있다.


대부분의 UI와 객체가 Singletone으로 짜여있는 구조의 프로젝트였기 때문에
메모리까지 체크가 이루어지는 해당 함수를 만들어서 사용했던 것 같다.
한번 생성되면 파괴되지 않는 구조기 때문에 Native 객체까지 이중으로 체크하는 UnityEngine.Object의 == 연산자는 너무 비싼 함수였을것 같다.


참고
👀 https://ansohxxn.github.io/unitydocs/fakenull/#google_vignette
👀 https://husk321.tistory.com/406
👀 https://m.blog.naver.com/sorang226/222740387351

0개의 댓글