싱글톤 패턴 (Singleton Pattern)

Unity에서 싱글톤 패턴을 사용하여 전역 접근 가능한 단일 인스턴스를 만드는 방법을 학습합니다.

싱글톤 패턴이란?

싱글톤 패턴은 클래스(class)의 인스턴스(instance)가 오직 하나만 존재하도록 보장하는 디자인 패턴입니다. 게임 매니저, 사운드 매니저 등 전역에서 접근해야 하는 오브젝트에 사용합니다.

기본 개념

  • 단일 인스턴스(instance): 클래스(class)의 인스턴스(instance)가 하나만 존재
  • 전역 접근: 어디서든 Instance로 접근 가능
  • 자동 생성: 인스턴스(instance)가 없으면 자동으로 생성
  • 씬 전환 유지: DontDestroyOnLoad()로 씬 전환 시에도 유지

1. 기본 싱글톤 구조

기본 싱글톤 구현

using UnityEngine;

public class GameManager : MonoBehaviour
{
    // 싱글톤 인스턴스 (정적 프로퍼티)
    public static GameManager Instance { get; private set; }

    void Awake()
    {
        // 싱글톤 초기화
        InitializeSingleton();
    }

    private void InitializeSingleton()
    {
        if (Instance == null)
        {
            // 첫 번째 인스턴스이면 설정
            Instance = this;
            
            // 씬이 변경되어도 파괴되지 않도록 설정
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            // 이미 인스턴스가 있으면 중복 방지를 위해 파괴
            Destroy(gameObject);
        }
    }
}

사용 예제

using UnityEngine;

public class FallingFireball : MonoBehaviour
{
    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player"))
        {
            // GameManager 인스턴스에 접근
            if (GameManager.Instance != null)
            {
                GameManager.Instance.GameOver();
            }
        }
    }
}

2. DontDestroyOnLoad() - 씬 전환 유지

DontDestroyOnLoad()는 씬이 변경되어도 오브젝트가 파괴되지 않도록 합니다.

사용법

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            
            // 씬이 변경되어도 파괴되지 않도록 설정
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

주의사항

  • 중복 방지: 같은 씬에 여러 개가 있으면 하나만 남기고 나머지는 파괴
  • 씬 전환 시 유지: 게임 매니저, 사운드 매니저 등에 사용
  • 메모리(memory) 관리: 필요 없을 때는 수동으로 파괴해야 함

3. 실제 사용 예제

예제 1: GameManager 싱글톤

using UnityEngine;

public class GameManager : MonoBehaviour
{
    #region Singleton
    public static GameManager Instance { get; private set; }
    #endregion

    #region Events
    public System.Action OnGameOverEvent;
    public System.Action OnGameRestartEvent;
    #endregion

    #region Fields
    [Header("게임 상태")]
    [SerializeField] private bool isGameOver = false;
    #endregion

    #region Properties
    public bool IsGameOver => isGameOver;
    #endregion

    #region Unity Lifecycle
    void Awake()
    {
        InitializeSingleton();
    }
    #endregion

    #region Initialization
    private void InitializeSingleton()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
    #endregion

    #region Game State Management
    public void GameOver()
    {
        // 이미 게임 오버면 중복 실행 방지
        if (isGameOver)
        {
            return;
        }

        isGameOver = true;
        Debug.Log("Game Over!");

        // 게임 오버 처리
        HandleGameOver();
    }

    private void HandleGameOver()
    {
        // 시간 정지
        Time.timeScale = 0f;
        
        // 게임 오버 이벤트 발생
        OnGameOverEvent?.Invoke();
    }

    public void RestartGame()
    {
        isGameOver = false;
        Time.timeScale = 1f;

        OnGameRestartEvent?.Invoke();
        ReloadScene();
    }

    private void ReloadScene()
    {
        UnityEngine.SceneManagement.SceneManager.LoadScene(
            UnityEngine.SceneManagement.SceneManager.GetActiveScene().name
        );
    }
    #endregion
}

예제 2: 다른 스크립트에서 사용

using UnityEngine;

public class FallingFireball : MonoBehaviour
{
    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player"))
        {
            // 싱글톤 인스턴스에 접근
            if (GameManager.Instance != null)
            {
                GameManager.Instance.GameOver();
            }
        }
    }
}
using UnityEngine;

