카테고리 없음

[BalancingLibra] 커스텀 싱글톤 클래스 구현 및 적용 과정

mazayong 2026. 1. 3. 23:57

싱글톤을 원하는 클래스에 일일히 설정하는 것이 아닌 클래스 상속으로 효율적으로 구현하고 싶어서 해당 부분을 리팩토링했다.

 

 

 

1. 싱글톤 패턴의 목적

2. 기존에 사용한 싱글톤 구현 방법

3. 코드 구현 및 수정

4. 적용 방법

5. 후기

 

 

 

 

1. 싱글톤의 목적

싱글톤은 게임 전반에 걸쳐 하나의 인스턴스만 전역적으로 사용하기 때문에 메모리 낭비가 적다. 또한 유일성이 보장되기 때문에 맴니저와 같은 관리자 시스템을 관리하기에 용이하다. 하지만 단점이라면 해당 인스턴스에 대한 의존성이 강해진다는 점이다.

 

 

 

 

2. 기존에 사용한 싱글톤 구현 방법

이전에는 싱글톤을 원하는 클래스에 아래 코드를 붙여넣었어야 한다.

// GameManager.cs
public static GameManager Instance;

//중략
void Awake() {
    if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); }
    else Destroy(gameObject);
}

 

해당 코드를 일일히 붙여넣어 보일러플레이트 코드가 생겨 비효율적이기 때문에 교체하기로 결정했다.

 

 

 

3. 코드 구현 및 수정

구현한 코드는 다음과 같다.

using UnityEngine;

public class SingletonBehaviour<T> : MonoBehaviour where T : SingletonBehaviour<T>
{
    private static object _lock = new object();
    private static bool _applicationIsQuitting = false;
    private static T _instance;

    public static T Instance
    {
        get
        {
            //게임 종료 시 null 반환
            if(_applicationIsQuitting)
            {
                Logger.LogError($"[SingletonBehaviour] Instance '{typeof(T)}' is already destroyed. Returning null.");
                return null;
            }

            //스레드 안전성 확보
            lock(_lock)
            {
                if(_instance == null)
                {
                    //씬에 이미 존재하는지 찾기
                    _instance = FindAnyObjectByType<T>();

                    if(FindObjectsByType<T>(FindObjectsSortMode.None).Length > 1)
                    {
                        Logger.LogError($"[SingletonBehaviour] Multiple instances of '{typeof(T)}' found. Reopening the scene might fix it.");
                        return _instance;
                    }
                    
                    //씬에 없으면 새로 생성
                    else if(_instance == null)
                    {
                        GameObject singleton = new GameObject();
                        _instance = singleton.AddComponent<T>();
                        singleton.name = "[Singleton] {typeof(T)}";

                        DontDestroyOnLoad(singleton);

                        Logger.Log($"[SingletonBehaviour] An instance of '{typeof(T)}' is needed in the scene, so '{singleton}' was created with DontDestroyOnLoad.");
                    }

                    //씬에 없으면 새로 생성
                    else
                    {
                        DontDestroyOnLoad(_instance.gameObject);
                        Logger.Log($"[SingletonBehaviour] An instance of '{typeof(T)}' is needed in the scene, so '{_instance.gameObject}' was created with DontDestroyOnLoad.");
                    }
                }
                return _instance;
            }
        }
    }

    protected virtual void Awake()
    {
        Init();
    }

    //중복 생성 방지
    protected virtual void Init()
    {
        if(_instance == null)
        {
            _instance = (T)this;
            DontDestroyOnLoad(gameObject);
        }
        else if(_instance != this)
        {
            Destroy(gameObject);
        }
    }

    //삭제 시 실행
    protected virtual void OnDestroy()
    {
        Dispose();
    }

    protected virtual void Dispose()
    {
        if (_instance == this)
        {
            _applicationIsQuitting = true;
        }
    }
}

커스텀 싱글톤 클래스를 다른 클래스에 상속해 싱글톤을 선언할 것이므로 제네릭 기반 재사용성을 확보하도록 코드를 작성했다.

lock 키워드 사용으로 멀티 스레드 환경에서의 Race Condition을 방지했다.

또한 Instance 호출 시점에 객체가 없으면 자동으로 구현하도록 설정했다. 

_applicationIsQuitting을 선언해 애플리케이션 종료 후 파괴된 싱글톤 재생성 방지 플래그를 추가했다.

 

 

 

4. 적용 방법

해당 클래스를 적용하면 아래와 같다.

// GameManager.cs
public class GameManager : SingletonBehaviour<GameManager>
{
}

 

 

 

5. 후기

lock 키워드는 처음 써봤는데 멀티 스레드 환경일 때 여러 개의 인스턴스가 동시에 생겨서 충돌하는 것을 막는 기능이다. 유니티를 꾸준히 해왔다고 생각하는데 이번 프로젝트를 할 때 배우는 게 더 많은 것 같다. 앞으로도 더 많이 배워나가서 유니티 도메인 지식이 많아지고 싶다.