[Unity6] Singleton 초기화 시 `OnEnable`에서 `null`이 나올 수 있는가

a-a·2026년 4월 22일

알쓸신잡

목록 보기
34/34

상황 요약

다음과 같은 제네릭 싱글톤 베이스가 있다.

using UnityEngine;
using Sirenix.OdinInspector;

public class Singleton<T> : SerializedMonoBehaviour where T : SerializedMonoBehaviour
{
    private static T instance;
    public static T Instance
    {
        get 
        {
            if (isQuitting) return null;
            if (instance == null)
            {
                instance = FindFirstObjectByType<T>();
                if(instance == null)
                {
                    instance = new GameObject(typeof(T).Name).AddComponent<T>();
                }
            }
            return instance; 
        }
        protected set { instance = value; }
    }

    [SerializeField] private bool isDontDestory = true;
    protected static bool isQuitting = false;

    protected virtual void Awake()
    {
        if (instance == null)
        {
            instance = this as T;
            if (isDontDestory)
            {
                DontDestroyOnLoad(gameObject);
            }
        }
        else
        {
            Debug.LogWarning($"{typeof(T)} 중복");
            Destroy(gameObject);
        }
    }

    public virtual void Init() { }

    protected virtual void OnApplicationQuit()
    {
        isQuitting = true;
    }
}

이 클래스를 상속받은 어떤 매니저가 Awake()에서 base.Awake()를 호출해 instance를 세팅한다.
그런데 다른 오브젝트가 OnEnable()에서 해당 매니저 이벤트에 등록하려고 했더니 null이 발생했다.

로그를 찍어보니 다음처럼 보였다.

다른 오브젝트 OnEnable() 로그가 먼저 찍힘
싱글톤 쪽 base.Awake() 아래 로그가 나중에 찍힘

그래서 의문이 생겼다.

“Unity는 모든 오브젝트의 Awake를 먼저 다 돌리고, 그다음 OnEnable을 돌리는 거 아니었나?”


결론

그렇게 전역 순서로 보장되지 않는다.
공식 문서 기준으로 보장되는 것은 다음에 가깝다.

AwakeOnEnable은 모두 Start보다 먼저 호출된다.
OnEnable은 같은 컴포넌트 기준으로는 Awake 다음, Start 이전이다.

하지만 여러 오브젝트 전체를 대상으로 “모든 Awake가 끝난 뒤에 모든 OnEnable이 돈다”는 보장은 문서에 없다.

매뉴얼은 AwakeOnEnable이 모두 Start 이전 구간에 속한다고 설명할 뿐, 그 둘 사이를 전역 배리어처럼 설명하지 않는다.


null이 생겼나?

시나리오를 순서로 풀면 이렇다.

  1. 싱글톤 인스턴스는 base.Awake() 안에서 세팅된다
protected virtual void Awake()
{
    if (instance == null)
    {
        instance = this as T;
        ...
    }
}

즉, 이 오브젝트의 Awake가 실제로 실행되기 전까지는 instance가 비어 있을 수 있다.

  1. 다른 오브젝트가 OnEnable()에서 이벤트를 구독한다.
    예를 들면 이런 코드다.
private void OnEnable()
{
    MyManager.Instance.SomeEvent += HandleSomething;
}

이 시점에 MyManagerbase.Awake()가 아직 안 끝났다면 Instance가 비어 있거나, 기대한 씬 오브젝트가 아닌 다른 경로로 평가될 수 있다.

  1. 그래서 OnEnable()에서 null이 나올 수 있다

이건 Awake -> OnEnable -> Start 규칙을 어긴 게 아니라, 오브젝트 간 실행 순서에 전역 동기화가 없기 때문이다. 공식 문서가 강하게 보장하는 전역 배리어는 Start 쪽이다. Start는 씬의 모든 오브젝트에서 Awake가 호출되기 전에는 호출되지 않는다.


유니티 입문 6년만에 알게 되었다..

profile
게임 개발자란 무엇일까!

3개의 댓글

comment-user-thumbnail
3일 전

unchecked 예외인 NullPointerException를 만나셨나 보네요? ㅋ

1개의 답글