[유니티] null 비교용 확장 함수 및 fake null

jh Seo·2025년 6월 9일
0

유니티

목록 보기
66/67

개요

게임오브젝트 접근할때 null 비교를 통해
null이면 debug찍고 return 하는 과정이 번거로워서
일종의 extension 함수를 작성해봣다.

    public static bool CheckNull(this object obj,
        string objName="",
        [CallerMemberName] string methodName = "",
        [CallerFilePath] string filePath = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        if (obj == null)
        {
            if (Debug.isDebugBuild)
            {
                // 파일 경로에서 클래스 이름(파일명이 클래스와 동일한 경우)을 추출
                string className = System.IO.Path.GetFileNameWithoutExtension(filePath);
                Debug.LogError($"[{className}] {methodName} - Object {objName} is null! (Line: {lineNumber})");
            }
            return true;
        }

        return false;
    }

문제는 object 비교를 하게되자 디버깅에서는 오브젝트가 null로 찍히지만
CheckNull 함수 내에서는 오브젝트가 null이 아니어서 계속 return false가 되었다.

관련 유니티 포럼 글

https://discussions.unity.com/t/is-null-returns-false-on-null-objects/821933

검색해보던 중 위 유니티 포럼을 찾았다.

핵심 내용은 다음과 같다.

  • Unity는 == 연산자를 오버로드하여 특정 객체가 삭제되었을 때도 null처럼 동작하도록 만든다.

  • 하지만 is null은 C#의 기본 연산자로, Unity의 오버로드된 == 연산자의 영향을 받지 않는다.

  • 따라서 obj == null은 true를 반환할 수 있지만, obj is null은 false를 반환할 수 있다.

  • object.ReferenceEquals(obj, null)을 사용하면 더 정확한 null 체크가 가능할 수 있다.

이유

핵심 원인은 함수 인자의 자료형으로 받은 object가
UnityEngine.Object 자료형이 아니고, C#의 object이므로 발생하는 현상이다.

  • UnityEngine.Object 타입의 객체에서는 == null이나 is null 같은 연산자들이 오버로드되어,
    객체가 Destroy() 되었을 경우나 기본값 null로 초기화되었을 때 null처럼 동작한다.

  • 하지만 C#의 object 타입은 기본 연산자를 사용하기 때문에 Unity의 == null 오버로드 기능을 무시하고 일반적인 null 체크로 수행된다.

  • 즉, obj == null을 검사할 때, obj가 UnityEngine.Object 타입이라면
    Unity의 특수한 null 검사 방식이 적용되지만,
    그냥 object 타입이라면 Unity의 null 오버로드를 우회하고
    일반적인 C#의 null 비교 규칙을 따르게 된다.

따라서 유니티 엔진의 == null 를 오버로드 하고싶다면

UnityEngine.Object obj

타입을 사용해야한다.

Null 비교 방식들

위 내용을 검색하면서 알아본 null 비교 방식들을 정리해보려고 한다.
대표적으로 세가지가 있다.

1. == null / != null

UnityEngine.Object 타입에서는 == 연산자가 오버로드되어 있으며,
C++ 네이티브 객체가 삭제되었는지까지 감안하여 null을 반환한다.

하지만 일반적인 C# 클래스(System.Object를 상속받은 클래스)에서는
== null일반적인 참조 비교만 수행하며, Unity의 오버로드된 ==의 영향을 받지 않는다.

2. is null / is not null

is null 연산자는 C#의 기본 연산자로,
일반적인 == null 비교보다 빠른 성능을 낼 수 있다.

하지만 is 연산자는 UnityEngine.Object에 대한 오버로드된 == 연산자의 영향을 받지 않으므로, Destroy된 오브젝트에 대해 정확한 null 비교에 문제가 생길 수 있다.

3. object.ReferenceEquals(go, null)

ReferenceEquals(a, b)메모리 주소를 직접 비교하는 방식으로,
일반적인 == 연산자보다 빠른 성능을 낼 수 있다.

하지만 UnityEngine.Object에 대해 오버로드된 == 연산자를 우회하므로,
마찬가지로 Destroy된 오브젝트에 대해 정확한 null 비교에 문제가 생길 수 있다.

Fake Null 문제

Destroy()를 호출하거나 GetComponent()를 호출했는데 적절한 컴포넌트를 찾지 못한 경우,
Unity는 반환된 객체를 null로 처리한다.
하지만 이 객체를 최상단 자료형인 System.Object로 캐스팅한 후
== null로 검사하면, 서로 다른 결과가 나올 수 있다.

예제 코드:

(Object) obj == null // UnityEngine.Object는 null로 평가됨
obj is null // false 반환 (System.Object로 캐스팅했기 때문)

원인

이 문제는 유니티는 객체에 C++(네이티브)상태 객체와 C#(유니티)상태 객체로 나뉘어 있기 때문에 발생한다.

  • 유니티는 내부적으로 C++ 코드가 있어 실제 객체는 C++로, 클래스 내에서 보이는건 C#으로 래핑 되어 있다.

  • Destroy()를 하면 C++의 실제 객체는 삭제

  • C#으로 래핑된 부분은 아직 GC의 동작을 기다리고 있다.

  • UnityEngine.Object == 는 C# 래핑 부분뿐 아니라 실제 객체도 존재하는지에 대한 부분도 보고 있다.

  • 위 경우 실제 C++ 객체는 Destroy에 의해 삭제되었으니 이를 알고 null이라 한다.

  • System.object의 경우 아직 C#의 래핑된 부분이 남아 있으므로 null이 아니라고 한다.

원인

이 문제는 Unity가 객체를 C++ 네이티브 객체C#에서 관리되는 객체로 나누어 관리하기 때문에 발생한다.

  1. Unity의 객체 구조

    • Unity의 오브젝트는 내부적으로 C++에서 관리되는 네이티브 객체와,
      C#에서 접근할 수 있도록 래핑된 관리 객체로 나뉜다.
    • Destroy()를 호출하면 C++ 네이티브 객체가 제거되지만,
      C#의 관리 객체는 여전히 존재하며 GC(가비지 컬렉션)에 의해 정리될 때까지 남아 있다.
  2. UnityEngine.Object== 오버로드 동작

    • Unity는 == 연산자를 오버로드하여 C++ 네이티브 객체가 삭제되었는지도 확인하여 null인지 판단한다.
    • 따라서, UnityEngine.Object == null을 검사하면 실제 C++ 객체가 삭제된 경우에도 true를 반환한다.
  3. System.Object를 통한 비교

    • 하지만 System.Object로 캐스팅하면 Unity의 == 연산자가 적용되지 않으며,
      C# 관리 객체가 아직 남아 있기 때문에 null로 평가되지 않을 수 있다.

레퍼런스

https://husk321.tistory.com/406

https://wlsdn629.tistory.com/entry/Unity-null-%ED%99%95%EC%9D%B8-%EB%B0%A9%EB%B2%95%EC%95%BD%EA%B0%84%EC%9D%98-%EC%B5%9C%EC%A0%81%ED%99%94%ED%8E%B8

profile
코딩 창고!

0개의 댓글