Unity 2D로 스페이스 슈터 게임을 만들며 점수를 기록하고 게임을 재시작할 때마다 초기화하기 위한 ScoreKeeper 스크립트를 생성하고, Scene을 관리하기 위한 LevelManager 스크립트에서 ScoreKeeper를 찾는 형식으로 코드를 구성했다.
// ScoreKeeper
public class ScoreKeeper : MonoBehaviour
{
int score = 0;
static ScoreKeeper instance;
void Awake()
{
ManageSingleton();
}
public ScoreKeeper GetScoreKeeper()
{
return instance;
}
void ManageSingleton()
{
if (instance != null)
{
gameObject.SetActive(false);
Destroy(gameObject);
}
else
{
instance = this;
DontDestroyOnLoad(gameObject);
}
}
public void ResetScore()
{
score = 0;
}
}
// LevelManager
public class LevelManager : MonoBehaviour
{
ScoreKeeper scoreKeeper;
void Awake()
{
scoreKeeper = FindObjectOfType<ScoreKeeper>();
}
public void LoadGame()
{
scoreKeeper.ResetScore();
SceneManager.LoadScene("Game");
}
}
정상적으로 실행될 줄 알고 프로그램을 실행했더니 NullReferenceException 에러를 보게 되었고, 오탈자나 빠뜨린 부분이 있는지 찾아보았지만, 어디에서도 발견하지 못했다.

scoreKeeper.ResetScore();
문제가 된 코드 라인, 하지만 해당 함수와 LevelManager, ScoreKeeper 스크립트에 이상은 없었다.
NullReferenceException은 객체를 참조하지 않는 변수에 접근하려고 하면 발생한다. 즉, 어떤 변수에 접근하려고 하는데 그 변수가 없으면 null로 처리되어 오류가 발생하는 것이다.
이 부분도 의아했던 것이, 분명 프로그램을 실행했을 때 ScoreKeeper가 Scene에 잘 포함되어있는 것을 확인했고, 스크립트가 제대로 실행되는지 확인하고자 2개의 스크립트의 Awake 함수에 Debug.Log()를 포함해 실행 과정을 출력해보았지만, 여전히 문제가 없었다.

실행 시 순서대로 출력되는 로그와 DDoL에 포함된 ScoreKeeper
만약 ScoreKeeper의 Awake 함수를 실행하기 전에 LevelManager의 Awake 함수를 실행했다면 ScoreKeeper에 접근이 안된다고 이해할 수 있었겠지만, 콘솔에 ScoreKeeper의 로그가 먼저 나온 후 LevelManager의 로그가 나온 것을 보아 그 문제는 아니라고 생각했다.
한참을 헤매며 구글링을 해보니 비슷한 문제를 마주한 사람들이 있었고, 그중에서 문제를 해결하도록 도와준 답변을 발견했다.
정리하자면, 처음 실행되는 Scene은 ScoreKeeper를 DDoL에 넣게 된다. 그다음으로 실행되는 Scene도 ScoreKeeper를 가지므로 2개의 ScoreKeeper가 생기게 되고, ScoreKeeper의 Awake 함수에서 ScoreKeeper가 2개 있다는 것을 발견하고 두 번째 ScoreKeeper를 제거할 것이다.

LevelManager의 Awake 함수는 DDoL의 ScoreKeeper가 아닌, 이후에 만들어진 ScoreKeeper를 참조했지만 그 ScoreKeeper가 제거됐기 때문에 참조가 사라지게 되어 NullReferenceException 에러가 발생하게 된다.
요점을 꼽자면, 객체가 생성했다고 해서 Awake 함수가 실행된 것은 아니다.
Scene이 실행되면 유니티는 현재 Scene에 존재하는 모든 객체를 모아서 Awake 함수를 차례대로 실행한다. 이때 Awake 함수들이 실행되는 순서는 정해져 있는데, 현재 코드를 실행했을 때 LevelManager의 Awake가 먼저 실행되어 참조를 하고, 이후에 ScoreKeeper의 Awake가 실행되어 참조된 객체가 제거되기 때문에 오류가 발생하는 것으로 보인다.
솔직히 이 과정을 100% 이해했다고 할 순 없지만, 그래도 원인을 발견한 것 같아 다행이라고 생각했다. (혹시 틀린 부분이 있다면 지적해주세요)
이를 해결하기 위해 LevelManager의 Awake 함수를 지우고 findScoreKeeper()라는 새로운 함수를 만들어 ScoreKeeper를 찾게 했더니, 에러가 사라지고 정상적으로 작동했다.
// 수정한 LevelManager
public class LevelManager : MonoBehaviour
{
[SerializeField] float sceneLoadDelay = 1f;
ScoreKeeper scoreKeeper;
void findScoreKeeper()
{
if (scoreKeeper == null)
{
scoreKeeper = FindObjectOfType<ScoreKeeper>();
}
}
public void LoadGame()
{
findScoreKeeper();
scoreKeeper.ResetScore();
SceneManager.LoadScene("Game");
}
}
유니티 공식 문서에 함수가 어떤 순서로 작동하는지 알려주는 멋진 그림이 있다.
https://docs.unity3d.com/Manual/ExecutionOrder.html#FirstSceneLoad

Scene에 추가된 모든 객체에 대해, Start, Update 함수를 실행하기 전 모든 스크립트의 Awake 함수가 실행된다. 또한 Script Execution Order를 조정하면 유니티가 어떤 함수를 먼저 실행하는지 정해줄 수 있다.
만약 방금 같은 상황에서 스크립트의 실행 순서로 인한 오류가 발생했더라면, 이 순서를 조정하여 에러를 없애는 방법도 고려해 보는 것이 좋을 듯하다.