싱글톤

2019. 7. 27. 22:33유니티

싱글톤은 클래스에 대해 하나의 객체만 사용하고 이를 전역으로 접근하는 디자인 패턴입니다. 작업을 하면서 많이 사용하고 별도의 설계없이 개발을 편리하게 해줍니다. 그렇다고 무작정 사용하면 게임에서 렉을 발생하는 주요 요인이 될 수도 있는데 이 부분을 언급하고자 합니다.

 

문제의 소스 코드

using UnityEngine;
 
public class ExampleSingleton : MonoBehaviour
{
    public static ExampleSingleton Instance
    {
        get
        {
            if (sInstance == null)
            {
                sInstance = GameObject.FindObjectOfType<ExampleSingleton>();
            }
 
            return sInstance;
        }
    }
 
    private static ExampleSingleton sInstance;
}

초기 작업을 하면서 큰 문제를 느낄 수 없었지만 후에 Hierarchy에 객체의 수가 늘어날 수록 느려지는 현상이 있었습니다. 유니티의 프로파일링을 해보니 FindObjectOfType가 문제였습니다.

해결책

Awake를 사용하여 Instance를 미리 초기화를 합니다.

확실히 이렇게 하니 속도는 상당히 개선되었습니다.(3초이상 걸리던 활성화가 즉시 응답을 하더군요.)

using UnityEngine;
 
public class ExampleSingleton : MonoBehaviour
{
    private void Awake()
    {
        sInstance = this;
    }
 
    public static ExampleSingleton Instance
    {
        get
        {
            return sInstance;
        }
    }
 
    private static ExampleSingleton sInstance;
}

최적화는 성공했지만 다음과 같은 수정 사항이 있었습니다.

수정 사항

Instance을 일일이 선언을 해줘야하는데 실수로 선언하지 않았을 경우 null참조 에러가 발생합니다.

싱글톤 객체를 만들 때마다 Instance를 구현해야되는 불편함을 해결해야합니다.

싱글톤의 추상 클래스

최적화도 하고 구조를 개선할 방법을 찾다가 싱클톤을 추상화하는 방법을 생각하게 되었습니다.

 

public abstract class Singleton<T> : MonoBehaviour
{
    public static T Instance
    {
        get
        {
            return GameObject.FindObjectOfType<T>();
        }
    }
 
    private static T _Instance;
}

하지만 위에서 언급했지만 FindObjectOfType는 느립니다. 그렇다고 Awak를 통해서 일일이 초기화를 하고 싶지 않습니다. 검색 능력이 부족해서 그런지 별 다른 해결책이 보이지가 않다가 우연히 DnSpy 툴을 사용하면서 타 사의 앱에서 싱글톤을 만드는 부분을 살펴봤는데 생성자를 통하여 초기화가 하는 방법이 있었습니다.

using UnityEngine;
 
public abstract class Singleton<T> : MonoBehaviour where T : class
{
    public Singleton()
    {
        if (_Instance == null)
        {
            _Instance = this as T;
        }
    }
 
    public static T Instance
    {
        get
        {
            return _Instance;
        }
    }
 
    private static T _Instance;
}

위와 같이 구현하면 FindObjectOfType를 사용할 필요도 없어지며 Awake를 통해서 일일이 초기화를 할 필요가 없어지며 싱글톤 객체마다 Instance를 일일이 구현할 필요도 없어집니다.

이와 같은 로직이 가능한 이유는 유니티의 생명 주기에서 Awake보다 먼저 호출되는 Reset에서 생성자를 호출하기 때문입니다. 생성자는 여러 번 호출될 수도 있으므로 가급적이면 시간이 오래 걸리는 작업을 하지 말아야 되며 반복적인 작업을 하지 말아야할 경우 null 체크를 통해서 한 번만 초기화하도록 주의하면서 작업을 해야합니다.

마지막으로 제너릭 타입인 T에 대한 제약 조건을 설정해줘야합니다. this를 형 변환할 때 구조체일 수도 있고 클래스일 수도 있는 애매한 상황이 발생하기 떄문에 명확하게 처리를 해달라는 에러를 출력하기 때문입니다.

전체 소스코드

Singleton.cs

using UnityEngine;
 
public abstract class Singleton<T> : MonoBehaviour where T : class
{
    public Singleton()
    {
        if (_Instance == null)
        {
            _Instance = this as T;
        }
    }
 
    public static T Instance
    {
        get
        {
            return _Instance;
        }
    }
 
    private static T _Instance;
}

ExampleSingleton.cs

using UnityEngine;
 
public class ExampleSingleton : Singleton<ExampleSingleton>
{
    public void DoExample()
    {
        Debug.Log("DoExample");
    }
}

Test.cs

using UnityEngine;
 
public class Test : MonoBehaviour
{
    public Test()
    {
        Debug.Log("Test");
    }
 
    private void Awake()
    {
        Debug.Log("Awake");
 
        ExampleSingleton.Instance.DoExample();
    }
}

유니티 인스펙터 창 및 로그 창

 

'유니티' 카테고리의 다른 글

Immediate Mode GUI(IMGUI)  (0) 2019.08.20
Bounds 살펴보기  (0) 2019.08.17
카메라 흔들기(Camera Shake)  (0) 2019.07.27