디자인 패턴

디자인 패턴에 대해 알아보자#2 싱글톤 패턴(feat. 유니티)

근본넘치는개발자 2024. 10. 8. 23:57

오늘은 싱글톤 패턴에 대해 알아보겠습니다.

유니티를 배우면서 싱글톤 패턴을 사용했다/하라는 이야기는 자주 들었는데,

 

조금 더 구체적으로 어떤 상황에서

어떻게 사용해야 하는 건지 정리해 봤습니다.

 

싱글톤 패턴이란?

 

클래스가 자신의 인스턴스 하나만 인스턴스화 할 수 있도록 보장하고,

이 인스턴스에 대해 어디에서나

해당 클래스의 인스턴스에 접근할 수 있도록 하는 패턴입니다.

 

// 유니티에서의 싱글톤 코드 예시 : MonoBehaviour 일 때

public class SimpleSingleton : MonoBehaviour{
public static SimpleSingleton instance; // 자기 자신을 정적으로 가짐

    private void Awake() {
	    if (instance == null) {
	        instance = this;
	    } else {
	        Destroy(gameObject);
	    }
    }
}

//새 Scene을 부르면 GameObject 지워짐
//사용되기 전에 하이어라키에서 셋업되어야 함

===============================
// 유니티 환경 외 혹은 MonoBehaviour가 아닐 때

public class SimpleSingleton {
public static SimpleSingleton Instance;

public static SimpleSingleton Instance {
        get {
            if (instance == null){  // 스레드 세이프 x
                instance = new Singleton();
            }
            return instance;
        }
    }
}

 

 

설명을 위해 가져온 싱글톤 패턴 코드입니다.

여기엔 몇 가지 문제가 발생할 가능성이 숨어 있습니다.

 

먼저 Monobehavior를 상속받는 경우입니다.

 

싱글톤은 항상 남아있어야 하는데,

새 Scenen을 부르면 GameObject가 지워지는 문제가 발생할 수 있습니다.

 

instance가 사용되기 전에 하이어라키에서 셋업 되어야 하는데,

 Awake로 instance가 호출되기 전이라서 문제가 발생하는 경우가 생길 수도  있습니다.

 

 

이를 해소하기 위해 다음과 같이 코드를 변형할 수 있습니다.

 

//MonoBehaviour을 상속받는 경우 

