문제
재정의된 비교 연산자
MonoBehaviour 인스턴스가 null인지 아닌지 확인하고 싶을때 보통 이렇게 코드를 작성한다.if(myGameObject == null) { }
하지만 유니티는 == 연산자를 재정의해 사용하고 있기 때문에 생각과는 다르게 동작한다.
우선 유니티가 왜 이렇게 재정의해서 사용하게 됐는지 알아보자.
왜 그랬을까?
먼저 요약하자면 유니티는 다음 두가지의 이유로 ==를 재정의해서 사용한다.
- 에디터에서의 디버깅 용이성
- C/C++로 이루어진 내부 구현과 C# 래퍼 오브젝트, 그로 인한 오브젝트 생명주기의 차이
디버깅 용이성
첫 번째로, Inspector에서 보여지는 필드들(public, SerializedField)에 대한 디버깅 용이성이다.
Monobehaviour가 어떤 필드를 갖고 있을때, 에디터는 이 값을 "real null"이 아닌 "fake null"으로 설정한다. 유니티 엔진이 재정의한 == 연산자는 이를 fake null인지 검사하는 기능을 한다.
필드에 "real null"을 넣으면 NullReferenceException
이 발생하긴 하지만 어떤 게임오브젝트에서 예외가 발생하는지 알 수 없다. 따라서 유니티는 정보를 얻을 수 있게 "fake null" 오브젝트를 할당하고, 만약 이 필드에 접근했을때 값이 fake null일 경우,
"이 Monobehaviour에는 null인 필드가 있고 그걸 접근하고 있어."
라고 알려주는 역할이다. 즉, 어떤 Monobehaviour의 필드가 null이었는지 stack trace를 통해 확인할 수 있게 하는 트릭이다.
간단히 말해서 실제론 null이 아니라 어떤 정보를 알려주는 null역할을 하는 오브젝트가 할당되어 있고, 비교 연산자는 이 값이 해당 오브젝트인지 비교하는 역할을 한다고 보면 된다.
생명주기의 차이
두번째로, 네이티브 오브젝트와 C# 래퍼오브젝트 생명주기의 차이이다.
UnityEngine.Object
로부터 파생되는 GameObject
등의 C# 오브젝트들은 거의 아무런 정보를 갖고 있지 않다.
유니티는 내부 구현이 C/C++으로 되어있는 엔진이기 때문에 이러한 오브젝트들에 대한 실질적인 정보는 C++ 영역에 존재한다. C# 오브젝트는 이러한 네이티브 C++ 오브젝트에 대한 포인터만을 갖고 있다. 유니티는 이를 "wrapper objects"라고 부른다. 즉, C++ 네이티브 오브젝트를 C# 오브젝트로 래핑한 셈이다.
UnityEngine.Object
로부터 파생된 오브젝트들(GameObject
포함)과 같은 네이티브 오브젝트들의 생명주기는 명시적으로 관리되는데, 씬을 새로 로드하거나 Object.Destroy(myObject);
를 호출할때 파괴된다.
반면 C# 오브젝트는 가비지컬렉터를 활용한 C#의 방법으로 관리된다. 이것은 C++ 오브젝트가 파괴되었음에도 불구하고 C# 래퍼 오브젝트는 남아있을수도 있다는 뜻이다. 이때 이 오브젝트를 ==연산자로 null 비교하면 실제로는 null이 아님에도 불구하고 true를 반환한다.
간단히 말해서 비교 연산자는 C++ 네이티브 오브젝트가 살아있는지 없는지만 비교하기 때문에, 실제로는
== null이 true를 반환할지라도 C# 래퍼 오브젝트는 GC에 의해 제거되지 않고 살아있을수도 있다는 뜻이다.
주의점
유니티 엔진의 구조 상 이렇게 설계된 것이지만 이 때문에 어쩔 수 없이 동반되는 단점도 있다.
- 직관적이지 않다.
UnityEngine.Object
를 서로 비교하는것 또는 null과 비교하는것은 우리가 예상하는 것 보다 느리다.- thread safe하지 않다. (따라서 메인 스레드 외의 다른 스레드에서 UnityEngine.Object를 비교할 수 없다.)
- C#의 null check 연산자(?? 연산자, ? 연산자)의 동작이 일관되지 않을 수 있다.
해결방안
ReferenceEquals
또는 암시적 bool 변환을 활용한 if(myObject)
를 사용하는 것을 권한다. 다만 주의할것은 ReferenceEqual
은 C# 래퍼 오브젝트가 "real null"인지 비교하는 것이기 때문에, "fake null"값이 들어가는 경우(inspector에서 보여지는 필드)는 false를 반환할 수 있다.
이에 대한 자세한 내용은 유니티 오브젝트의 null 비교 시 유의사항 | Overworks’ lab in GitHub를 참고하는 것이 좋을 듯 하다.