유니티 싱글톤 패턴 [유니티 C#]

싱글톤 패턴을 구현하는 3가지 방법

게임을 개발하며 정말 많이 사용되는 디자인 패턴 중 하나가 싱글톤 패턴(Singleton pattern)입니다.

오늘은 싱글톤 패턴이 무엇인지, 왜 필요한 지 그리고 싱글톤 패턴을 구현하는 3가지 방법에 대해 알아보겠습니다.

  • Monobehaviour를 상속 받는 싱글톤 패턴
  • Monobehaviour를 상속 받지 않는 싱글톤 패턴
  • 제네릭 클래스 싱글톤

1. 싱글톤 패턴?

싱글톤 패턴의 간략한 설명은 이렇습니다.

‘하나의 클래스 인스턴스만 생성되도록 보장하며, 전역 접근이 가능하도록 디자인 된 패턴’

다시 말해 게임 내 하나만 존재해야 되는 클래스 (GameManager 등)가 씬의 이동이나
클래스의 추가 등으로 중복으로 존재하는 상황을 방지하며

해당 클래스를 어디서든 접근이 가능하도록 ‘접근성’을 부여하는 디자인 패턴 입니다.

위 특성 때문에 싱글톤 패턴을 ‘유일성, 접근성, 지속성을 띈다’라고 표현하기도 합니다.


2. 왜 싱글톤 패턴을 사용할까?

C#
using UnityEngine;

public class TimeDisplay : MonoBehaviour
{
    [SerializeField] private GameManager _gameManager;

    private void Update()
    {
        Debug.Log($"현재 시간은 {_gameManager.currentTime} 입니다.");
    }
}
C#
using UnityEngine;

public class TimeDisplay : MonoBehaviour
{
    GameManager _gameManager;

    private void Start()
    {
        _gameManager = FindAnyObjectByType<GameManager>();
    }

    private void Update()
    {
        Debug.Log($"현재 시간은 {_gameManager.currentTime} 입니다.");
    }
}

두 코드는 모두 같은 기능을 하는 코드입니다.
GameManager currentTime을 출력하는 코드죠.

두 코드의 차이는 유니티 에디터 인스펙터에 GameManager를 넣어서 사용을 하느냐 아니면,
Start 함수에서 GameManager를 찾아 초기화 하여 사용을 하느냐의 차이입니다.

그런데, TimeDisplay.cs가 더 많은 클래스에서 값을 받아와야 된다면 어떨까요?
또는 게임 중 씬이 이동하는 상황이 되면 어떨까요?

위 상황을 쉽고 효율적으로 해결하는 방법이 ‘싱글톤 패턴’을 사용하는 방법입니다.


3. Monobehaviour을 상속 받는 싱글톤

C#
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public float currentTime;

    public static GameManager instance;

    private void Awake()
    {
        // 인스턴스 할당
        if (instance == null)
        {
            // 인스턴스 초기화
            instance = this;

            // 씬 전환 시에도 파괴되지 않도록 설정
            DontDestroyOnLoad(gameObject);
        }
        // 인스턴스가 이미 존재하는 경우 제거
        else
        {
            Destroy(gameObject);
        }
    }
}

위 코드는 Monobehaviour를 상속받아 사용하는 일반적인 싱글톤 패턴입니다.


주요 코드

static

static 변수는 프로그램이 실행 되는 동안 메모리에 고정되어 사용됩니다.
또한, 접근 제어자(public, private)에 따라 선언한 static 변수가 사용 될 수 있는 범위가 달라집니다.

public static GameManager instance

인스턴스를 전역에서 접근할 수 있도록 해줍니다.

instance = this

인스턴스를 GameManager 클래스로 초기화 합니다.

DontDestroyOnLoad(gameObject)

씬 전환 시에도 클래스가 제거되지 않도록 합니다.

Destroy(gameObject)

인스턴스가 이미 존재할 시 자신을 제거합니다. (중복 방지)


그럼, 싱글톤을 사용한 클래스를 어떻게 활용할 수 있을까요?

C#
using UnityEngine;

public class TimeDisplay : MonoBehaviour
{
    private void Update()
    {
        Debug.Log($"현재 시간은 {GameManager.instance.currentTime} 입니다.");
    }
}

이제 TimeDisplayGameManager를 초기화하거나 에디터에서 드래그 앤 드랍 할 필요 없이
GameManagerinstance를 통해 전역 변수, 전역 함수 등에 접근할 수 있습니다!

물론, instance를 많이 받아와야 되는 상황에서는 Start, Awake 함수에서 초기화를 하여 사용하면 가독성 및 효율성 측면에서 이점이 있을 수 있습니다.

초기화를 하는 상황에서도 instace를 통해 초기화를 할 수 있다는 게 큰 싱글톤의 큰 이점입니다.

Monobehaviour를 상속받는 싱글톤은 특성 상 유니티의 기능들을 활용해야 되는 매니저 (GameManager, AudioManager 등)에 주로 사용 됩니다.


