일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Modern C++
- level3
- Euclidean
- two pointer
- knapsack Problem
- PrefixSum
- 프로세스 상태
- level1
- BFS
- 8-Puzzle
- stack
- SWEA
- programmers
- LEVEL2
- trie
- algorithm
- Gold
- 3D RPG
- Unity
- Silver
- solid 원칙
- BOJ
- 프로그래머스
- Project
- dirtyflag pattern
- Flyweight Pattern
- effective C++
- Bronze
- Zenject
- binary search
- Today
- Total
Patrick's Devlog
[Unity] Singleton Pattern 본문
Singleton Pattern
유일무이한 존재의 인스턴스, 즉 하나의 프로젝트에서 하나의 객체만 존재하는 것이다. 보통은 클래스를 생성할 때 인스턴스화한 후 여러 개 생성이 가능하나, 클래스가 자신의 인스턴스 하나만 인스턴스화할 수 있도록 보장하는 것이 싱글톤 패턴이다.
여러 곳에서 불러와야 하므로 해당 단일 인스턴스에 대한 손쉬운 전역 액세스를 제공하고 하나의 객체만 존재할 필요가 있을 때 사용된다. (example> 상태창, 게임 매니저, 오디오 매니저, 파일 관리자 UI Setting etc)
그러나 모든 곳에서 접근해야 하다보니 안티 패턴(비효율적이거나 비생산적인 패턴)으로 취급되기도 하며, 커플링이 발생하기 쉬운구조이다. 생성 시점이 명확하지 않다보니 테스트나 디버깅이 불편하다는 점이다. 싱글톤 패턴을 쓰되 주의하면서 사용해야 한다.
예제
싱글톤의 클래스 다이어그램을 확인해보면 구현 자체가 어렵진 않다. 인스턴스가 본인을 잡고있다는 것을 알 수 있다.

해당 다이어그램을 코드로 나타내면 아래와 같다. 싱글톤을 생성하는 코드의 버전은 Scene에 올리기 위한 MonoBehaviour를 상속했을 때와 상속하지 않았을 때 두 가지로 구현되어 있다.
MonoBehaviuor 상속
public class SimpleSingleton : MonoBehaviour { // Scene에 올려야할 때
public static SimpleSingleton instance;
private void Awake() {
if(instance == null)
instance = this;
else
Destroy(gameObject);
}
// 새로운 scene을 부르면 GameObject가 지워짐
// 사용되기 전 하이어라키에서 셋업되어야 함
}
해당 클래스들은 자기 자신의 인스턴스를 정적으로 지니고 있다. 이렇게 되면 여러 인스턴스가 생기더라도 메모리 공간은 하나만 잡게되고, 하나의 인스턴스만 존재하게 하도록 한다.
MonoBehaviour를 상속받는 SimpleSingleton의 코드의 문제점은 새로운 Scene을 부르게 되면 GameObject가 지워지게 되며, 사용되기 전에 하이어라키에 무조건 셋업이 되어야 한다. 셋업이 되지 않으면 사용될 때 객체가 없을 수 있으므로 문제가 생긴다. 이러한 문제를 해결하기 위해 아래 코드처럼 수정하면 된다.
public class Singleton : MonoBehaviuor {
private static Singleton instance;
public static Singleton Instance {
get {
if (instance == null) Setupinstance();
return instance;
}
}
private void Awake() {
if(instance == null) {
instance = this;
DontDestroyOnLoad(this.gameObject);
}
else Destroy(gameObject);
}
private static void SetupInstance() {
instance = FindObjectOfType<Singleton>();
// singleton을 찾음
if (instance == null) { // 존재하지 않을 때 생성
GameObject gameObj = new GameObject();
gameObj.name = "Singleton";
instance = gameObj.AddComponent<Singleton>();
DontDestroyOnLoad(gameObj);
}
}
}
중요한 점은 DontDestroyOnLoad를 추가하여 새로운 신을 로딩했을 때, 해당 객체를 죽이지 않고 살리라고 코드에 명시해야 한다. 간혹 싱글톤이 게임매니저, 오디오 매니저 등 여러가지가 존재할 때 generic 타입으로 명시하여 구현하는 경우도 있다.
public class Singleton<T> : MonoBehaviuor 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 void Awake() {
RemoveDuplicates();
}
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);
}
}
private RemoveDuplicates() {
if (instance == null) {
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else Destroy(gameObject);
}
}
// ---
public class GameManager : Singleton<GameManager> { ... }
// Generic 타입 Singleton 사용 예제
여러 싱글톤을 사용할 수 있으며, 코드 재사용성도 높아지게 된다.
MonoBehaviour 비상속
public class SimpleSingleton { // Scene에 올리는 객체가 아닐 때
public static SimpleSingleton Instance;
public static SimpleSingleton Instance {
get {
if (instance == null) instance = new Singleton(); // thread safe X
return instance;
}
}
}
위의 코드는 작동에는 큰 문제가 없으나, 멀티 스레드 환경에서는 문제가 될 수 있다. 두 스레드에서 동시에 new Singleton()에 접근할 수 있으므로 두개가 동시에 생성될 수 있다. MonoBehaviuor에서는 해당 부분이 문제가 되지 않는 이유가 유니티에서는 멀티 스레드 환경을 사용할 수 없으므로, 싱글 스레드만 요구하게 된다.
위의 코드에서 해당 문제를 해결하기 위해 아래 코드처럼 lock을 사용하면 된다.
public class SimpleSingleton {
private static volatile SimpleSingleton instance = null;
// volatile : 멀티 스레드 환경에서 안정성을 위해 사용하는 키워드
// -> 캐시 최적화를 건너뛰는 대신 멀티 스레드 환경에서 안정적으로 접근
private static readonly object padlock = new object();
...
public static SimpleSingleton Instance {
get {
if (instance == null) {
lock(padlock) { // critical section
if (instance == null) {
instance = new SimpleSingleton();
}
}
}
return instance;
}
}
}
lock을 사용하게 되면 A 스레드가 먼저 접근했을 때, lock이 풀릴 때까지 B 스레드는 접근하지 못한다. instance 확인을 두번 하는 이유는 한번만 단순히 확인했을 때, 간발의 차로 A가 생성하고 있을 때 B는 instance가 존재하지 않는다고 판단하는 경우도 있기 때문이다. 이러한 경우를 회피하기 위해 instance 확인을 두번 하게 된다.
위의 코드로는 멀티 스레드 환경에서 안정성을 완전히 보장하기 어려우므로, 아래의 코드를 통해 더 안정성을 충족할 수 있다.
public sealed class SimpleSingleton {
private static readonly SimpleSingleton instance = new SimpleSingleton();
private SimpleSingleton() { }
public static SimpleSingleton Instance {
get { return instance; }
}
}
Singleton 선언 중 생성하면 문제를 해결할 수 있다. 하지만 클래스가 생성될 때, 바로 생성되다 보니 로딩시간이 조금 길어질 수 있는 단점이 존재할 수 있다. 상황에 맞춰서 코드를 구현하면 된다.
참고 문서
'Unity > Design Pattern' 카테고리의 다른 글
[Unity] Strategy Pattern (2) | 2024.10.10 |
---|---|
[Unity] MVC, MVP, MVVM Pattern (1) | 2024.10.08 |
[Unity] Object Pool Pattern (0) | 2024.10.03 |
[Unity] Factory Pattern (0) | 2024.10.01 |
[Unity] Command Pattern (0) | 2024.09.30 |