개요
- Unity에서 Singleton 사용법을 알아보자
- 시간 없으면, 제네릭 방법만 참고
Singleton Pattern은?
- 어떤 클래스의 객체가 최대 n개만 생성되어야 할때
- 전역에서 접근 가능한 객체를 만들려고 할때 사용되는 디자인 패턴이다.
Monobehaviour를 사용한 싱글톤
- Awake가 호출되면서 객체가 초기화 된다.
- Awake의 호출이 필요하기 때문에, Monobehaviour를 상속해야 한다.
- 당연하지만, 이 방식을 사용하면 경우에 따라 NullPointException이 발생한다.
public class DataManager : Monobehaviour
{
public static DataManager Instance;
void Awake()
{
if (Instance == null)
{
Instance = this;
}
DontDestroyOnLoad(this.gameObject);
}
}
Thread-Safe 싱글톤
- Thread만 Safe하고, 실제로는 별로 안전하지 않은 싱글톤
- 이렇게 구현하면 당연하게도 "some objects were not cleaned up when closing the scene"가 발생한다.
- 씬이 close되는데, 정리되지 않은게 있어서 leak이 발생하는 경우다.
- 친구들이 가끔 저렇게 짜오면, "콘솔 안보이냐 병신아?" 라고 하는데..
- 이전 직장에서는 게임사들을 위한 SDK를 개발할때, 이 방식으로 구현했다. (게임회사들 미안)
- 10년차고 C#을 해본적이 책임급이 묻지도 따지지도 않고 회의 시간때마다 wiki 내용은 틀린 방식이라고 지랄하는데..
- 정신병 걸릴뻔...
public class DataManager : MonoBehaviour
{
private const string ObjectName = "DataManager";
private static readonly object _lock = new object();
private static DataManager _instance;
public static DataManager Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
// 컴포넌트 타입으로 검색하여, 처음 발견한 오브젝트를 반환한다.
_instance = FindObjectOfType(typeof(DataManager)) as DataManager;
// 컴포넌트를 못찾았으면
if (!_instance)
{
// 약속된 이름의 GameObject를 찾는다.
var container = GameObject.Find(ObjectName);
// 약속된 이름의 GameObject가 없으면, 생성한다.
if (container == null)
{
container = new GameObject(ObjectName);
}
// 약속된 이름의 GameObject에 컴포넌트를 추가하고,
// 씬이 변경되어도 사라지지 않도록 DontDestroyOnLoad로 지정한다.
_instance = container.AddComponent<DataManager>();
DontDestroyOnLoad(_instance);
// 객체가 생성될때, 최초 1회만 호출되는 초기화 함수
_instance.Initialize();
}
}
}
return _instance;
}
}
public void Initialize()
{
UnityEngine.Debug.Log("Initialize instance");
}
}
제네릭을 이용한 싱글톤
- 개인적으로 싱글톤을 구현할때, 이게 가장 합리적인 방법이라고 생각한다.
- 난 대학교 강의 나가면 아직도 Unity 싱글톤은 이렇게 쓰는거라고 추천한다.
using UnityEngine;
using System.Collections;
// abstract 추상 클래스고
// MonoBehaviour를 상속 받았고
// T는 MonoBehaviour를 상속받은 클래스여야 한다.
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance = null;
private static object _lock = new object();
private static bool _applicationQuit = false;
public static T Instance
{
get
{
// 고스트 객체 생성 방지용
// 위에서 설명한 leak을 방지한다.
if (_applicationQuit)
{
// null 리턴
return null;
}
// thread-safe
lock (_lock)
{
if (_instance == null)
{
// 현재 씬에 싱글톤이 있나 찾아본다.
_instance = FindObjectOfType<T>();
// 없으면
if (_instance == null)
{
// 해당 컴포넌트 이름을 가져온다.
string componentName = typeof(T).ToString();
// 해당 컴포넌트 이름으로 게임 오브젝트 찾기
GameObject findObject = GameObject.Find(componentName);
// 없으면
if (findObject == null)
{
// 생성
findObject = new GameObject(componentName);
}
// 생성된 오브젝트에, 컴포넌트 추가
_instance = findObject.AddComponent<T>();
// 씬이 변경되어도 객체가 유지되도록 설정
DontDestroyOnLoad(_instance);
}
}
// 객체 리턴
return _instance;
}
}
}
// 원칙적으로 싱글톤은 응용 프로그램이 종료될때, 소멸되어야 한다.
// 유니티에서 응용 프로그램이 종료되면 임의 순서대로 오브젝트가 파괴된다.
// 만약 싱글톤 오브젝트가 파괴된 이후, 싱글톤 오브젝트가 호출된다면
// 앱의 재생이 정지된 이후에도, 에디터 씬에서 고스트 객체가 생성된다.
// 고스트 객체의 생성을 방지하기 위해서 상태를 관리한다.
// 앱이 종료될때 호출
protected virtual void OnApplicationQuit()
{
_applicationQuit = true;
}
// 객체가 파괴될때 호출
public virtual void OnDestroy()
{
_applicationQuit = true;
}
}
public class Manager : Singleton<Manager>
{
// 이 생성자는 사용하려고 선언한건 아니다.
// 비록 Singleton으로 열심히 정의해 놨지만
// Manager manager = new Manager() 이렇게 생성하는 것을 방지 할 수 없다.
// 저러면 Manager는 항상 Singleton 클래스라고 볼 수 없는데, Manager의 생성자를 protected로 해줌으로써
// Manager가 항상 Singleton 임을 보장한다.
protected Manager() {}
}
참고