오늘은 싱글톤 패턴에 대해 알아보겠습니다.
유니티를 배우면서 싱글톤 패턴을 사용했다/하라는 이야기는 자주 들었는데,
조금 더 구체적으로 어떤 상황에서
어떻게 사용해야 하는 건지 정리해 봤습니다.
싱글톤 패턴이란?
클래스가 자신의 인스턴스 하나만 인스턴스화 할 수 있도록 보장하고,
이 인스턴스에 대해 어디에서나
해당 클래스의 인스턴스에 접근할 수 있도록 하는 패턴입니다.
// 유니티에서의 싱글톤 코드 예시 : 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라고 되어있는 부분에서 문제가 발생할 수 있습니다.
스레드란?
프로세스 내부에서 생성되는, 실제로 작업을 하는 주체
프로세스 ?
메모리에 적재된 실행되는 프로그램
그럼 멀티 스레드는 ?
프로그램 내에서 여러 실행 흐름(스레드)을 동시에 실행하는 기법 정도로 이해하시면 될 것 같습니다.
이때 어떻게 동시에 스레드를 처리할까요?
크게 두 가지로 나뉩니다.
실제로 여러 개를 동시에 처리하는 병렬성과
여러 개를 빠르게 번갈아 가며 처리하는 동시성으로 구분됩니다.
자세한 내용은 아래 블로그로 대체하겠습니다.
[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
'디자인 패턴' 카테고리의 다른 글
디자인 패턴 - MVC(Model-View-Controller)패턴에 대해 알아보자(+ MVP, MVVM) (0) | 2024.11.07 |
---|---|
디자인 패턴에 대해 알아보자#5 옵저버 패턴 (Oberserver) (0) | 2024.11.05 |
디자인 패턴에 대해 알아보자#4 - 팩토리 패턴 (Factory Pattern) (feat : 팩토리 메소드, 추상 팩토리 패턴) (0) | 2024.10.31 |
디자인 패턴에 대해 알아보자#3 - 전략 패턴 (Strategy) (0) | 2024.10.29 |
디자인 패턴에 대해 알아보자 #1 Solid 원칙 (0) | 2024.10.07 |