Priv's Blog

6. Singleton pattern 본문

Unity Learn/Game Programming Patterns

6. Singleton pattern

Priv 2023. 10. 31. 20:05


 


 

Level up your code with game programming patterns

A guide written by programmers, for programmers, that demonstrates how to leverage design best practices

resources.unity.com


 

1. 싱글톤 패턴

싱글톤 패턴은 평가가 그렇게 좋지만은 않습니다. 만약 여러분이 초보 Unity 개발자라면, 싱글톤 패턴은 여러분이 개발 현장에서 처음 마주하게 디자인 패턴 하나일 것입니다. 또한 디자인 패턴 중에서 가장 많이 비방을 받는 패턴이기도 하죠.

본래의 GOF(Gang of Four) 따르면 싱글톤 패턴은:

- 하나의 클래스는 오직 자기 자신의 인스턴스 하나만 인스턴스화할 있습니다.

- 개의 인스턴스를 통해 편리한 전역 접근을 제공합니다.

패턴은 전체 씬을 통틀어 작업을 조정하는 오브젝트가 하나만 필요할 경우에 유용합니다. 예를 들어, 메인 게임 루프를 제어하는 게임 매니저는 씬에서 정확히 하나만 필요할 있습니다. 또한 제시간에 파일 시스템에 접근해 데이터를 기록하는 파일 매니저도 하나만 필요할 있죠. 이처럼 중앙 관리자 역할을 하는 오브젝트들은 싱글톤 패턴을 적용하기에 적합한 후보가 되는 경향이 있습니다.

게임 프로그래밍 패턴에서 싱글톤 패턴은 장점보다 단점이 많으며, 피해야 디자인 패턴 중에 하나로 취급합니다. 이렇게 나쁜 평판은 싱글톤 패턴의 사용 편의성 때문에 남용될 우려가 높기 때문입니다. 개발자분들은 싱글톤 패턴을 부적절한 상황에 적용하여 불필요한 전역 상태 또는 종속성을 도입하려는 경향이 있습니다.

, 이제 Unity 엔진에서 싱글톤 패턴을 직접 구현해 보고 패턴의 장단점을 직접 평가해 봅시다. 그러면 여러분의 애플리케이션에 패턴을 적용할 값어치가 있는지 직접 판단해 있을 것입니다.

 


 

2. 예제: 간단한 싱글톤 패턴 실습

가장 간단한 싱글톤은 아래와 같이 생겼을 것입니다:

using UnityEngine;

