목록

싱글톤(Singletone) vs C#정적클래스(static class) 차이점 무엇?

우리가 흔히 쓰는 디자인 패턴인 싱글톤과 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";
      }
      1. Generic을 사용한 방법에서는 Singleton을 상속받은 클래스에서는 생성자를 반드시 protected로 선언을 해서 외부에서는 생성이 되지 않게 막는다.

      2. 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
	}
}

#참고