I'm FanJae.

[20260529] Unity 정리 ( 싱글톤 패턴, Singleton Pattern ) 본문

Unity/Unity 초격차캠프

[20260529] Unity 정리 ( 싱글톤 패턴, Singleton Pattern )

FanJae 2026. 5. 29. 23:16

1. 정의

- 싱글톤 패턴은 특정 클래스의 객체가 프로그램 전체에서 단 하나만 존재하도록 보장하고, 어디서든 그 객체에 접근할 수 있게 만드는 디자인 패턴이다.

- Unity에서는 보통 GamaManager , SoundManager , UIManager 처럼 게임 전체에서 하나만 존재해야 하는 관리자 객체에서 자주 사용한다.

 

public static GameManager Instance { get; private set; }

- 위 코드는 외부에서 GameManager.Instance 로 접근할 수 있지만, Instance 를 바꾸는 것은 클래스 내부에서만 가능하게 만든다.


2. 필요한 이유

- 게임에서는 여러 오브젝트가 같은 관리자 객체에 접근해야 하는 경우가 많다.

- 예를 들어 점수를 올릴 때마다 매번 GameManager를 찾아야 한다면 번거롭다.

GameManger.Instance.AddScore(10);

- 싱글톤을 사용하면 위처럼 전역 접근 지점을 통해 쉽게 호출이 가능하다.


3. 적용 시 유의 사항

- GameManager 가 여러 개 생기면 점수, 상태, UI 갱신 기준이 꼬일 수 있다.

- 예를 들어, A GameManager 는 점수 10점, B GameManager 는 점수 0점을 가지고 있다면, 게임 점수가 무엇인지 애매해진다.

- 그래서 Awake()에서 이미 인스턴스가 존재하는지 검사한다.

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

- Unity에서 Awake는 Start보다 먼저 호출되므로, 싱글톤 인스턴스 초기화 위치로 자주 사용된다.


4. 적용 예시

using TMPro;
using UnityEngine;

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

    [SerializeField] private TextMeshProUGUI scoreText;
    private int score;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject); // 씬이 전환되어도 파괴되지 않게 만듦. (삭제되지 않음)
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Start()
    {
        score = 0;
        UpdateScoreUI();
    }

    public void AddScore(int amount)
    {
        score += amount;
        UpdateScoreUI();
    }

    private void UpdateScoreUI()
    {
        if (scoreText != null)
        {
            scoreText.text = $"Score : {score}";
        }
    }
}
  • 일반적으로 Unity에서 씬을 새롭게 로드하는 경우 기존 씬 오브젝트는 모두 파괴되지만, DontDestoryOnLoad 를 호출하면 유지된다.

5. 제네릭 싱글톤 패턴

- 매니저 클래스마다 같은 싱글톤 코드를 반복 작성하면 중복 작성이 많아진다.

public static GameManager Instance { get; private set; }

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

- 이런 공통 로직을 부모 클래스로 빼면 제네릭 싱글톤을 만들 수 있다.

using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    public static T Instance { get; private set; }

    protected virtual void Awake()
    {
        if (Instance == null)
        {
            Instance = this as T;
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

- 사용하는 쪽에서는 이와 같이 사용할 수 있다.

public class GameManager : Singleton<GameManager>
{
    public void AddScore(int amount)
    {
        Debug.Log($"Score + {amount}");
    }
}

6. 장점

- 싱글톤의 장점은 하나의 객체만 사용하므로 게임 상태를 중앙에서 관리하기 쉽다.

GameManager.Instance.AddScore(100);

- 또한 다른 스크립트에서 참조를 일일이 연결하지 않아도 접근할 수 있어 코드 작성이 편하다.


7. 주의점 또는 단점

싱글톤은 편하지만 남용하면 문제가 생긴다.

- 가장 큰 문제는 의존성이 강해진다.

GameManager.Instance.AddScore(10);
  • 이 코드가 여러 클래스에 퍼져있으면 각 클래스가 GameManager 에 의존하게 되고, 나중에 GameManager 구조가 바뀌면, 관련 코드도 수정이 필요하다.

8. 싱글톤을 대체할 수 있는 방법

- 싱글톤은 편하지만 모든 객체를 Instance로 접근하게 만들면 의존성이 강해진다. 그래서 상황에 따라 다른 방법을 쓰는 것이 더 낫다.

 

① Inspector 참조

- 가장 단순한 대체 방법은 필요한 객체를 직접 연결하는 것이다.

public class Player : MonoBehaviour
{
    [SerializeField] private GameManager gameManager;

    private void OnKillEnemy()
    {
        gameManager.AddScore(10);
    }
}

- 이 방식은 Player가 어떤 GameManager를 사용하는지 Inspector에서 바로 확인할 수 있다.

- 단점은 오브젝트가 많아지면 참조 연결이 번거롭다는 점이다.

 

② ScriptableObject 사용

- 이런 기법도 있다고 하는데… 이건 나중에 좀 더 알아봐야 할 것 같다.

 

③ 이벤트 기반 구조

- 객체가 서로 참조하지 않아도 되게 만들고 싶다면 이벤트를 사용할 수 있다.

public class Enemy : MonoBehaviour
{
    public static event System.Action<int> OnEnemyKilled;

    private void Die()
    {
        OnEnemyKilled?.Invoke(10);
        Destroy(gameObject);
    }
}
public class GameManager : MonoBehaviour
{
    private int score;

    private void OnEnable()
    {
        Enemy.OnEnemyKilled += AddScore;
    }

    private void OnDisable()
    {
        Enemy.OnEnemyKilled -= AddScore;
    }

    private void AddScore(int amount)
    {
        score += amount;
    }
}

- 이 방식을 사용하면 Enemy가 GameManger 를 직접 알 필요가 없다.

- 대신 이벤트 구독 해제를 제대로 해줘야 하고, 예상하지 못한 호출이나 메모리 참조 문제가 생기지 않게 주의해야 한다.


④ 의존성 주입

- 의존성 주입은 객체가 필요한 의존 객체를 직접 찾지 않고, 외부에서 전달받는 방식이다.

- Unity에서는 간단히 생성자 주입보다는 메서드나 초기화 함수로 전달하는 형태를 많이 쓴다.

public class Player
{
    private readonly IScoreService scoreService;

    public Player(IScoreService scoreService)
    {
        this.scoreService = scoreService;
    }

    public void KillEnemy()
    {
        scoreService.AddScore(10);
    }
}

- 인터페이스를 사용하면 실제 점수 시스템을 교체하기가 쉬워질 수 있다.

public interface IScoreService
{
    void AddScore(int amount);
}

- 다만, Unity의 MnonoBehaviour 는 new 로 직접 생성하는 구조가 아니다. 따라서 일반 C# 객체보다 DI 적용이 번거로워질 수 있다.


9. 정리

- 싱글톤 패턴은 객체를 하나만 만들고, 전역적으로 접근할 수 있게 하는 패턴이다.

- Unity에서는 GameManager, SoundManager, DataManager처럼 게임 전체에서 하나만 존재해야 하는 객체에 자주 사용된다.

- 다만, 모든 관리자를 무조건 싱글톤으로 만들면 클래스 간 의존성이 강해지고 유지보수가 어려워질 수 있다.

-  따라서 게임 전체 상태를 관리해야 하는 객체에만 제한적으로 사용하는 것이 좋다.

Comments