public class SimpleSingleton : MonoBehaviour
{
    public static SimpleSingleton Instance;
    
    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

public static Instance 안에 있는 Singleton 인스턴스 하나를 참조하게 것입니다.

Awake 메서드에서 if 조건문을 사용해 Instance 참조하는 값이 이미 있는지 검사합니다. 만약 instance 현재 null 경우, Instance this라는 특정 오브젝트를 할당받습니다. 이는 씬에서 번째 싱글톤이어야 합니다.

Destroy(gameObject) 호출하여 여러분의 싱글톤이 씬에서 하나뿐인 컴포넌트가 되도록 보장해 주지 않으면, 인스턴스가 복제될 있습니다.

만약 여러분이 게임이 실행되는 동안 Hierarchy 창에서 하나 이상의 게임 오브젝트에 SimpleSingleton 스크립트를 부착할 경우, Awake 메서드에 작성한 if 로직을 통해 오브젝트를 하나만 남겨두고 나머지는 모두 지워버릴 것입니다.

Instance 필드는 public이며 static입니다. 어떠한 컴포넌트도 어디에서나 하나뿐인 싱글톤에 대해 전역 접근이 가능하다는 것입니다.

 


 

3. 지속성과 게으른 인스턴스화

SimpleSingleton 작성된 대로 동작합니다. 하지만, 이는 아래와 같은 가지 문제를 지니고 있습니다:

- 새로운 씬을 로딩할 게임 오브젝트가 파괴됩니다.

- 실제로 사용하기 전에 Hierarchy 상에서 싱글톤을 설정해 주어야 합니다.

싱글톤은 어디에나 존재하는 매니저 스크립트 역할을 수행하는 경우가 많기 때문에, DontDestroyOnLoad 사용하여 오브젝트가 파괴되지 않고 계속 유지되도록 만들면 이점을 얻을 있습니다.

나아가, 여러분은 게으른 인스턴스화를 사용하여 싱글톤이 처음 필요한 순간에 자동으로 생성되게 만들 수도 있습니다. 게임 오브젝트를 생성하고 적절한 싱글톤 컴포넌트를 추가하는 간단한 로직만 있으면 됩니다.

이를 바탕으로 개선된 싱글톤 코드는 다음과 같습니다:

public class Singleton : MonoBehaviour
{
    private static Singleton instance;
    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                SetupInstance();
            }
            return instance;
        }
    }

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(this.gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private static void SetupInstance()
    {
        instance = FindObjectOfType<Singleton>();

        if (instance == null)
        {
            GameObject gameObj = new GameObject();
            gameObj.name = "Singleton";
            instance = gameObj.AddComponent<Singleton>();
            DontDestroyOnLoad(gameObj);
        }
    }

이제 Instance private instance 참조하는 public 프로퍼티로 선언된 지원 필드(backing field)입니다. 싱글톤을 처음 참조할 , Instance 이미 존재하는지 먼저 확인합니다. 만약 없다면 SetupInstance 메서드가 적절한 컴포넌트를 지니고 있는 게임 오브젝트를 생성하게 됩니다.

DonDestroyOnLoad(gameObject) 씬을 로드할 Hierarchy 상에 있는 싱글톤 인스턴스가 사라지지 않도록 해줍니다. 싱글톤 인스턴스는 이제 지속성을 지니고 있으므로, 게임상에서 씬을 변경하더라도 계속 활성화된 상태를 유지할 것입니다.

 


 

4. 제네릭 사용하기

스크립트의 어느 버전도 동일한 내에서 다른 싱글톤을 구현하는 방법을 다루지 않습니다. 예를 들어, 만약 여러분이 AudioManager에서 동작하는 싱글톤과 GamaManager에서 동작하는 다른 싱글톤을 원한다고 가정했을 , 현재로써는 둘이 서로 공존할 없습니다. 그러므로 관련이 있는 코드를 복제하고 클래스에 복사한 로직을 붙여 넣어야 합니다.

대신, 아래와 같이 제네릭을 사용한 버전의 싱글톤을 만들어 봅시다:

public class Singleton<T> : MonoBehaviour where T : Component
{
    private static T instance;
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = (T)FindObjectOfType(typeof(T));
                if (instance == null)
                {
                    SetupInstance();
                }
            }
            return instance;
        }
    }
    public virtual void Awake()
    {
        RemoveDuplicates();
    }

    private static void SetupInstance()
    {
        instance = (T)FindObjectOfType(typeof(T));
        
        if (instance == null)
        {
            GameObject gameObj = new GameObject();
            gameObj.name = typeof(T).Name;
            instance = gameObj.AddComponent<T>();
            DontDestroyOnLoad(gameObj);
        }
    }

    private void RemoveDuplicates()
    {
        if (instance == null)
        {
            instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

코드는 어떠한 클래스도 싱글톤으로 변경할 있게 해줍니다. 여러분이 클래스를 하나 선언할 , 단순히 위에서 만든 제네릭 타입의 싱글톤을 상속하기만 하면 됩니다. 예를 들어, GameManager라는 이름의 클래스에 싱글톤을 적용하고 싶다면, 아래와 같이 선언하면 됩니다:

public class GameManager : Singleton<GameManager>
{
    // ...
}

이제 언제 어디서나 여러분이 원할 public과 static으로 선언된 GameManager.Instance 참조할 있습니다.

 


 

5. 싱글톤 패턴의 장단점

싱글톤은 책에서 소개되는 다른 패턴들과 다르게 여러 측면에서 SOLID 원칙들을 위반하는 부분이 있습니다. 많은 개발자가 아래와 같은 다양한 이유로 인해 싱글톤 패턴을 기피합니다:

- 싱글톤은 전역 접근 권한을 요구합니다: 전역 접근이 가능한 인스턴스를 사용하는 때문에 수많은 의존성을 숨길 있습니다. 이는 수정하기 까다로운 버그를 야기할 있죠.

- 싱글톤은 태스트하기가 까다롭습니다: 유닛 테스트는 무조건 독립적인 환경에서 이루어져야 합니다. 싱글톤을 사용하면 씬에서 수많은 게임 오브젝트의 상태를 변경할 있게 되기 때문에, 여러분의 테스트를 방해할 있습니다.

- 싱글톤은 강한 결합을 유도합니다: 책에서 소개되는 대부분의 패턴은 느슨한 종속성을 유지하도록 노력하지만, 싱글톤은 정반대입니다. 강한 결합은 코드 리팩토링을 까다롭게 만듭니다. 만약 컴포넌트 하나를 변경한다면, 해당 컴포넌트와 연결된 모든 컴포넌트가 영향을 받게 됩니다. 이는 지저분한 코드가 탄생하는 원인이 되죠.

싱글톤을 사용하는 것을 반대하는 목소리는 상당히 많습니다. 만약 여러분이 장기간 유지보수가 필요한 엔터프라이즈 수준의 게임을 제작하신다면, 싱글톤 패턴은 피하는 것이 좋습니다.

하지만 모든 게임들이 엔터프라이즈 수준인 것은 아닙니다. 다른 비즈니스 소프트웨어처럼 지속적으로 기능을 확장할 필요는 없습니다.

실제로 싱글톤은 확장성이 필요 없는 소규모 게임을 개발할 가지 매력적일 있는 이점을 제공해 주기도 합니다.

- 싱글톤은 상대적으로 학습하기 용이합니다: 싱글톤의 핵심 패턴 자체가 매우 간단합니다.

- 싱글톤은 사용자 친화적입니다: 싱글톤을 다른 컴포넌트에 사용하고 싶다면, 단순히 public static으로 선언된 인스턴스를 참조하기만 하면 됩니다. 싱글톤 인스턴스는 내에 존재하는 모든 오브젝트로부터 필요할 언제든지 접근해 사용할 있습니다.

- 싱글톤은 성능이 좋습니다: static으로 선언된 싱글톤 인스턴스를 전역 접근 권한을 통해 접근하여 사용하기 때문에, GetComponent 또는 Find 명령어를 사용할 발생하는 캐싱으로 인한 성능 저하 문제를 피할 있습니다.

이를 활용하여 내에 존재하는 모든 게임 오브젝트가 언제나 접근할 있는 매니저 오브젝트(게임 플로우 매니저 또는 오디오 매니저 ) 제작할 있습니다. 또한, 만약 오브젝트 풀을 구현했다면, 여러분이 구현한 오브젝트 풀링 시스템 코드에 싱글톤 패턴을 적용하여 보다 쉽게 오브젝트를 풀링할 있도록 만들 수도 있습니다.

만약 여러분의 프로젝트 내에서 싱글톤 패턴을 사용하기로 했다면, 최대한 프로젝트의 규모를 작게 유지하세요. 절대 싱글톤을 남용하지 마세요. 싱글톤 패턴은 전역 접근을 통해 이점을 얻을 있는 소수의 스크립트 상에서만 사용하세요.

 


 


수고하셨습니다!


'Unity Learn > Game Programming Patterns' 카테고리의 다른 글

8. State pattern  (0) 2023.11.20
7. Command pattern  (0) 2023.11.12
5. Object pool  (0) 2023.10.23
4. Factory pattern  (0) 2023.10.11
3. Design patterns for game development  (0) 2023.10.05
Comments