4. 싱글톤 패턴 with Unity

sonohoshi·2021년 1월 1일
1

Singleton Pattern

싱글톤 패턴은 주로 여러 객체를 하나의 클래스에서 관리하는 매니저 클래스라던가, 여러 객체가 이용하는 전역 변수나 리소스 등을 관리할 때 '단 하나' 의 클래스가 생성 및 관리할 수 있도록 할 때 쓰는 디자인 패턴이다. 우리가 쓰는 유니티에서는 주로 게임매니저 같은 클래스를 만들 때 주로 이용한다.

장점

  1. 여러 객체를 간단히 관리할 수 있다.
    이는 각 객체들이 서로를 참조하기 위해 복잡한 참조 관계를 거치지 않고 싱글톤 클래스만 이용해서 서로에게 상호작용을 할 수 있다는 말이 된다.

  2. 이벤트 등을 관리하기 쉽다.
    싱글톤에서는 주로 여러 객체를 관리하기 때문에, 현재 씬에서 작용하는 많은 객체에게 이벤트가 적용되어야 하는 경우 이용할 수 있다. (ex-게임 일시정지 등등)

  3. 특정 기능을 위해 하나의 클래스만 이용한다.
    싱글톤 클래스는 기본적으로 단 하나의 객체만 생성되는 것을 원칙으로 하므로, 여러 개의 똑같은 매니저 객체가 생긴다거나 하는 걱정을 하지 않아도 된다.

단점

  1. 싱글톤 클래스에 너무 많은 기능이 부여되면 싱글톤 클래스에 대한 의존성이 엄청나게 높아지게 된다.
    이는 각 기능별 싱글톤을 분리하거나, 각자 객체에서 해결할 수 있는건 적당히 분배하는 식으로 보완할 수 있다. 그럼에도 불구하고, 싱글톤에 너무 많은 기능을 몰아주진 말도록 하자.

  2. 싱글톤 클래스의 인스턴스는 메모리에서 사라지지 않는다.
    이는 메모리를 비효율 적으로 사용하게 된다는 말이 되므로, 싱글톤의 남발은 저사양 게임을 상당히 무겁게 만들 수 있다.

구현은 어떻게?

using UnityEngine;

[DisallowMultipleComponent]
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    public bool dontDestroyOnLoad;

    private static volatile T _instance;
    private static object _syncRoot = new System.Object();

    public static T instance {
        get {
            Initialize();
            return _instance;
        }
    }

    public static bool isInitialized {
        get {
            return _instance != null;
        }
    }

    public static void Initialize()
    {
        if (_instance != null)
        {
            return;
        }
        lock (_syncRoot)
        {
            _instance = GameObject.FindObjectOfType<T>();

            if(_instance == null)
            {
                var go = new GameObject(typeof(T).FullName);
                _instance = go.AddComponent<T>();
            }
        }
    }

    protected virtual void Awake()
    {   
        if (_instance != null)
        {
            Debug.LogError(GetType().Name + " Singleton class is already created.");
        }

        if (dontDestroyOnLoad){
            DontDestroyOnLoad(this);
        }

        OnAwake();
    }

    protected virtual void OnDestroy()
    {
        if (_instance == this)
        {
            _instance = null;
        }
    }

    protected virtual void OnAwake() { }
}

쉽게, 정말 간단하게 구현할 수 있는 방법은 후술하도록 하겠다. 이번엔 C#의 고오급 문법들을 이용해서 보기 좋게, 깔-쌈하게 만들어봤다.

자, 천천히 설명해보겠다. 먼저 DisallowMultipleComponent 애트리뷰트를 이용해서 하나의 게임오브젝트에 싱글톤 컴포넌트는 '하나만' 붙일 수 있도록 했다. 너무나도 당연하기에 문법적으로도 처리해준 것이다.

기본적으로 MonoBehaviour를 상속받고, 거기에 where문을 이용해 앞으로 본 클래스를 상속하려면 무조건 Class_name : Singleton<Class_name> 의 방식으로 본 클래스를 일반화 시켜 상속받았을 경우에만 구현할 수 있도록 정의해줬다. 잘 모르겠다면, 상속 시 where를 쓰는 구문에 대해 공부해보면 좋을 것 같다.

싱글톤 인스턴스는 꼭 Initialize() 메소드를 이용한 후 반환해주도록 만들었는데, 본 메소드에서 인스턴스 변수가 생성되어있다면 바로 메소드를 종료시키고 인스턴스를 반환하고- 인스턴스가 없다면 생성하는 과정을 거친 후 반환하도록 만들었기 때문이다.