4. Monobehaviour을 상속 받지 않는 싱글톤

C#
public class DataManager
{
    public string playerName = "Mr.Kim";

    private static DataManager _instance;

    // 외부에서 접근 가능한 프로퍼티
    public static DataManager Instance
    {
        get
        {
            // 인스턴스가 없으면 새로 생성
            if (_instance == null)
            {
                _instance = new DataManager();
            }

            // 인스턴스 반환
            return _instance;
        }
    }
}

게임을 개발하다 보면 Monobehaviour를 상속받지 않아도 되는 스크립트가 존재합니다.
위 코드는 Monobehaviour가 필요 없는 즉, 유니티의 기능을 이용하지 않는
단순 데이터 관리를 위한 스크립트의 싱글톤 패턴입니다.


주요 코드

private static DataManager _instance

이번에는 프로퍼티를 통해 외부에서 접근을 하는 방식이기에 private로 선언합니다.

public static DataManager Instance

외부에서 접근하는 프로퍼티입니다.

get{}

get은 외부에서 선언한 프로퍼티를 가져올 때 (접근 할 때) 정의되는 키워드입니다.
해당 프로퍼티를 가져올 때 중괄호{} 내의 코드블럭을 실행합니다.

_instance = new DataManager()

인스턴스를 초기화 합니다. 초기화 시 프로퍼티를 초기화 하는 게 아닌 ‘_instance’를 초기화 해야합니다.

return _instance

get이 반환하는 값입니다. _instance를 반환합니다.


Monobehaviour를 상속 받지 않는 싱글톤 또한 똑같은 방법으로 사용할 수 있습니다.

C#
using UnityEngine;

public class NameDisplay : MonoBehaviour
{
    private void Update()
    {
        Debug.Log($"플레이어의 이름은 {DataManager.Instance.playerName} 입니다.");
    }
}

Monobehaviour를 상속 받지 않는 싱글톤 패턴은 GameObject가 필요 없는 순수 데이터 관리자에 적합합니다.

또한, Monobehaviour를 상속 받지 않기에 가볍습니다.

아마 ‘씬이 이동해도 내가 선언한 싱글톤이 남아있나?’라는 궁금증이 생기실 수도 있습니다.
결론부터 말씀드리면,

Monobehaviour를 상속 받지 않는 싱글톤’은 씬이 이동해도 접근이 가능합니다!

static은 메모리에 고정되기 때문에 프로그램이 종료될 때까지 메모리를 점유하기 때문입니다.

단, Monobehaviour를 상속 받는 경우에는 씬이 이동하거나, 오브젝트가 파괴 될 경우 인스턴스에 접근할 수 없습니다. 그래서 DontDestroyOnLoad()를 사용해 이를 방지하는 겁니다.


5. 제네릭 클래스 싱글톤

그렇다면, 싱글톤 패턴이 필요한 스크립트들이 많아지면 어떡할까요?

사실, 일일이 Awake함수에 싱글톤 패턴을 집어넣고, 선언하는 일은 귀찮은 작업입니다.

그런 귀찮음을 완화하기 위한 세 번째 방법이 ‘제네릭 클래스 싱글톤’을 사용하는 것 입니다!


