본문 바로가기

Unity/작업방식

Unity: Workflow. 자주 사용되는 싱글톤 패턴 Singleton

 

한 번의 객체 생성으로 전역성을 띄고 어디서든 접근이 가능하기 때문에 편리하게 사용되나 필수적으로 커플링이 생긴다는 점에서, 남용하지 않는 선을 지켜 적당히 사용하게 됩니다. 게임 아키텍쳐가 매우 복잡하고 크지 않는 이상 10개가 넘는 싱글톤 객체가 생기는 일은 잘 없고, 필요한 매니저 클래스들 정도만 활용하는 편입니다. 유니티에서는 MonoBehaviour를 상속받는 컴포넌트를 토대로 구조가 쌓이기 때문에, MonoBehaviour를 위한 싱글턴 템플릿 클래스를 만들어두면 편리합니다. 그래서 제가 자주 사용하는 싱글톤 클래스들 코드를 기록해두려고 합니다.

 

1. 일반 클래스 싱글톤 템플릿

데이터 DB나 리소스를 관리해주는 클래스, 또는 MonoBehaviour의 인스펙터 기능이 필요 없이 코드로만 동작하게되는 매니저 클래스가 필요할 때 상속받아 사용합니다. 특별한 점은 없고, new() 가능한 T 타입을 받아 제네릭 클래스로 동작하게 구현했습니다.

 

namespace UnityEngine.Pattern {
  /* Usage
   * public class CharacterDB : Singleton<CharacterDB> { }
   * */
  /// <summary>
  /// 
  /// </summary>
  /// <typeparam name="T"></typeparam>
  public abstract class Singleton < T > where T: Singleton < T > , new() {
    private static T instance =
      default;

    public static T Instance {
      get {
        if (instance == null) {
          instance = new T();

          Debug.Log($"Singleton created :: {typeof(T).Name}");
        }

        return instance;
      }
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void ReloadDomain() {
      instance = null;

      Debug.Log($"Singleton cleaned up :: {typeof(T).Name}");
    }
  }
}

 

 

2. 동적 생성이 가능한 MonoBehaviour 싱글톤 템플릿

보통 이 패턴이 범용적으로 사용됩니다. 타입을 검색하고, 발견되면 그대로 사용하고 없다면 새로 생성합니다. 그러나 대체로 이미 객체가 생성되어있는 경우 instance 필드에 할당이 되어 있을 것이므로 검색은 그다지 필요없는 수행입니다. SingletonMonoBehaviour, MonoSingleton 등의 작명 방식이 있지만 Singleton의 S를 Prefix로 붙여 구분했습니다.

 

namespace UnityEngine.Pattern {
  /* Usage
   * public class GameModeManager : SMonoBehaviour<GameModeManager> { }
   */
  public abstract class SMonoBehaviour < T >: MonoBehaviour where T: SMonoBehaviour < T > {
    private static T instance =
    default;

    public static T Instance {
      get {
        if (instance == null) {
          instance = FindObjectOfType < T > ();

          if (instance == null) {
            instance = new GameObject($"[{typeof(T).Name}]").AddComponent < T > ();
            DontDestroyOnLoad(instance.gameObject);

            Debug.Log($"SMonoBehaviour->Create({typeof(T).Name})");
          } else {
            Debug.Log($"SMonoBehaviour->Find({typeof(T).Name})");
          }
        }

        return instance;
      }
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void ReloadDomain() {
      instance = null;

      Debug.Log($"SMonoBehaviour->CleanUp({typeof(T).Name})");
    }
  }
}

 

 

3. 동적 생성이 불가능한 로컬 씬 한정 MonoBehaviour 싱글톤 템플릿

싱글톤 패턴이 적용된 컴포넌트를 씬에 배치하고 씬을 옮겨다니다보면 특별한 처리 없이는 단일 객체가 존재해야한다는 속성을 깨트리는 실수를 범할 수 있습니다. Awake에서 instance 여부를 확인할 수 있겠지만, 임의로 클래스를 파괴하거나 초기화 타이밍 이슈로 실행 마다 다르게 동작할 여지를 주며 추가로 명확하게 결정해주는 장치를 구현해야합니다. 로컬 씬에 배치해두고 사용하나 씬 내에서만 싱글톤처럼 단일 객체임을 보장하고 전역적으로 접근하고 싶은 경우를 위해 MonoInstance라는 클래스를 사용하고 있습니다.

 

클래스 이름은 직관적이지 못하나 제한적인 단일 인스턴스이면서 MonoBehaviour를 상속받는 패턴 상 의미가 통한다고 생각했습니다. 카메라 시스템, 네트워크 커넥터, 씬에 상주하는 UI와 HUD 관련 클래스에 활용하고 있습니다.

 

using System;

namespace UnityEngine.Pattern {
  /* Usage
   * public class LocalWorld : MonoInstance<LocalWorld> { }
   */
  [DisallowMultipleComponent]
  public abstract class MonoInstance < T >: MonoBehaviour where T: MonoInstance < T > {
    private static T instance =
    default;

    public static T Instance {
      get {
        if (instance == null) {
          instance = FindObjectOfType < T > ();

          if (instance == null) {
            throw new Exception($"Cannot find instance :: {typeof(T).Name}");
          }

          Debug.Log($"MonoInstance->Find({typeof(T).Name})");
        }

        return instance;
      }
    }

    public static bool IsValid() {
      return instance != null;
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void ReloadDomain() {
      instance?.OnReloadDomain();
      instance = null;

      Debug.Log($"MonoInstance->CleanUp({typeof(T).Name})");
    }

    private void Awake() {
      instance = GetComponent < T > ();
      OnInstanceAwake();
    }

    private void OnDestroy() {
      instance = null;
      OnInstanceDestroy();
    }

    protected virtual void OnInstanceAwake() {

    }

    protected virtual void OnInstanceDestroy() {

    }

    protected virtual void OnReloadDomain() {

    }
  }
}

 

4. 싱글톤 EqualityCompare 클래스

자주 사용되는 복잡한 비교를 위해 EqualityComaprer를 구현하여 사용하는 일이 잦습니다. 매번 클래스를 생성하지 않고 하나의 객체만 생성하여 재사용하기 위해 자식 클래스를 구현했습니다. Input 프로세싱과 클라이언트의 인메모리 DB 관리에서 활용하고 있습니다. 특이한 이유가 없이 매번 생성되고 파괴되는 클래스를 재사용한다는 의미가 있습니다.

 

using System.Collections.Generic;

namespace UnityEngine.Pattern {
  public abstract class EnumComparerBase < T, C >: IEqualityComparer < C > where T: class, new() {
    private static T instance =
      default;

    public static T Instance {
      get {
        if (instance == null) {
          instance = new T();
        }

        return instance;
      }
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void ReloadDomain() {
      instance = null;

      Debug.Log($"EnumComparer cleaned up :: {typeof(T).Name}");
    }

    public abstract bool Equals(C x, C y);

    public abstract int GetHashCode(C obj);
  }
}

 


이 외에도 자주 사용되는 틀으로 템플릿을 구성해두면 편리할 수 있습니다. MonoBehaviour 뿐만 아니라 ScriptableObject를 이용해서도 활용되는 패턴들이 있습니다. SO를 적극적으로 개발에 활용하는 방식은 따로 다룰 예정입니다.