목차
Singleton
게임개발을 하며 디자인 패턴을 생각하면 가장 처음 떠오르는, 아마 GoF의 디자인패턴 책에서 가장 유명하고 가장 무분별하게 쓰이는 패턴이 싱글톤 패턴일 것입니다.GoF를 포함한 모든 글에서 싱글톤을 남용하지 말라 강조하지만 잘 지켜지지 않습니다.
'게임프로그래밍 패턴 (Robert Nystrom)'에서도 '이번 장은 어떻게 하면 패턴을 안 쓸 수 있는지를 보여준다는 점에서 다른 장과는 정반대다.'라 말하는 것을 볼 때 "어떻게 하면 싱글톤을 안쓰고 문제를 해결할 수 있을까?"에 초점을 맞추는 것이 싱글톤을 이해하는 좋은 방향성이라 생각됩니다.
목적
싱글톤은 '오직 1개의 인스턴스만 갖도록 보장'한다는 점과 '전역 접근을 제공'한다는 점 총 2개의 문제를 한 번에 해결하는 디자인 패턴입니다.
● 오직 1개의 인스턴스만 갖도록 보장
인스턴스가 여러 개면 제대로 작동하지 않는 몇몇 상황이 있을 수 있습니다. 이런 경우 싱글톤을 사용하면 클래스가 인스턴스를 하나만 가지도록 컴파일 단계에서 강제할 수 있습니다.
● 전역 접근 제공
여러 클래스에서 싱글톤 클래스의 인스턴스를 따로 생성할 수 없기에 어떻게 접근할지에 대한 고민이 생깁니다. 이 때 싱글톤 패턴은 전역으로 접근할 수 있는 기능을 제공합니다.
이 덕분에 다음과 같은 장점도 가지고 있습니다.
● 사용되기 전에는 인스턴스를 생성하지 않는다
싱글톤 클래스는 처음 사용될 때 인스턴스를 생성합니다. Unity Memory Profiler를 확인하면 최초 로드를 하면서 생성되는 것을 알 수 있습니다.
● 런타임에 초기화 됩니다
전역 접근 제공을 위해 정적 멤버 변수를 대안으로 쓰기도 합니다. 이 때 정적 멤버 변수는 자동 초기화되는 문제가 있다는데, 이는 컴파일러가 main함수 호출 전에 정적 변수를 초기화하기 때문에 프로그램 실행 이후에 알 수 있는 정보(파일 내용 등)은 쓸 수 없다는 문제점입니다. 또한 정적 변수 초기화 순서도 컴파일러가 보장하지 않기 때문에 한 정적 변수가 다른 정적 변수를 안전하게 의존할 수 없습니다.
이를 위해 싱글톤에서 *게으른 초기화 방법으로 초기화 순간을 뒤로 늦춰, 프로그램 실행 이후 필요한 정보를 준비하고 초기화할 수 있습니다. 또 순환 의존만 없다면 싱글톤간의 참조도 가능합니다.
* 게으른 초기화 : 객체 생성, 값 계산, 일부 기타 비용이 많이 드는 과정을 처음 필요한 시점까지 지연시키는 기법.
● 싱글톤은 상속할 수 있습니다
파일 시스템 래퍼가 크로스 플랫폼을 지원한다면 추상 인터페이스 아래 플랫폼 별 구체 클래스를 만들고, 인터페이스에 인스턴스로 접근할 수 있습니다. 또한 단위 테스트용 *모의 객체(mock object)를 만들 때도 유용하다고 합니다.
* 주로 OOP에서 개발한 프로그램을 테스트할 경우 실제 모듈을 흉내내는 가짜 모듈을 작성하여 테스트의 효용성을 높이는데 사용하는 객체
문제점
● 전역변수
싱글톤을 전역 접근을 제공한다는 점에서 클래스로 캡슐화한 전역변수와 다를게 없습니다.
전역변수는 접근이 쉬운 만큼 온갖 클래스에서 다 쓸 수 있습니다. 어느 클래스는 전역 변수를 이상하게 변경하고 있을 수 있고, 마치 거미줄처럼 얽히고 설켜 복잡한 코드와 커플링을 만들 수 있습니다.
또한, 전역 변수는 멀티스레딩과 같은 동시성 프로그래밍에 적합하지 않습니다.
● 언제나 2개의 문제를 한 번에 해결한다.
보통 싱글톤을 쓸 때 '단 하나의 인스턴스임을 보장'과 '전역 접근' 2가지 문제를 한 번에 해결합니다. 하지만 문제가 1개라면 2개의 문제를 해결하는게 오히려 독일 수 있습니다. 단 하나의 인스턴스임을 보장하고 싶지만 이 때 전역 접근 또한 제공한다면 다른 클래스에서 무분별하게 인스턴스를 훼손시킬 수도 있습니다.
▶️ 단 하나의 인스턴스 보장
한 가지 방법으로 클래스 내에 private 전역 변수 1개를 만들어 생성자에서 *단언문을 사용해 디버깅 중 단 하나의 인스턴스만 가지도록 보장할 수 있습니다. 단 싱글톤은 컴파일 시간에 단일 인스턴스를 보장하지만, 이 방법은 런타임에 인스턴스 개수를 확인한다는 단점이 있습니다. 다른 방법을 고민해볼 수도 있습니다.
class FileSystem
{
private static bool instantiated;
FileSystem()
{
Debug.Assert(!instantiated);
instantiated = true;
}
~FileSystem()
{
instantiated = false;
}
}
*단언문 : 코드에 제약을 넣을 때 쓰며 값이 거짓이면 중지합니다.
▶️ 전역 접근
정적 멤버 변수 혹은 전역 클래스를 만들어 접근점을 제공할 수 있습니다. 혹은 의존성 주입과 같이 객체를 필요로 하는 함수에 인수로 제공할 수 있습니다. 옵저버 패턴을 통해 전달할 수도 있습니다.
가장 좋은 해결법은 충분한 추상화와 구조를 설계해 패턴을 굳이 쓸 상황을 만들지 않도록 노력할 수 있습니다.
● 게으른 초기화는 제어할 수 없습니다.
가상 메모리도 사용할 수 있고 성능 요구도 심하지 않은 데스크톱 PC에서는 게으른 초기화가 괜찮은 기법이라 합니다.
하지만 게임에 경우는 상황이 다른데, 시스템을 초기화할 때 시간이 꽤 걸리기에 처음 소리를 재생할 때 게으른 초기화를 하게 만들면 전투 도중에 초기화가 시작되어 프레임 드랍이 생길 수 있습니다. 또한 메모리 단편화를 막기 위해 힙에 메모리 할당을 여러 기준을 두는데 위 시스템을 초기화할 때 적절한 위치와 시점도 찾아야 합니다.
Unity Singleton
Unity에서 싱글톤을 만드는 방법은 크게 2가지로 나뉩니다. Unity API인 MonoBehaviour를 상속받아 만들거나, 기본 C# 파일로 만드는 방법이 있습니다.
MonoBehaviour를 상속받는 경우 Unity API가 메인 스레드에서만 관리를 받기에 싱글 스레드로 작동합니다. 그래서 스레드에 관한 고민을 안해도 됩니다. 또한 Unity API를 사용할 수 있습니다. 대신 GameObject로 존재하니 씬 이동시 이미 해당 씬에 있다면 어떤을 사용할지, 중복 생성, 파괴 시 고려할 점들이 있습니다.
public abstract class MonoBehaviourSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance
{
get
{
if(instance == null)
{
instance = FindFirstObjectByType<T>();
if(instance == null)
{
SetupInstance();
}
}
return instance;
}
}
private static void SetupInstance()
{
var singletonObject = new GameObject
{
name = typeof(T).Name
};
instance = singletonObject.AddComponent<T>();
DontDestroyOnLoad(singletonObject);
}
private void Awake()
{
RemoveDuplitcates();
}
private void RemoveDuplitcates()
{
if(instance == null)
{
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
/*
How to use
public class TestSingleton : MonoBehaviourSingleton<TestSingleton>
{
// ....
}
*/
다른 예시로 Unity VisualScripting의 Singleton<T>도 확인하면 좋을 것 같습니다.
기본 C# 클래스로 만들 경우 씬 이동 등을 고려하지 않아도 됩니다. 대신 멀티 스레드에서 접근하는 경우가 있다면 이에 대한 대비가 필요합니다.
다음 예시는 volatile 구문을 사용해 CPU 내 캐시와 메모리를 동기화하고(캐시 최적화 제외) lock을 사용함으로써 경쟁 상태를 해결합니다.
*C#에서는 volatile 구문이 캐시 최적화 제외, 코드 최적화 제외 등등 여러가지 일을 하는 형태로 작동하기에 해당 구문 사용을 추천하지 않는다고 합니다.
static, readonly, 즉시초기화를 쓰면 immutable하게 생성을 즉시하기에 스레드 세이프하게 사용할 수 있습니다.
정리
만능 마술봉처럼 매니저에 다 넣어버리는고 쉽게 쓰다보니 여러 글에서 오남용하지 말라는 내용이 많은 것 같습니다. 특히 디자인패턴을 보면서 싱글톤만큼 '망치를 든 사람에겐 모든 문제가 못으로 보인다'가 어울리는 패턴이 없는 것 같습니다.
장기적으로 볼 때는 싱글톤 사용 자체를 자제하는 것이 공부하는 것에는 더 도움이 되는 방향이라 생각됩니다.
게임에서는 게임 매니저와 같이 매니저라는 이름을 가지고 다른 객체를 관리하는 용도로 존재하는 경우가 많은 것 같은데 이때 관리하는 싱글톤 클래스가 진짜로 필요한 것인지 고민하고 장단점을 고려하며 써야할 것 같습니다.
Singleton이 글에서 대부분 '싱글톤'으로 번역하는데, 외국 글을 번역한 글에서는 대부분 '싱글턴'으로 쓰고 있고 실제 발음은 '톤'보다는 '턴'에 가깝다는 것 같습니다.
'디자인패턴' 카테고리의 다른 글
상태 패턴 (State) (0) | 2024.08.03 |
---|---|
의존성 주입 (Dependency Injection) (0) | 2024.06.27 |
Unity MVP 패턴 (0) | 2024.06.24 |
MVVM 패턴 (0) | 2024.06.20 |
MVP 패턴 (0) | 2024.06.19 |