본 게시글은 면접준비 및 자기계발을 목적으로 작성된 게시글입니다
공부한 내용을 토대로 남들에게 설명할 수 있도록 이해하는 과정에 작성한 게시글이니 참고바랍니다
소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글톤 패턴이라고 한다. 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다.ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ- 위키백과
원조 GoF에 따르면 싱글톤 패턴은 다음을 수행합니다
전체 씬에서 행동을 조정하는 오브젝트가 정확히 하나만 필요할 때 유용합니다
예를 들면 씬에 메인 게임 루프를 총괄하는 게임 관리자가 딱 하나만 필요할 수 있습니다
한 번에 하나의 파일 관리자만 파일 시스템에 작성하기를 원할 수도 있습니다
이러한 관리자 레벨의 오브젝트는 대체로 싱글톤 패턴을 적용하기에 좋은 대상입니다
using UnityEngine;
public class SimpleSingleton : MonoBehaviour
{
public static SimpleSingleton Instance;
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
}
공용 정적 Instance
는 씬에서 Singleton
의 인스턴스 하나를 갖게 됩니다
Awake
메서드에서 인스턴스가 이미 설정되어 있는지 확인하고 Instance
가 현재 null
이면 인스턴스는 이 특정 오브젝트로 설정됩니다 (이 오브젝트는 씬에서 가장 첫 번째 싱글톤이어야 합니다)
그렇지 않으면 이 인스턴스는 복제본이어야 하며, 싱글톤이 씬에서 그러한 컴포넌트를 오직 하나만 갖게 하도록 Destroy(gameObject)
를 호출합니다
런타임에 계층 구조에서 둘 이상의 게임 오브젝트에 스크립트를 연결하는 경우, Awake
의 로직은 첫 오브젝트만 유지하고 나머지는 폐기합니다
Simple Singleton
은 정상적으로 작동하지만 다음과 같은 문제가 있음
싱글톤은 보편적인 관리자 스크립트 역할을 하는 경우가 많으므로 DontDestroyOnLoad
를 사용하여 지속성을 갖게 해서 이점을 활용할 수 있습니다
나아가 지역 인스턴스화를 사용하면 싱글톤이 처음으로 필요할 때 자동으로 만들어지도록 할 수 있습니다. 게임 오브젝트를 만들고 적절한 싱글톤 컴포넌트를 추가하기 위한 로직만 있으면 됩니다
public class Singleton : MonoBehaviour
{
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.gameOjbect);
}
else
{
Destroy(gameObject);
}
}
private static void SetupInstance()
{
instance = FindObjectOfType<Singleton>();
if(instance == null)
{
GameObject gameObj = new GameObject();
gameObj.name = "Singleton";
instance = gameObj.AddComponent<Singleton>();
DontDestroyOnLoad(gameObj);
}
}
}
Instance
는 이제 프라이빗 instance 지원 필드의 공용 프로퍼티입니다. 싱글톤을 처음 참조할 때는 가져오려는 인스턴스가 있는지 확인하고 인스턴스가 존재하지 않으면, SetupInstance
메서드가 적절한 컴포넌트를 갖는 게임 오브젝트를 만듭니다
DontDestroyOnLoad(gameObject)
는 씬 로드로 인해 계층 구조에서 싱글톤이 지워지지 않게 합니다
싱글톤 인스턴스는 지속적이며, 게임에서 씬을 변경해도 액티브 상태를 유지합니다
어떤 버전의 스크립트도 같은 씬 내에서 여러 싱글톤을 만드는 방법을 다루지 않습니다
예를 들어 AudioManager
로서 작동하는 싱글톤과 GameManager
로 작동하는 다른 싱글톤이 필요한 경우, 이 두 싱글톤은 현재 공존할 수 없습니다
관련 코드를 복제하고 로직을 각 클래스로 붙여 넣어야하는데 대신 다음과 같이 스크립트의 제네릭 버전을 만듭니다
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;
}
}
public virtual void Awake()
{
RemoveDuplicates();
}
private static void SetupInstance()
{
instance = (T)FindOjbectOfType(typeof(T));
if(instance == null)
{
GameOjbect gameObj = new GameObject();
gameObj.name = typeof(T).Name;
instance = gameObj.AddComponent<T>();
DontDestroyOnLoad(gameObj);
}
}
private void RemoveDuplicates()
{
if(instance == null)
{
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
이렇게 하면 어떤 클래스나 싱글톤으로 변환할 수 있습니다. 클래스를 선언하면 간단히 제네릭 싱글톤에서 상속합니다. 예를 들어 GameManager
라는 MonoBehaviour
를 다음과 같이 선언하여 싱글톤으로 만들 수 있습니다
public class GameManager : Singleton<GameManager>
{
// ...
}
이제 필요할 때마다 언제나 공용 정적 GameManager.Instance를 참조할 수 있습니다
싱글톤은 여러 측면에서 SOLID 원칙
을 위반한다는 점에서 다른 패턴과 다르고, 여러 이유로 많은 개발자들이 싱글톤에 호의적이지 않습니다
📌 Race Condition(경쟁 상태) : 둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태
싱글톤에 대한 반감은 상당한 편입니다
수년 동안 유지 관리하려는 엔터프라이즈 수준의 게임을 제작할 예정이라면 싱글톤은 적절한 선택이 아닙니다
하지만 엔터프라이즈 수준의 애플리케이션이 아닌 게임도 많으며, 그러한 게임은 비즈니스 소프트웨어에 준하는 방식으로 계속 확장할 필요가 없습니다
사실 싱글톤에는 확장성이 필요하지 않은 소규모 게임 개발에 유용할 수 있는 장점이 있습니다
이러한 방식으로 씬의 다른 모든 게임 오브젝트에서 항상 액세스할 수 있는 관리자 오브젝트(게임 플로 관리자 또는 오디오 관리자 등)를 만들 수 있습니다
또한 오브젝트 풀을 구현한 경우, 풀링 시스템을 싱글톤으로 디자인하면 풀링된 오브젝트를 더 쉽게 가져올 수 있습니다
ㅤㅤㅤㅤㅤㅤㅤㅤ | ㅤㅤㅤㅤSingletonㅤㅤㅤㅤ | ㅤㅤㅤㅤStatic Classㅤㅤㅤㅤ |
---|---|---|
원리 | 사용자 요청시 하나의 인스턴스를 생성해 재사용 | 인스턴스를 만들 수 없고, 생성자도 갖지 않는다 |
인터페이스 구현 | 가능 | 불가능 |
follow OOP | O | X, 절차적 디자인으로 '함수'에 가깝다 |
override | 가능 | 불가능 |
load | 필요할 때 Lazy Load 가능 | Static Binding in Compile-Time |
performance | 상대적으로 느림 | 빠름 (by static binding) |
test | Easier | Hard to mock and test |
저장 위치 | Heap | Stack |
👉 Singleton을 쓰는 이유
DBCP(Database Connection Pool)
처럼 공통된 객체를 여러 개 생성해야 하는 상황에 많이 사용👉 Static Class를 쓰는 이유
Global Access
를 제공할 때 유용Static Binding
으로 싱글톤보다 좀 더 빠름static
을 붙여 사용할 수 없음 (inner class일 때만 가능)📚 참고출처
- Level Up Your Code With Game Programming Patterns - 2021 LTS Edition Unity
- Elenaljh.log : velog.io/@hye9807/싱글톤과-정적static-클래스-차이
안녕하세요 개발자 지한님 벨로그 잘 보고 있습니다~!
제네릭 싱글톤 클래스 관련해서 추가 내용 전달해보고자 합니다.
하나(FindObjectOfType) 가 아닌 (FindObjectsOfType) 중복된 인스턴스를
안정적으로 처리하는 게 더 좋아보입니다.
하는 기능을 제공하면 좋을 것 같습니다.
제가 사용하고 있는 제네릭 싱글톤 코드도 공유해드립니다!