C#
using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;

    // 인스턴스 접근용 프로퍼티
    public static T Instance
    {
        get
        {
            // 씬에 이미 존재하는지 확인
            if (_instance == null)
            {
                // 해당 타입의 오브젝트를 찾음
                _instance = (T)FindAnyObjectByType(typeof(T));

                // 씬에 없으면 새로 생성 (혹시 모를 경우를 대비)
                if (_instance == null)
                {
                    GameObject obj = new GameObject(typeof(T).Name);
                    _instance = obj.AddComponent<T>();
                }
            }

            // 인스턴스 반환
            return _instance;
        }
    }

    // 싱글톤 패턴
    protected virtual void Awake()
    {
        if (_instance == null)
        {
            _instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

기존 싱글톤 패턴보다 코드가 조금 더 많아졌습니다만, 제네릭 싱글톤은 한 번만 만들어 놓으면
Monobehaviour를 상속 받는 모든 싱글톤 패턴에 사용이 가능합니다!

주요 코드


public class Singleton<T>

일반적인 제네릭의 문법입니다. 꺽쇠<> 안에 원하는 데이터 타입을 넣어 타입을 지정할 수 있도록 합니다.
주로 Type의 앞글자를 따 ‘T’를 매개변수의 이름으로 사용합니다.
쓰임새가 많은 문법이기에 알아두시면 활용도가 굉장히 높은 문법입니다!

MonoBehaviour where T : MonoBehaviour

: 뒤에 바로 붙어있는 ‘MonoBehaviour’Singleton 클래스가 상속받는 MonoBehaviour입니다.
where 제네릭의 제약조건인데, 위 코드의 경우 ‘TMonoBehaviour일 때만 사용할 수 있다’는 의미입니다.

public static T Instance

프로퍼티를 통해 외부에서 데이터를 접근 하도록 합니다.

_instance = (T)FindAnyObjectByType(typeof(T))

_instance로 할당 될 T를 찾습니다.
FindAnyObjectByType(typeof(T))는 씬에 존재하는 오브젝트중 T를 가진 오브젝트를 반환합니다.

*FindAnyObjectByType 문법은 Unity 2023 버전 이상에서 제공되는 문법입니다.
이전 버전의 사용자 분들은 FindObjectOfType 문법을 사용하시면 됩니다.*
(T)

FindAnyObjectByType 전의 (T)의 의미는 FindAnyObjectByType로 찾은 오브젝트를 T타입으로 변환하라는 의미입니다. 전문적인 용어로 이를 캐스팅(Casting)이라고 합니다.

GameObject obj = new GameObject(typeof(T).Name)

만약 제네릭 싱글톤을 상속한 클래스가 오브젝트가 없을 경우 이를 생성해주는 방어 코드입니다.
오브젝트를 생성하고 오브젝트의 이름을 ‘typeof(T).Name’으로 설정합니다.

protected

protected 키워드는 클래스와 그 클래스를 상속받은 자식 클래스에서만 접근 가능하도록 접근 제한자입니다.
위 코드에서 Awake 함수는 Singleton 클래스를 상속받은 클래스에서도 사용이 가능해야 하기에 protected 키워드를 사용했습니다.

virtual

virtual 키워드는 클래스를 상속받은 클래스가 override 키워드를 통해 ‘재정의’할 수 있도록 하는 키워드입니다.
여기서 말하는‘재정의’함수를 상속받은 자식 클래스가 사용 시 부모 클래스 함수의 기능 외에 추가적인 로직을 달 수 있도록 한다는 의미입니다.


제네릭 싱글톤을 만들었으니 이제 활용할 차례입니다.

C#
using UnityEngine;

public class GameManager : Singleton<GameManager>
{
    public float currentTime;
}

기존 GameManager에서 상속받던 MonoBehaviour대신 Singleton을 상속받는 구조로 변경되었습니다.

SingletonMonoBehaviour를 상속받기 때문에 Singleton을 상속 받는 클래스 또한 MonoBehaviour의 기능을 이용할 수 있습니다.

Singleton 클래스는 제네릭을 통해 특정 타입을 가져야 되므로 GameManager를 타입으로 설정합니다.

이렇듯 제네릭 싱글톤 패턴을 만들어 놓으면 싱글톤 패턴이 필요한 모든 클래스에 Singleton을 상속하여 싱글톤으로 사용할 수 있습니다.

6. 싱글톤 패턴의 단점?

‘어디서나 접근할 수 있는 유일한 클래스’를 만드는 싱글톤 패턴은 정말 매력적인 디자인 패턴입니다.

다만, 싱글톤을 더욱 안정적이고 효과적으로 사용하기 위해서는 싱글톤의 단점 또한 파악 해야 될 필요가 있습니다.

I. 높은 결합도

프로젝트가 커질수록 여러 클래스가 싱글톤 인스턴스를 참조하면서 클래스 간의 결합도가 높아질 수 있습니다.
‘결합도가 높다’는 것은 ‘의존도가 높다’라는 의미이기도 합니다.
이는 곧 확장성 저하(스파게티 코드의 가능성)과 성능의 저하로 이어질 수도 있습니다.

II. 데이터 추적 난이도 상승

싱글톤 패턴이 가진 높은 접근성은 그 자체로 단점이 될 수 있습니다.
여러 클래스에서 싱글톤 인스턴스를 통해 값을 변경 시킨다면, 언제 어디에서
값을 변경 했는지에 대한 추적이 어렵습니다.

III. 클래스의 비대화

싱글톤 패턴 자체의 문제는 아니지만, 싱글톤 패턴을 사용하는 클래스 자체가 비대해질 시
싱글톤이 무거운 클래스의 인스턴스를 생성할 때 성능상의 문제가 발생할 수 있습니다.
클래스의 비대화를 막는 방법은 추후에 포스팅 해보도록 하겠습니다!


7. 비교

MonoBehaviour 상속Unity의 기능이 필요한 매니저급 스크립트 /
싱글톤 패턴 사용이 적은 프로젝트
MonoBehaviour 상속 X단순 데이터 저장용, 계산, 유틸리티 클래스
제네릭 클래스싱글톤 패턴 사용이 잦은 경우 /
대규모 프로젝트

8. 마치며

싱글톤 패턴이 무엇이고, 왜 필요한 것인지 그리고 이를 구현하는 몇 가지 방법에 대해 살펴보았습니다.

사실 싱글톤 패턴이 단점이 있다고 하더라도 개발자들이 이 패턴을 애용하는 이유는 싱글톤 패턴이 가진 장점이 단점을 상쇄하기 때문일 겁니다.

이 글을 보시는 분들도 여러분의 프로젝트에 싱글톤 패턴을 적재적소에 활용하셔서
프로젝트를 완성 시켜나가셨으면 좋겠습니다!

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다