본문 바로가기

Unity

Unity의 fake null 문제

유니티 엔진의 구조

UnityEngine.Object를 == null로 비교해서 true가 떨어졌다고 진짜 null인 것은 아니다.

이 문제는 유니티 개발자들에게는 잘 알려져 있는 문제지만, 워낙 직관적이지 않은 문제이기 때문에 유니티를 처음 접하는 사람들은 백이면 백 빠지는 함정이기도 하다.

유니티 오브젝트의 구조

유니티 코어 레벨의 객체들은 모두 다 C++로 구현되어 있고, C++ 객체로서 존재한다. 유니티 스크립트 상에서 보이는 C# 객체는 C++ 객체를 참조하는 wrapper로 볼 수 있다. 이 C# wrapper 객체는 무조건 생성되는게 아니라, C++ 객체가 C#레벨에서 노출되어야 할 때만 생성되는데, 상황에 따라서는 C++ 객체만 생성되어있고 C# 객체는 생성되어 있지 않을 수도 있다. 하지만 반대로 C++ 객체는 없고 C# 객체만 남아있는 일도 벌어질 수 있는데, 이 상황이 바로 유니티의 악명높은 fake null이다.

 


fake null

문제는 유니티 오브젝트가 파괴될 때 생긴다. 유니티 오브젝트가 파괴되면 C++ 객체가 파괴되지만 C# 객체는 그대로 남아 있다. C#에서는 인스턴스를 수동으로 삭제하는 방법이 없으니 당연한 결과이다. 그래서 유니티 오브젝트는 == 연산자를 오버로드해서 만약 참조하고 있는 C++객체가 파괴되었다면 == null과의 비교해서 true를 돌려준다. 그러나 실제로는 C# 객체가 살아있는 상태이다.

 

fake null 상태의 오브젝트

 

이 상태를 유니티에서 fake null이라고 부른다. 유니티에서 파괴된 오브젝트를 판단할 때 IsDestroy() 같은 메서드를 제공하는 대신 null과 비교하는 것으로 판단할 수 있도록 하기 위해서 이런 구조를 만든 것 같은데, 이 때문에 오히려 직관적이지 않게 된 느낌도 있다.

 

이 fake null로 일어나는 문제는 대략 요약하면 다음과 같다.

  1. 문법적인 문제
  2. 성능상의 문제
  3. 메모리 누수

 


문법적인 문제

널 병합 연산자 및 is 연산자를 사용할 때 주의할 필요가 있다.

== 연산자를 오버로딩했다고 해도 널 병합 연산자 ?. 나 ?? 등에 적용되지는 않는다. 따라서 런타임에 파괴되는 오브젝트에 대해 ?. 연산자를 적용할 경우 ?.을 그대로 통과해서 MissingReferenceException을 발생시킬 수 있다.

is null 등의 연산도 마찬가지의 문제가 있다.A is null은 Object.ReferenceEqual(A, null)과 같기 때문에 ==연산자를 통하는 대신 직접 레퍼런스를 비교하게 된다. 

 

성능상의 문제

일반적으로 맞닥트리는 문제는 아니지만, 성능에 매우 민감한 곳에서는 유니티 오브젝트에 대한 == 연산자가 생각외로 느릴 수 있다. 매번 내부의 C++객체까지 비교를 하기 때문이다. 그래서 ==을 쓰는 대신 is null을 쓰거나, 기타 다른 방식으로 코드를 수정해야 할 가능성이 있다. 

 

메모리 누수

fake null로 발생하는 문제 중, 실질적으로 가장 문제가 되는 부분이다. 유니티의 C# 객체 자체는 C++을 가리키는 몇백byte짜리 작은 wrapper일 뿐이지만, 다른 커다란 에셋을 참조하기 시작하면 문제가 될 수 있다.

오브젝트가 있고 그 오브젝트를 그리는 텍스처가 있는 상황이다. 여기서 Destory호출이나 씬 전환 등으로 오브젝트가 파괴되었는데, C#부분은 fake null로 남아있는 상황을 가정해 보겠다.

오브젝트가 파괴되었지만, C#부분의 껍데기가 남아있어서 C# 텍스처를 참조하고 있고, C# 텍스쳐는 실질적으로 커다란 메모리를 차지하고 있을 C++ 텍스쳐를 참조하고 있다. 만약 해당 C# 객체가 static 등에 물려 있어서 GC에 수집되고 있지 않다면,  이 fake null 상태인 C#껍데기가 사라지지 않는 이상 Resources.UnloadUnusedAssets 등을 아무리 호출해도 텍스쳐가 메모리에서 내려가지 않는다. 프로그램의 관점에선 여전히 사용중인 상태이기 때문이다.

 


그래서..?

유니티 오브젝트 타입의 필드는 파괴 후 반드시 null을 대입하고, 유니티 오브젝트의 콜렉션은 사용 후 Clear해주는 등 확실하게 참조를 풀어줄 필요가 있다. 특히 static 변수의 경우 메모리 누수 혹은 불필요한 메모리 소모를 하는 원인이 될 수 있으므로 꼼꼼하게 살펴보야 한다.

C#에서는 GC의 존재로 개발자들이 메모리 누수에 대해 신경쓰지 않는 경향이 있다. 그러나 fake null 자체를 C++의 메모리 누수처럼 생각하고 가급적 fake null이 존재하지 않도록 관리할 필요가 있다.