(C#유니티) Singleton 패턴 (싱글톤)

티원·2025년 10월 20일

디자인패턴

목록 보기
5/16

Singleton (싱글톤)

Singleton 시연 영상(Scene전환을 통한 Singleton 확인)

  • 프로그램 전체에서 단 하나의 인스턴스만 존재하도록 보장
  • 어디서든 접근할 수 있음
  • 불필요한 중복생성을 방지할 수 있다

(코드에 너무 집착하지말고 개념을 확실히 하는것이 좋음. 물론 코드도 알면 좋다)

사용하는 이유

  • 유니티에서 게임의 상태를 관리할 관리자 클래스(Manager)를 만들때
  • 씬이 바뀌어도 유지되어야 하는정보 (플레이어 점수, 설정값)
  • 전역적 접근해야 하는 시스템(사운드,데이터,UI)

장점

  • 전역 접근성: 어디서든 인스턴스에 접근 가능
  • 유일성 보장: 인스턴스 하나만 존재
  • 데이터 공유 용이: 여러 씬간 데이터 유지에 유용 (예:DontDestroyOnLoad)

단점

  • 의존성 증가: 여러 클래스가 싱글톤을 직접 참조하면 결합도가 높아짐 (유지보수가 어려워짐, 해당 싱글톤을 수정하면 해당 싱글톤을 참조하는 것들이 모두 영향을 받으므로)
  • 테스트 어려움: 인스턴스가 고정되어 단위 테스트 불리 (이전 테스트에서 실행한 상황이 계속 남아있음)
  • 메모리 관리 어려움: Destroy를 잊으면 씬 전환 후에도 계속 남아 있을수 있다.

예제 코드

  • Instance 프로퍼티와 Awake에서 둘다 null체크를 하는 이유:
    외부에서 Instance로 호출하는경우 null일 경우 생성해야 하고,
    Hierachy에서 생성되어 있을경우 Awake에서 최초 호출되므로
    외부 호출용 프로퍼티와 Awake 둘다 null체크가 필요하다
public class SingletonTestBuf : MonoBehaviour
{
    private static SingletonTestBuf _instance;
    public static SingletonTestBuf Instance //프로퍼티로 싱글톤 객체를 호출했을때 null체크
    {
        get
        {
            if(_instance == null)
            {
                _instance = FindObjectOfType<SingletonTestBuf>();
                if(_instance == null)
                {
                    _instance = new SingletonTestBuf();
                }
            }
            return _instance;
        }
    }
    private void Awake() //남들한테 호출당해서 하는게 아닌 자기자신이 깨어났을경우 null처리
    {
        if(_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }
        _instance = this;
        DontDestroyOnLoad(_instance); //다른씬에서도 사용할 수 있어야하면 추가
    }
}

싱글톤 + 제너릭

싱글톤과 제너릭을 함께 사용하여 매번 싱글톤으로 생성할 클래스를 정의할 필요가 없다. (싱글톤 클래스마다 static 인스턴스 만들고.. Awake에 null처리 하는 과정 제네릭으로 편하게 통합)

사용하긴하지만 개념적으로는 권장되지는 않는 이유?

싱글톤과 제네릭의 존재이유가 충돌
싱글톤 -> 하나만 존재
제네릭 -> 여러 타입으로 여러개 만듬
정체성이 흔들림(타입별로 하나씩 존재하는 싱글톤?) -> 전역 유일성이 떨어짐

예제코드 (싱글톤+ 제네릭)

1.Singleton.cs

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;
    //싱글톤 구조를 가져가지만 다음씬에서 제거되어야 할때 사용 *씬별로 존재하는 UI매니저 등)
    protected bool isDestroyOnLoad = true;
    //MVP에서 싱글톤 매니저의 이벤트를 해제(Dispose)해야되는데 이때 Null체크가 필요해서 추가
    //isHaveInstance를 안쓰고 if(Manager.Instance != null) 이렇게 하면 인스턴스에 접근해서 새로 생겨버림
    public static bool isHaveInstance => _instance != null;

    //Instance 프로퍼티와 Awake에서 둘다 null체크를 하는 이유:
    //외부에서 Instance로 호출하는경우 생성되고
    //이미 씬에서 생성되어있을경우 프로퍼티로 호출되지는않고 Awake에서 최초 호출되므로

    //외부 호출용 프로퍼티, 해당 타입의 싱글톤이 없으면 찾아보고 그래도 없으면 새로 생성 후 결정
    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<T>(); //찾아봄

                if (_instance == null)
                {
                    GameObject singletonObj = new GameObject();
                    _instance = singletonObj.AddComponent<T>();
                    singletonObj.name = typeof(T).ToString();
                }
            }
            return _instance;
        }
    }
    protected virtual void Awake() //중복체크 및 연결기능 구현 (재정의 가능)
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return; //이게 없으면 자식의 Awake가 끝까지 실행됨
        }
        else
        {
            _instance = this as T;
            if (isDestroyOnLoad)
            {
                DontDestroyOnLoad(gameObject);
            }
        }
        OnAwake(); //이거 추가해보자 . 구조 개선 필요할듯
    }
    개선 예약 -> Awake에 virtual 지우고, protected virtual void OnAwake() 추가
    그다음에 자식에서 OnAwake를 정의하면 된다
}

2.GameManager.cs

public class GameManager : Singleton<GameManager>
{
    private bool _isPlaying = false;

    public bool IsPlaying { get { return _isPlaying; } }
    public event Action OnGameStartAction;
    [field: SerializeField] public UnityEvent OnGameEndAction { get; private set; } = new UnityEvent();

    //현재 게임이 진행중인지 여부를 전환하는 함수
    public void ChangeGameState()
    {
        _isPlaying = !_isPlaying;

        if (_isPlaying) OnGameStartAction?.Invoke();

    }
    protected override void Awake()
    {
    	//현재 클래스에서 Awake를 써야할경우. 사용가능
        //isDestroyOnLoad = false; //다음씬에서 제거되어야 하는경우 추가
        
        //부모의 Awake 호출
        base.Awake(); 
        //이미 있는 싱글톤일때 base.Awake에서 Destroy하고 return하는데 return이라는건 현재 메소드를 빠져나가는거라서 자식에서의 override Awake는 빠져나가지않음.
        //그래서 여기서도 한번 더 싱글톤 중복 체크해야함, 이걸 안하면 중복 싱글톤이 아래 코드를 실행한다...
        if (Instance != this) return; 
        //다음 원하는 코드 작성
    }
}

만약 싱글톤처럼 쓰되, 다음 씬에서 삭제되도록 쓰고싶다면?

싱글톤처럼 현재 씬에서 1개만 유지되고, 전역적으로 호출이 되지만, 다음 씬에서 삭제되도록 하고싶다면? 아래 코드처럼 static으로 선언하면 된다

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
    }

    private void OnDestroy()
    {
        if (Instance == this)
            Instance = null;
    }
}

0개의 댓글