Singleton
# Introduce
오늘 소개드릴 디자인 패턴은 싱글턴 패턴입니다.
싱글턴 패턴은 언제 어디서나 손쉽게 접근하고 제어할 수 있다는 장점 덕분에 널리 쓰이고 있습니다.
하지만 그만큼 한계도 뚜렷하기 때문에 장,단점을 명확히 이해한 후에 사용해야 합니다.
싱글턴 패턴이란 하나의 인스턴스만 존재하며 전역적으로 접근 가능한 클래스 디자인 패턴입니다. 일반적으로 언제 어디서나 접근 가능한 변수나 함수를 제공하는 매니저 타입의 클래스(GameManager, AudioManager 등)에 사용하면 유용합니다.
# Pros & Cons
장점
1. 전역적으로 접근 가능하기에 해당 클래스를 매번 생성, 검색할 필요가 없으며 따라서 레퍼런스를 저장해놓을 필요도 없습니다.
2. 여러 씬에 걸쳐서 사용되는 데이터를 저장하기에 좋습니다.
3. 정적 클래스와 달리 상속하거나 인터페이스를 구현할 수 있습니다.
단점
1. 클래스 간 의존관계를 숨깁니다(=파악하기 어려움).
2. 어디에서나 접근 가능하기에 의존관계가 복잡해지며, 남용할 경우 변수 하나를 바꾸는 것만으로도 게임 자체가 작동하지 않을 수 있습니다.
3. 객체의 변경 시점과 변경 주체를 알아내기가 쉽지 않으며 최악의 경우 객체에 접근하는 모든 코드들을 다 확인해야 할 수도 있습니다.
# For example
1. 싱글턴은 클래스 간 의존관계를 숨깁니다.
public class GameManager : MonoBehaviour
{
// 멤버변수 방식 : GameManager가 Player에 의존한다는 사실을 명시합니다.
//[SerializeField] private Player player;
private void OnPlayerDied()
{
// 멤버변수 방식
//Debug.Log("Final Score : " + player.Score);
// 싱글턴 방식 : GameManager가 Player에 의존한다는 사실을 숨깁니다.
Debug.Log("Final Score : " + Player.getInstance.Score);
}
}
2. 일반적인 C# 싱글턴 패턴
생성자의 접근제한자를 private으로 설정해서 클래스 외부에서의 생성을 제한합니다.
클래스에 처음으로 접근하는 시점에 Getter가 하나의 인스턴스를 생성합니다.
public class Singleton
{
private static Singleton _instance;
public static Singleton getInstance
{
get
{
if(_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
private Singleton() { }
}
3. 유니티 C# 싱글턴 패턴
유니티 클래스는 대부분 MonoBehaviour를 상속받기 때문에 꼭 생성자를 통하지 않더라도 인스턴스를 생성할 수 있습니다. 예를 들면, Object.Instantiate() 함수로 생성하거나 게임오브젝트에 컴포넌트로 추가된 상태로 씬에 배치하는 경우가 있습니다. 따라서 유니티에서는 싱글턴이 하나의 인스턴스임을 보장하기 위해 아래와 같은 추가 로직이 필요합니다.
using UnityEngine;
public class Singleton : MonoBehaviour
{
private static Singleton _instance;
public static Singleton getInstance
{
get
{
if(_instance == null)
{
_instance = FindObjectOfType<Singleton>();
if(_instance == null)
{
_instance = new GameObject().AddComponent<Singleton>();
}
}
return _instance;
}
}
private void Awake()
{
if (_instance == null)
{
_instance = this;
}
else if (_instance != this)
{
Destroy(this.gameObject);
}
}
}
4. 유니티 C# 싱글턴 패턴(제네릭 기본버전)
제네릭 Singleton 클래스를 상속받은 클래스(아래 예시의 TestManager)는 싱글턴으로 사용할 수 있습니다.
using UnityEngine;
public class Singleton<T> : MonoBehaviour
where T : MonoBehaviour
{
protected static T _instance = null;
public static T getInstance
{
get
{
if(_instance == null)
{
_instance = FindObjectOfType<T>();
if(_instance == null)
{
_instance = new GameObject().AddComponent<T>();
}
}
return _instance;
}
}
}
public class TestManager : Singleton<TestManager>
{
// 싱글턴이 아닌 오브젝트 생성을 막기 위해 생성자의 접근제한자를 protected로 설정합니다.
protected MySingleton() { }
private void Awake()
{
if (_instance == null)
{
_instance = this;
}
else if (_instance != this)
{
Destroy(this.gameObject);
}
}
}
5. 유니티 C# 싱글턴 패턴(제네릭 심화버전)
using UnityEngine;
using UnityEngine.SceneManagement;
public abstract class Singleton : MonoBehaviour
{
// 씬 전환시 싱글턴 오브젝트의 유지 여부를 설정하는 플래그입니다.
[SerializeField] private bool m_IsPersistent = true;
// 일반적으로 싱글턴은 앱 종료시 제거됩니다. 이때 유니티가 임의의 순서로 오브젝트를 제거하기
// 때문에 싱글턴 객체가 이미 제거된 시점에 외부에서 싱글턴으로 접근하면 싱글턴 오브젝트가
// 다시 생성됩니다. 따라서 이를 방지하기 위해 싱글턴 오브젝트가 제거중인지를 체크하는
// 플래그를 추가합니다.
protected static bool m_IsQuitting = false;
// 스레드 세이프를 위한 코드입니다.
protected static readonly object _lock = new object();
protected virtual void Awake()
{
if (m_IsPersistent)
{
DontDestroyOnLoad(this.gameObject);
}
OnAwake();
}
private void Start()
{
// 씬 변경시 파괴되지 않고 유지되는 오브젝트는 Awake, Start 함수를 다시 호출하지
// 않기 때문에 변경된 씬에서 싱글턴 클래스의 초기화가 필요한 경우
// SceneManager.sceneLoaded 델리게이트를 이용합니다.
// Awake -> OnEnable -> sceneLoaded -> Start 함수순으로 실행되기에
// Awake 함수 내에서 델리게이트 연결시 최초 씬에서도 OnSceneLoaded 함수가 호출됩니다.
// 이를 방지하기 위해 Awake 함수가 아니라 Start 함수 내에서 델리게이트를 연결합니다.
SceneManager.sceneLoaded += OnSceneLoaded;
OnStart();
}
private void OnDestroy()
{
m_IsQuitting = true;
if (m_IsPersistent)
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
}
private void OnApplicationQuit()
{
m_IsQuitting = true;
}
protected virtual void OnAwake() { }
protected virtual void OnStart() { }
protected virtual void OnSceneLoaded(Scene scene, LoadSceneMode mode) { }
}
public abstract class Singleton<T> : Singleton
where T : MonoBehaviour
{
private static T _instance;
public static T getInstance
{
get
{
if (m_IsQuitting)
{
Debug.LogWarning($"[Singleton] Instance '{typeof(T)}'" +
" already destroyed on application quit." +
" Won't create again - returning null.");
return null;
}
lock (_lock)
{
if (_instance != null)
{
return _instance;
}
var instances = FindObjectsOfType<T>();
int count = instances.Length;
if (count > 0)
{
if (count == 1)
{
return _instance = instances[0];
}
Debug.LogWarning($"[{nameof(Singleton)}<{typeof(T)}>] There should never be " +
$"more than one {nameof(Singleton)} of type {typeof(T)} in the scene, " +
$"but {count} were found. The first instance found will be used, and all others will be destroyed.");
for (int i = 1; i < instances.Length; i++)
{
Destroy(instances[i]);
}
return _instance = instances[0];
}
Debug.Log($"[{nameof(Singleton)}<{typeof(T)}>] An instance is needed in the scene " +
$"and no existing instances were found, so a new instance will be created.");
return _instance = new GameObject($"({nameof(Singleton)}){typeof(T)}").AddComponent<T>();
}
}
}
protected sealed override void Awake()
{
if (_instance == null)
{
_instance = this as T;
}
else if (_instance != this)
{
Destroy(this.gameObject);
}
base.Awake();
}
}
# Epilogue
싱글턴 패턴은 간편하고 유용하다는 장점 덕분에 널리 쓰이는 디자인 패턴 중 하나입니다.
장단점을 숙지하고 적재적소에 사용해서 장점만 극대화하시면 좋을 것 같습니다.
감사합니다.
# Reference
https://mentum.tistory.com/205
https://qastack.kr/gamedev/116009/in-unity-how-do-i-correctly-implement-the-singleton-pattern
http://wiki.unity3d.com/index.php/Singleton
https://github.com/bivisss/unity-reusable-singleton/blob/master/Singleton.cs
'개발 > Design Pattern' 카테고리의 다른 글
SendMessage로 FSM 구현해보기 (Unity3D) (0) | 2022.01.27 |
---|---|
Command Pattern (0) | 2022.01.24 |