dontDestroyOnLoad 변수는 단순히 public bool 변수로만 지정해두고, 씬이 넘어가더라도 작용을 해야하는가? 를 기준으로 개발자가 직접 지정할 수 있도록 만들었다. true인 경우 DontDestroyOnLoad() 메소드를 통해 해당 싱글톤을 씬이 넘어가더라도 사라지지 않게 해준다.

대충 적당히, 이제 우리는 이 싱글톤 클래스를 부모로 한 후 각 싱글톤 클래스를 만들어서 적당하게 써주면 된다. 아래의 코드는 저번 인디게임위크엔드 2020 에서 만든 게임, EmoGarden 에서 같은 팀원분이셨던 김찬영님이 작성하신 싱글톤 클래스들 중 보기 좋은 것을 가져온 것이다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class SoundManager : Singleton<SoundManager>
{
    [SerializeField]
    private AudioSource _audioSource;

    private Dictionary<string, AudioClip> _audioDictionary;

    protected override void OnAwake()
    {
        base.OnAwake();

        _audioDictionary = new Dictionary<string, AudioClip>();

        AudioClip[] clips = Resources.LoadAll<AudioClip>("Sounds");

        int len = clips.Length;

        for(int i = 0; i < len; i++)
        {
            AudioClip clip = clips[i];
            string clipName = clip.name;

            if (!_audioDictionary.ContainsKey(clipName))
            {
                _audioDictionary.Add(clipName, clip);
            }
        }
    }

    public void PlayEffectSound(string name)
    {
        _audioSource.PlayOneShot(GetClip(name));
    }

    private AudioClip GetClip(string name)
    {
        AudioClip clip = null;

        if (_audioDictionary.ContainsKey(name))
        {
            clip = _audioDictionary[name];
        }

        return clip;
    }
}

이 코드의 내용은 굳이 열심히 읽어볼 필요가 없다.
위 코드에서 궁금한 문법이나 궁금한 부분이 있다면 댓글로 남겨주시면 답변드리겠습니다.

이 코드는 게임 안의 소리를 관리하는 SoundManager를 싱글톤 클래스를 상속받아 만든 것이다. 만약 이 코드를 가지고, 내가 무언가를 먹었을 때 나오는 효과음을 재생시키고 싶다! 라는 생각이 들면, 이렇게 이용하면 되는 것이다.

 SoundManager.instance.PlayEffectSound("Eat_Effect");

이렇게 쓰면 된다. 어느 그 어떤 클래스에서도, 그냥 이렇게만 쓰면 된다.
솔직히 설명이 빈약한거 안다. 근데 그냥 이렇게 간단하게 쓸 수 있다는 걸 보여주기 위해서 적은 것이다. 뭐든지 써봐야 알 수 있을테니까.

다음 코드는, 이렇게 복잡한 틀을 쓰기 위한 나같은 꼬꼬마, 혹은 귀차니스트들을 위한 싱글톤을 만드는 방법이다. 뭐하러 이렇게 복잡한 코드를 썼나 싶을정도로 간단할지도 모른다.

public class GameManager : MonoBehaviour {
    public static GameManager Instance;
    
    private void Awake() {
        if (Instance == null)
            Instance = this;
        // Do Something...
    }
    
    // More Do Something...
}

그냥 이렇게 만들어도 된다. 솔직히 필자도 이렇게 많이 썼었는데, 위 코드를 접하고 나니까 너무 깔끔하고 각 매니저들을 만들 때 정형화된 틀을 쓸 수 있어서 앞으론 이렇게 쓰면 좋을 것 같다는 생각이 들어서 소개하게 됐고, 간단히 개발을 원하는 사람은 그냥 이런식으로만 만들어도 무방하다!

마치며

대충, 그리고 허접하게 디자인 패턴 중 가장 많이 개발자들에게 알려져 있고 친숙할지도 모르는 싱글톤 패턴에 대해 몇 자 적어보았다. 강력하고 간편한 패턴임에는 분명하지만, 그만큼 빛과 어둠이 명확한 방법이기도 하니 쓰는 분들은 조심히, 안전하게 쓰시길 바란다.

코드가 대부분인데다 읽기 싫으셨겠지만 여기까지 읽어주셔서 감사합니다.
다음에는 더 나은 퀄리티의 글로 만나뵙겠습니다.

고등학생 게임 개발자 김선민이었습니다.

profile
22년 기준 글을 작성하지 않고 있습니다. 해당 블로그의 글은 학생 시절 공부하다 적은 내용이며 잘못된 정보가 있을 수 있음을 알려드립니다.

0개의 댓글