숏컷 코드
전역에서 하나
-> 정말 "한 인스턴스"가 규칙일 때만 singleton
전역 접근이 편한 것
!= singleton이 맞는 것문법과 예시
singleton은 접근 편의보다 인스턴스 규칙이 먼저다
싱글톤 패턴은 "어디서든 쉽게 접근할 수 있다"보다 "애초에 인스턴스가 하나여야 한다"가 먼저여야 합니다. 오디오 매니저, 설정 레지스트리, 세션 단위 서비스처럼 정말 단일 인스턴스 규칙이 있는 경우는 singleton 후보입니다. 반대로 단지 참조 전달이 귀찮다는 이유로 싱글톤을 만들면, 의존성이 숨어 버리고 테스트와 분리가 어려워집니다.
좋은 후보
- 앱 전체 오디오 출력 조정
- 세션 단위 설정 레지스트리
- 현재 런타임 하나만 존재해야 하는 서비스
나쁜 후보
- 적 AI
- 무기 데이터
- 플레이어 상태전역 접근은 호출부를 단순하게 보이게 하지만 의존성을 숨긴다
GameManager.Instance는 짧고 편합니다. 문제는 클래스가 무엇에 의존하는지 생성자나 필드에서 드러나지 않는다는 점입니다.
전역 접근이 늘어날수록 "이 코드는 어떤 시스템 없이는 못 도는가"가 코드 표면에서 사라집니다. 나중에 씬 테스트를 따로 돌리거나
플레이 모드 초기화 순서를 바꾸면 숨겨진 의존성이 한꺼번에 터지기 쉽습니다.
Unity에서는 singleton과 씬 생명주기 충돌이 자주 난다
Unity에서는 DontDestroyOnLoad, domain reload, play mode 재시작, bootstrap scene이 얽히면서 singleton 버그가 흔합니다.
씬 전환마다 다시 만들어지는지, 중복 인스턴스를 제거하는지, play mode 재시작 후 정적 상태가 남는지를 같이 봐야 합니다.
그래서 단순한 Instance = this보다, 누가 언제 생성하고 폐기하는지를 더 먼저 설계해야 합니다.
지속성과 지연 생성은 편리하지만 수명주기 추론을 더 어렵게 만든다
단순 싱글톤에서 한 단계 더 나아가 DontDestroyOnLoad와 lazy initialization을 붙이면 편리해집니다. 하지만 이 둘은 실제로 자주 쓰이면서도,
편해지는 대신 "언제 존재하기 시작했고 누가 만들었는지"가 흐려집니다. 특히 lazy singleton은 없으면 자동 생성되므로 참조는 쉬워지지만,
부트스트랩 순서와 설정 주입 지점을 추적하기 더 어려워집니다. 편의 기능을 붙일수록 수명주기 규약을 더 명확히 적어 둬야 합니다.
제네릭 singleton은 복제를 줄이지만 전역 상태 문제를 해결해 주진 않는다
Singleton<T> 같은 제네릭 기반 구현은 중복 코드를 줄이는 데는 유용하지만,
전역 접근이 숨기는 결합도와 테스트 어려움을 없애 주지는 않습니다. 즉 구현 중복 문제와 아키텍처 문제는 별개입니다.
generic singleton은 "패턴 사용을 더 쉽게" 만들 뿐, "패턴 남용을 더 안전하게" 만들지는 않습니다.
대안이 더 좋은 경우가 많다
ScriptableObject 기반 설정 자산, 명시적 bootstrapper, 의존성 주입, scene context 같은 방식이 더 읽기 쉽고 테스트하기 쉬운 경우가 많습니다. 특히 "전역 읽기 전용 데이터"는 singleton MonoBehaviour보다 asset reference가 더 단순할 수 있습니다. singleton은 마지막 수단은 아니지만, 가장 쉬운 기본값으로 두기엔 비용이 큽니다.
singleton을 고를 때 핵심
| 상황 | 적합한 선택 |
|---|---|
| 런타임에서 하나만 존재해야 하는 서비스 | singleton 후보 |
| 단순히 참조 전달이 번거로운 경우 | singleton보다 명시적 의존성 전달 검토 |
| 읽기 전용 전역 설정 데이터 | ScriptableObject asset이나 config 자산 검토 |
| 씬 전환을 넘어 계속 살아야 하는 서비스 | bootstrap + 수명주기 설계 후 singleton 여부 결정 |
| singleton 구현을 여러 관리자에 반복 복사하고 있을 때 | generic base는 가능하지만 남용 여부를 먼저 점검 |
| 테스트 가능성과 분리가 중요할 때 | singleton보다 주입/컨텍스트 우선 |
| 중복 생성과 초기화 순서가 자주 꼬일 때 | singleton 자체보다 생성 시점 재설계 |
특이 케이스와 주의할 점
흔한 실패는 "전역에서 자주 쓰인다"는 이유만으로 모든 관리자를 singleton으로 만드는 것입니다.
이렇게 되면 의존성은 숨고, 초기화 순서는 꼬이며, 씬 테스트는 어려워집니다. singleton은 접근 편의 패턴이 아니라
인스턴스 제약 패턴으로 봐야 안전합니다. DontDestroyOnLoad, lazy 생성, generic base를 붙이는 순간
편의성은 오르지만 문제도 더 잘 숨으므로, 정말 소수의 관리자에만 제한하는 편이 낫습니다.
실패 예시
- UIManager, QuestManager, SaveManager, EnemyManager를 전부 singleton으로 둠
- 서로가 서로의 Instance를 바로 호출
결과
- 초기화 순서가 고정됨
- 부분 테스트가 어려워짐
- 씬 전환 버그가 생기면 추적 범위가 넓어짐참고 링크
1 sources