public class SheepController : MonoBehaviour
{
    void Update()
    {
        // 게임 오버 상태면 입력 무시
        if (GameManager.Instance != null && GameManager.Instance.IsGameOver)
        {
            return;
        }

        // 입력 처리...
    }
}

4. 프로퍼티를 사용한 접근

읽기 전용 프로퍼티

using UnityEngine;

public class GameManager : MonoBehaviour
{
    private static GameManager instance;
    
    // 읽기 전용 프로퍼티
    public static GameManager Instance
    {
        get
        {
            // 인스턴스가 없으면 자동으로 찾기
            if (instance == null)
            {
                instance = FindObjectOfType<GameManager>();
            }
            return instance;
        }
        private set
        {
            instance = value;
        }
    }

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else if (Instance != this)
        {
            Destroy(gameObject);
        }
    }
}

5. 자동 생성 싱글톤

인스턴스(instance)가 없으면 자동으로 생성하는 싱글톤입니다.

구현

using UnityEngine;

public class GameManager : MonoBehaviour
{
    private static GameManager instance;

    public static GameManager Instance
    {
        get
        {
            if (instance == null)
            {
                // 씬에서 찾기
                instance = FindObjectOfType<GameManager>();
                
                // 없으면 새로 생성
                if (instance == null)
                {
                    GameObject go = new GameObject("GameManager");
                    instance = go.AddComponent<GameManager>();
                    DontDestroyOnLoad(go);
                }
            }
            return instance;
        }
    }

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

6. 주의사항

1. 중복 인스턴스(instance) 방지

void Awake()
{
    if (Instance == null)
    {
        Instance = this;
    }
    else if (Instance != this)
    {
        // 중복 인스턴스 파괴
        Destroy(gameObject);
    }
}

2. null 체크

// ✅ 안전한 방법
if (GameManager.Instance != null)
{
    GameManager.Instance.GameOver();
}

// ❌ 위험한 방법 (null 참조 에러 가능)
GameManager.Instance.GameOver();

3. 씬 전환 시 주의

// DontDestroyOnLoad를 사용하면 씬 전환 시에도 유지됨
// 필요 없을 때는 수동으로 파괴해야 함
void OnDestroy()
{
    if (Instance == this)
    {
        Instance = null;
    }
}

4. 초기화 순서

// Awake()에서 초기화하는 것이 안전
// Start()는 다른 오브젝트의 Start()보다 먼저 호출되지 않을 수 있음
void Awake()
{
    InitializeSingleton();
}

7. 정리

싱글톤 패턴의 장점

  1. 전역 접근: 어디서든 Instance로 접근 가능
  2. 단일 인스턴스(instance): 하나의 인스턴스(instance)만 존재 보장
  3. 메모리(memory) 효율: 불필요한 중복 인스턴스(instance) 방지

싱글톤 패턴의 단점

  1. 전역 상태: 전역 상태는 테스트와 디버깅이 어려움
  2. 의존성: 다른 클래스(class)와 강하게 결합될 수 있음
  3. 멀티스레드(thread): 멀티스레드(thread) 환경에서 주의 필요

사용 시기

  • 게임 매니저: 게임 상태 관리
  • 사운드 매니저: 사운드 재생 관리
  • 씬 매니저: 씬 전환 관리
  • 설정 매니저: 게임 설정 관리

베스트 프랙티스

  1. Awake()에서 초기화: 다른 오브젝트보다 먼저 초기화
  2. null 체크: 항상 null 체크 후 사용
  3. 중복 방지: 중복 인스턴스(instance)는 즉시 파괴
  4. 적절한 사용: 꼭 필요한 경우에만 사용

연습 문제

  1. SoundManager 싱글톤을 만들어서 어디서든 사운드를 재생할 수 있도록 하세요.

  2. GameManager 싱글톤에 점수(score) 변수(variable)를 추가하고, 다른 스크립트에서 점수를 증가시킬 수 있도록 하세요.

  3. 싱글톤 패턴을 사용하여 씬 전환 시에도 유지되는 설정 매니저를 만드세요.