[Unity] Destroy()를 통해 알아보는 유니티의 참조 관리

Running boy·2023년 8월 30일
0

유니티

목록 보기
7/9

.NET의 GC는 호출될 때마다 루트 참조를 잃은 데이터를 메모리에서 해제한다.
유니티도 .NET을 사용하기 때문에 내부적으로 GC가 있고 작동원리 자체는 비슷할 것이다.

하지만 그럼 한가지 의문이 생긴다.
Destroy() 메서드는 씬에서 게임오브젝트를 파괴하는데 과연 파괴된 게임오브젝트는 메모리에서 해제가 될까?
만약 다른 오브젝트가 파괴된 오브젝트를 참조하고 있었다면 파괴된 오브젝트의 루트 참조는 유지되기 때문에 더미 메모리로 남지 않을까?

실험1

씬에 게임오브젝트 A, B를 생성하고 아래의 스크립트 컴포넌트를 추가한다.

using Sirenix.OdinInspector;
using UnityEngine;

public class TestObject : MonoBehaviour
{
    [SerializeField]
    GameObject gameObject;

    [Button]
    public void DestoryObject()
    {
        Destroy(gameObject);
        Debug.LogFormat("Destroy {0}", gameObject.name);
    }

    [Button]
    public void Print()
    {
        Debug.LogFormat("gameObject == null: {0}", gameObject == null);
    }
}

이제 인스펙터창에서 B의 gameObject 필드에 A를 할당한다.

이론상 B의 TestObject 컴포넌트가 A를 참조하기 때문에 A가 파괴되더라도 참조는 유지되어 메모리에서 해제되지 않을 것이다.

결과

하지만 실험 결과는 반대였다.

B의 gameObject 필드가 null로 초기화 됐음이 확인됐다.
그 말은 A의 루트 참조가 해제되어 GC의 수집 대상이 됐음을 의미한다.

진실

사실 A의 루트 참조가 해제된 것이 아니다.
이는 A가 파괴된 이후 B의 인스펙터창을 확인하면 짐작할 수 있다.

gameObject 필드에 할당된 값이 'None'이 아니라 'Missing'으로 표기된다.
즉 뭔가를 참조하고 있긴 한데 그것을 찾을 수 없다는 뜻이다.
그럼 왜 실험의 결과는 null로 초기화 됐다고 나왔을까?

사실 '==' 연산자는 UnityEngine.Object 타입에서 파괴된 오브젝트에 대한 참조를 잃을 경우 이를 null로 초기화하도록 오버로딩된 상태다.
평범한 생각으로는 게임오브젝트가 파괴되었으니 null로 초기화 되는 것이 당연하다.
유니티에서도 이를 알고 게임 로직에서 오류가 발생하지 않게 하기 위해 비록 참조를 잃지 않았지만 잃은 것처럼 오버로딩해서 null로 초기화 시켜버린다.

유니티에 의해 오버로딩되지 않은 진짜 참조를 확인하려면 System.Object 타입의 참조 비교를 해야 한다.
object.ReferenceEquals 메서드를 사용해서 실험을 다시 해보자.

실험2

스크립트를 아래와 같이 수정하고 동일하게 실험한다.
이번에는 필드를 직접 null로 초기화하는 메서드도 추가적으로 수행한다.

using Sirenix.OdinInspector;
using UnityEngine;

public class TestObject : MonoBehaviour
{
    [SerializeField]
    GameObject gameObject;

    [Button]
    public void DestoryObject()
    {
        Destroy(gameObject);
        Debug.LogFormat("Destroy {0}", gameObject.name);
    }

    [Button]
    public void Print()
    {
        Debug.LogFormat("gameObject == null: {0}", gameObject == null);
        Debug.LogFormat("ReferenceEquals(gameObject, null): {0}", ReferenceEquals(gameObject, null));
    }

    [Button]
    public void InitNull()
    {
        gameObject = null;
        Debug.LogFormat("gameObject = null");
    }
}

결과

일단 이 실험을 통해 게임오브젝트가 파괴된다고 참조를 잃지 않는다는 것을 확실하게 증명해냈다.
그리고 파괴된 오브젝트의 참조를 직접 null로 초기화 시켜줘야 비로소 참조를 잃는다는 것까지 알 수 있다.
덤으로 참조를 직접 null로 초기화하면 인스펙터창에서도 변화를 확인할 수 있다.

정리

  1. Destroy()는 프레임의 끝에서 게임오브젝트를 파괴한다. 하지만 이는 씬에서의 제거일 뿐 메모리가 해제된 것은 아니다.
  2. 파괴된 오브젝트의 모든 참조는 유니티에 의해 null로 초기화된다. 하지만 .NET의 관점에서 참조는 유지되기 때문에 가비지 콜렉팅의 대상이 아니다.
  3. 파괴된 오브젝트의 참조는 반드시 코드상에서 null로 초기화하자.
profile
Runner's high를 목표로

0개의 댓글