public class Singleton : MonoBehaviour{

private static Singleton instance;
public static Singleton Instance {
    get {
        if (instance == null) {
            SetupInstance(); // 메서드 호출로 해결
        }
        return instance;
    }
}

private static void SetupInstance(){
    instance = FindObjectOfType<Singleton>(); // 없는 경우 찾기

    if (instance == null) {
        GameObject gameObj = new GameObject();
        gameObj.name = "Singleton";
        instance = gameObj.AddComponent<Singleton>();
        DontDestroyOnLoad(gameObj);
    }
}

private void Awake() {
    if (instance == null) {
    instance = this;
    DontDestroyOnLoad(this.gameObject);
    } else {
        Destroy(gameObject);
    }
}

 

 

MonoBehavior를 상속받지 않는 경우는 어떨까요

 

유니티는 기본적으로 싱글스레드를 기준으로 하고 있기에

Monobehaviour를 상속하면 괜찮지만, 그 외의 경우

멀티 스레드 환경에서, 세이프 x라고 되어있는 부분에서 문제가 발생할 수 있습니다.

 

스레드란?

프로세스 내부에서 생성되는, 실제로 작업을 하는 주체

 

프로세스 ? 

메모리에 적재된 실행되는 프로그램 

 

그럼 멀티 스레드는 ?

프로그램 내에서 여러 실행 흐름(스레드)을 동시에 실행하는 기법 정도로 이해하시면 될 것 같습니다.

 

이때 어떻게 동시에 스레드를 처리할까요?

 

크게 두 가지로 나뉩니다.

 

실제로 여러 개를 동시에 처리하는 병렬성

여러 개를 빠르게 번갈아 가며 처리하는 동시성으로 구분됩니다.

 

자세한 내용은 아래 블로그로 대체하겠습니다.

 

https://velog.io/@yarogono/%EB%8F%99%EC%8B%9C%EC%84%B1Concurrency%EA%B3%BC-%EB%B3%91%EB%A0%AC%EC%84%B1Parallelism

 

[C#] 동시성(Concurrency)과 병렬성(Parallelism)

C#으로 게임 서버를 만들면서 멀티 스레딩에 대한 이해도가 필요하다고 생각했습니다. 그렇게 공부를 하다가 동시성과 병렬성의 개념이 멀티 스레드 프로그래밍에 대한 이해를 도와줄 것 같아

velog.io

 

정리하자면 동시에 데이터를 처리하는 과정 속 같은 데이터에 접근했을 때

서로 다른 데이터 값으로 인한 문제가 발생할 수 있다는 이야기였습니다.

 

이러한 문제는 유니티 스크립트의 생명주기와도 이어질 수도 있습니다.

 

이참에 아래 공식 문서 링크를 통해 유니티 스크립트들이

어떤 순서로 돌아가는지 한번 확인해 보시는 것도 좋을 것 같습니다.

 

https://docs.unity3d.com/kr/2022.3/Manual/ExecutionOrder.html

 

이벤트 함수의 실행 순서 - Unity 매뉴얼

Unity 스크립트를 실행하면 사전에 지정한 순서대로 여러 개의 이벤트 함수가 실행됩니다. 이 페이지에서는 이러한 이벤트 함수를 소개하고 실행 시퀀스에 어떻게 포함되는지 설명합니다.

docs.unity3d.com

 

 

이를 해소하기 위해 다음과 같은 방식을 사용합니다.

 

방법 1

public class Singleton
{
    private static volatile Singleton instance = null;
    private static readonly object padlock = new object();

    Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                lock (padlock) //락을 통해 해소
                {
                    if (instance == null) // 락 하는 과정에서 데이터에 접근할 수도 있기에 한 번 더 확인
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}

 

// 방법 2 
// 멀티 스레드 환경에서 가장 완벽하게 해결
// 하지만 지연 초기화의 이점을 가져가지 못함

public sealed class SimpleSingleton {
private static readonly SimpleSingleton instance = new SimpleSingleton(); // 인스턴스 바로 생성


private SimpleSingleton() {
}

public static SimpleSingleton Instance {
get {
return instance;

}

 

지연 초기화(Lazy Initialization)

인스턴스가 처음 필요할 때까지 생성을 미루는 기법.

미룸으로서 메모리 효율성과 성능 최적화의 이점을 가질 수 있다는 장점이 있습니다.

 

 

싱글톤이 게임메니저, UI메니저 등 여러곳에서 사용될 때 

제너릭을 통해 효율적으로 관리하는 방법도 있습니다.

public class Singleton<T> : MonoBehaviour where T : Component
{
   private static T instance;
   public static T Instance {
       get {
           if(instance == null) {
            instance = (T)FindObjectOfType(typeof(T));
            if (instance == null) {
                SetupInstance();
                }
            }
            return instance;
        }
    }


    private static void SetupInstance(){
        instance =(T)FindObjectOfType(typeof(T));
        if(instance == null) {
        GameObject gameObj = new GameObject();
        gameObj.name = typeof(T).Name;
        instance = gameObj.AddComponent<T>();
        DontDestroyOnLoad(gameObj);
       }
    }


    public virtual void Awake(){
        RemoveDuplicates();
    }

    private void RemoveDuplicates() {
        if (instance == null) {
            instance = this as T;
            DontDestroyOnLoad(gameObject);
        } else {
            Destroy(gameObject);
        }
    }


//이전과 방식은 같지만 제너릭만 추가
=======================
public class GameManager : Singleton<GameMananger>{
//... 이렇게 상속받아서 간단하게 처리
}

public class UIManager : Singleton<UIManager>{
//... 
}

 

 

공부하다 보니 다 이어지는군요... ㅎㅎ;;

 

참고한(혹은 참고하면 좋을) 자료

https://youtu.be/Tf_VZEgnag0?si=LRc8n8_Aw0S-CGrp

 

https://dev-nicitis.tistory.com/4

 

유니티 C# 싱글턴 패턴 + Lazy를 이용한 버전

2023.08.08 추가: 본문에 존재하는 코드(특히 Lazy를 사용한 코드!)는 충분히 검증된 코드는 아닙니다. 따라서 아이디어는 채용하되, 직접 복사-붙여넣기할 경우에 오류가 발생하지 않는다는 보장은

dev-nicitis.tistory.com

https://www.youtube.com/watch?v=a5TCCQgdv-E