우리가 흔히 쓰는 디자인 패턴인 싱글톤과 C#에서 지원하는 정적클래스는 기능이 비슷해서 헷갈려는데, 한번 둘의 차이점이 무엇인지 언제 쓰는게 좋은지 정리하고자 한다.
#특징
-
싱글톤
- 싱글톤은 클래스를 확장하고 인터페이스를 구현할 수 있다.
- 느린 또는 비동기적으로 초기화 할 수 있어 초기화시점 조절 가능.
- 파생 된 유형을 반환 할 수 있는 기능(느린 로딩 및 인터페이스 구현의 조합)
if( useD3D )
IRenderer::instance = new D3DRenderer
else
IRenderer::instance = new OpenGLRenderer
-
정적클래스
- static 메소드를 가지는 클래스를 일컭는다.
- 더 나은 성능 - 정적 메서드는 컴파일 타임에 결함 된다.
- 실제로 표준 클래스가 아니다, 함수와 변수가 있는 네임스페이스다.
- 객체 지향 프로그래밍 원칙을 위반 하므로 지양하자.
- 스레드 관리 구현이 어렵다.
-
그 외
- 싱글 톤은 느리게 또는 비동기 적으로 초기화 될 수 있지만 정적 클래스는 일반적으로 처음로드 될 때 초기화된다.
- 힙에 싱글 톤 객체 저장하지만 스택에 정적 객체 저장
- 싱글 톤의 또 다른 장점은 쉽게 직렬화 할 수 있다는 것. 이 직렬화는 디스크에 상태를 저장하거나 원격으로 보낼 필요가있을 때 필요할 수 있다.
- 정적 클래스는 정적 메서드 만있는 클래스이며, 더 좋은 단어는 “함수”이다. 정적 클래스에 구현 된 디자인 스타일은 순전히 절차 적이며, 반면에 글톤은 객체 지향 디자인과 관련된 패턴이다. 그것은 일생 동안 그 특별한 역할의 인스턴스가 오직 하나만 존재한다는 것을 보장하는 생성 프로 시저를 가진 다형성과 같은 모든 가능성을 가진) 객체의 인스턴스이다.
#어느걸 사용?
=> 상황에 맞춰 사용
초기부터 필요한 utility메소드들은 정적클래스로 사용하는게 좋을거 같고,
그 외 다른것에는 싱글톤을 사용하는게 좋은거 같다.
#유니티에서의 싱글톤 사용
-
C# 싱글톤 버전
-
간단한 싱글톤 - 쓰레드 세이프 하지 않은 버전
//Bad code! Do not use! public sealed class Singletone { private static Singleton instance = null; private Singleton() { } public static Singleton Instance { get { if(instance == null) { instance = new Singleton(); } return instance; } } }
-
간단한 싱글톤 - 세이프 한 버전
public sealed class Singleton { private static Singleton instance = null; private static readonly object padlock = new object(); private Singleton() { } public static Singleton Instance { get { lock(padlock) { if(instance == null) { instance = new Singleton(); } return instance; } } } }
-
제네릭을 사용한 방법
using System; using System.Reflection; public class Singleton<T> where T : class { private static object syncobj = new object(); private static volatile T instace = null; public static T Instance { get { if(instace == null) { CreateInstance(); } return instace; } } private static void CreateInstance() { lock(syncobj) { if(instace == null) { Type t = typeof(T); // public constructors..가 있나 확인. // ConstructorInfo는 클래스 생성자 특성을 검색하고 생성자 메타 데이터에 대한 액세스를 제공. ConstructorInfo[] ctors = t.GetConstructor(); if(ctors.Length > 0) { // 한개 이상 존재 시 exeption 발생. throw new InvalidOperationException(String.Format("{0} has at least one accesible ctor making it impossible to enforce singleton behaviour", t.Name)); } //using System에서 지원 Activator는 해당 타입의 인스턴스를 동적으로!! 생성할 수 있게 도와준다. instace = (T)Activator.CreateInstance(t, true); } } } }
여기서 리플렉션 기능을 사용하여 생성된 인스턴스 수를 체크 후 1개 이상이면 익셉션 에러를 띄웠는데..
C#리플렉션..? 잘 모르니 나중에 정리…
-
-
MonoBehaviour 싱글톤 버전
-
간단한 방법
using UnityEngine; using System.Collections; public class Singleton: MonoBehaviour { public static Singleton instance = null; //Static instance of GameManager which allows it to be accessed by any other script. //Awake is always called before any Start functions void Awake() { //Check if instance already exists if (instance == null) { //if not, set instance to this instance = this; } //If instance already exists and it's not this: else if (instance != this) { //Then destroy this. This enforces our singleton pattern, meaning there can only ever be one instance of a GameManager. Destroy(gameObject); } //Sets this to not be destroyed when reloading scene DontDestroyOnLoad(gameObject); } }
-
제너릭 사용한 방법
using UnityEngine; using System.Collections; /// <summary> /// Be aware this will not prevent a non singleton constructor /// such as `T myT = new T();` /// To prevent that, add `protected T () {}` to your singleton class. /// As a note, this is made as MonoBehaviour because we need Coroutines. /// </summary> public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour { private static T _instance = null; private static object _syncobj = new object(); private static bool appIsClosing = false; public static T Instance { get { if (appIsClosing) return null; lock (_syncobj) { if (_instance == null) { T[] objs = FindObjectsOfType<T>(); if (objs.Length > 0) _instance = objs[0]; if (objs.Length > 1) Debug.LogError("There is more than one " + typeof(T).Name + " in the scene."); if (_instance == null) { string goName = typeof(T).ToString(); GameObject go = GameObject.Find(goName); if (go == null) go = new GameObject(goName); _instance = go.AddComponent<T>(); } } return _instance; } } } /// <summary> /// When Unity quits, it destroys objects in a random order. /// In principle, a Singleton is only destroyed when application quits. /// If any script calls Instance after it have been destroyed, /// it will create a buggy ghost object that will stay on the Editor scene /// even after stopping playing the Application. Really bad! /// So, this was made to be sure we're not creating that buggy ghost object. /// </summary> protected virtual void OnApplicationQuit() { // release reference on exit appIsClosing = true; } }
-
제너릭 싱글톤 사용방법
public class Manager : Singleton<Manager> { protected Manager () {} // guarantee this will be always a singleton only - can't use the constructor! public string myGlobalVar = "whatever"; }
-
Generic을 사용한 방법에서는 Singleton을 상속받은 클래스에서는 생성자를 반드시 protected로 선언을 해서 외부에서는 생성이 되지 않게 막는다.
-
appIsClosing 변수의 경우는 유니티 에디터 상에서 갑자기 나가버리는 경우 에러가 발생하는 경우라 필요한 변수. 이렇게 로직 처리 시에는 instance가 null이 오는 경우가 생기므로 null처리를 따로 해야한다.
만약 씬 전환시에도 파괴되지 않게 하려면 상속받는 자식 클래스 awake함수에 DontDestroyOnLoad(this.gameObject); 코드를 넣어주자!
-
-
#가치 알아두면 좋은.
- lock키워드
C#의 lock 키워드는 특정 븍럭의 코드(Critical Section이라 부른)를 한번에 하나의 쓰레드만 실행 할 수 있도록 해준다.
lock()의 파라미터에는 임의의 객체를 사용할 수 있는데, 주로 object 타입의 private 필드를 지정한다.
즉, private object obj = new object() 와 같이 private 필드를 생성한 후, lock(obj)와 같이 지정한다.
특히, 클래스 객체를 의미하는 this를 사용하여 lock(this)와 같이 잘못 사용할 수 있는데, 이는 의도치 않게 데드락을 발생시키거나 Lock Granularity를 떨어뜨리는 부작용을 야기할 수 있다.
즉, 불필요하게 객체 전체 범위에서 lock을 걸어 다수의 메서드가 공유 리소스를 접근하는데 Lock Granularity를 떨어뜨리는 효과가 있으며, 또한 만약 외부에 해당 클래스 객체를 lock하고 그 객체 안 메서드를 호출한 경우 그 메서드 안에서 다시 lock(this)를 하는 경우, 이미 외부에서 잡혀있는 lock이 풀리기를 메서드 내에서 계속 기다릴 것이므로 데드락이 발생될 수 있다.
그리고 Critical Section 코드 블록은 가능한 한 범위를 작게하는 것이 좋은데, 이는 필요한 부분만 Locking한다는 원칙에 따른 것이다.
using System;
using System.Threading;
namespace MultiThrdApp
{
class MyClass
{
private int counter = 1000;
// lock문에 사용될 객체
private object lockObject = new object();
public void Run()
{
// 10개의 쓰레드가 동일 메서드 실행
for(int i = 0; i < 10; i++)
{
new Thread(SafeCalc).Start();
}
}
//Thread-Safe 메서드
private void SafeCalc()
{
//한번에 한 쓰레드만 lock블럭 실행
lock(lockObject)
{
//필드값 변경
counter++;
//가정 : 다른 복잡한 일을 한다
for(int i = 0; i < counter; i++)
for(int j = 0; j < counter; j++)
// 필드값 읽기
Console.WriteLine(counter);
}
}
//출력 예:
// 1001
// 1002
// 1003
// 1004
// 1005
// 1006
// 1007
// 1008
// 1009
// 1010
}
}