서비스 로케이터
프레임워크와 게임의 어떤 서비스든 가져올 때 호출하는 단 하나의 static 클래스.
어떤 서비스든 가져올 때 호출하는 단 하나의 클래스 · 모든 프레임워크·게임 서비스의 전역 레지스트리 · 항상 초기화됨 — 교체 불필요
무엇을 하는가
서비스 로케이터는 "X 서비스는 어디에 있지?"라는 한 가지 질문에 답하는 작은 static 레지스트리입니다. 부트스트랩이 시작 시 23개 프레임워크 서비스 각각을 여기에 등록합니다. 여러분의 코드는 어디서든 ServiceLocator.Resolve<IMyService>()를 호출해 인스턴스를 돌려받습니다 — 씬 참조도, 싱글턴도, 컴파일 타임 배선도 필요 없습니다. 구현을 교체하고 싶다면(예: 기본 저장 시스템 대신 직접 만든 것) Replace<T>()를 한 번 호출하면 됩니다; 그 외에는 아무것도 바꿀 필요가 없습니다.
빠른 예제
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // Bootstrap registers services first — class-level attribute
public class GameController : MonoBehaviour
{
[SerializeField] private AudioClip _clickSound;
// Cache in a field; never call Resolve in Update().
private IAudioService _audio;
private ISaveService _save;
void Awake()
{
// Bootstrap has already called ServiceLocator.Register<T>() for each service.
_audio = ServiceLocator.Resolve<IAudioService>();
_save = ServiceLocator.Resolve<ISaveService>();
}
public void OnPlayButtonPressed()
{
_audio.PlaySfx(_clickSound);
}
}
실제 저장 시스템 없이 실행하고 싶다면 — 테스트를 위해서든, 기능을 끄기 위해서든 — 내장된 no-op 구현으로 교체하십시오:
// In a test setup, or anywhere before the code under test resolves it:
ServiceLocator.Replace<ISaveService>(new NullSaveService());
// New Resolve calls now return the replacement.
// Code that cached the old instance earlier keeps using that old instance —
// the locator never updates references you already hold.
전체 API
모든 메서드는 ServiceLocator 클래스의 static 메서드입니다. 전부 메인 스레드 전용입니다(동작 및 엣지 케이스 참조).
핵심 연산
-
void Register<T>(T instance)— 키T로 새 서비스를 추가합니다.T에 이미 등록된 서비스가 있으면ServiceAlreadyRegisteredException을,T가 구체 클래스이면ArgumentException을 던집니다(키는 인터페이스 또는 추상 클래스여야 합니다). 여러분 자신의 게임 서비스에 사용하십시오; 프레임워크 자체의 서비스는 Bootstrap이 등록합니다. -
T Resolve<T>()— 등록된 서비스를 가져옵니다.T에 아무것도 등록되어 있지 않으면ServiceNotRegisteredException을 던집니다. 호출할 때마다 딕셔너리 조회가 일어납니다 — 매 프레임 호출하지 마십시오;Awake/Start에서 한 번 resolve해 결과를 필드에 캐시하십시오. -
void Replace<T>(T instance)— 등록을 덮어쓰거나 새로 만듭니다(이미 있어도 오류 없음). 밀려난 인스턴스를 dispose하지 않습니다 — 여러분이 아직 참조를 들고 있는지 알 수 없기 때문입니다. 나가는 서비스가 내부 GameObject를 소유한다면(Time, 오브젝트 풀, 오디오, UI 프레임워크, 스케줄러, 트윈, 트윈 시퀀싱은 각각[CGS] …GameObject를 만듭니다) 직접 dispose하십시오. 그러지 않으면 그 GameObject가 세션이 끝날 때까지 살아서 계속 틱을 돕니다:var previous = ServiceLocator.Resolve<ITimeService>() as System.IDisposable; ServiceLocator.Replace<ITimeService>(new NullTimeService()); previous?.Dispose(); // after the swap, so nothing resolves a disposed instance -
bool TryResolve<T>(out T service)— 소프트 조회; 아무것도 등록되어 있지 않으면false를 반환하고service를null로 설정합니다. 예외 없음. 선택적 서비스에 사용하십시오. -
bool IsRegistered<T>()— 인스턴스를 가져오지 않고 존재 여부만 확인합니다. -
void Unregister<T>()— 등록을 제거합니다. 아무것도 등록되어 있지 않아도 안전하게 호출할 수 있습니다(두 번 호출해도 괜찮습니다).
디버그 및 테스트 헬퍼
void Reset()— 모든 등록을 지웁니다. 에디터와 Development 빌드에서만 제공됩니다; Release 빌드에서는 컴파일 시 제거되어 프로덕션 코드 경로에서는 호출할 수 없습니다.IReadOnlyDictionary<Type, object> GetRegistrySnapshot()— 등록된 모든 서비스의 읽기 전용 사본. 에디터 전용.int RegisteredServiceCount— 등록 항목의 실시간 개수. 에디터 전용.
예외
ServiceLocatorException— 모든 서비스 로케이터 오류의 기반 타입; 이것을 catch하면 전부 처리할 수 있습니다.ServiceAlreadyRegisteredException— 키가 이미 존재해Register가 실패했습니다;.ServiceType프로퍼티를 노출합니다.ServiceNotRegisteredException—Resolve가 실패했습니다;.ServiceType과, 가장 흔한 원인들을 나열하는 메시지를 노출합니다.
누락된 서비스와 선택적 조회
서비스 로케이터 자체에는 no-op 대체물이 없습니다 — 레지스트리는 매 Play 진입 시와 에디터 어셈블리 리로드 후에 항상 초기화됩니다. 서비스가 없으면 Resolve<T>()가 즉시 ServiceNotRegisteredException을 던집니다. 이는 의도된 동작입니다: 나중에 조용히 알게 되는 것이 아니라, 그 서비스가 필요했던 바로 그 줄에서 문제를 발견하게 됩니다.
선택적 서비스를 우아하게 처리하고 싶다면 대신 TryResolve<T>를 사용하십시오:
if (ServiceLocator.TryResolve<IAudioService>(out var audio))
Debug.Log("Audio service is available");
else
Debug.LogWarning("Audio service not registered");
동작 및 엣지 케이스
-
resolve한 것은 캐시하십시오.
Resolve<T>()는 매번 딕셔너리 조회입니다.Update()에서 호출하면 프레임마다 시간을 낭비합니다.Awake나Start에서 한 번 resolve해 참조를 필드에 보관하십시오. -
메인 스레드 전용. 모든 public 메서드는 디버그 빌드에서 Unity 메인 스레드에서 실행되는지 assert합니다.
Task.Run, 스레드 풀, 기타 백그라운드 스레드에서 호출하면 "[ServiceLocator] Main thread only."와 함께 실패합니다. 백그라운드 스레드에서 서비스가 필요하면 먼저 메인 스레드에서 resolve한 뒤 참조를 넘겨주십시오. -
키는 인터페이스 또는 추상 클래스만.
Register<ConcreteClass>(impl)는ArgumentException을 던집니다.T에는 항상 인터페이스나 추상 클래스를 사용하십시오. 이렇게 해야 호출자가 모르는 채로 모든 서비스를 교체할 수 있습니다. -
인터페이스 상속은 따라가지 않습니다.
ISaveService가 어떤IPersistenceService를 상속하고 여러분이ISaveService만 등록했다면,Resolve<IPersistenceService>()는 예외를 던집니다. 레지스트리는 정확한 타입 기준의 딕셔너리입니다. 같은 인스턴스를 두 키 모두로 등록하거나, 등록한 바로 그 키로 resolve하십시오. -
Replace는 이미 들고 있는 참조를 바꿔주지 않습니다.Replace<T>()이후 새Resolve<T>()호출은 새 인스턴스를 반환하지만, 이전 인스턴스를 캐시해 둔 코드는 계속 그것을 사용합니다. 소비자가 resolve하기 전에 구현을 교체하십시오 — 보통은 더 일찍 실행되는Awake나 테스트 셋업에서 합니다. -
너무 이른 resolve. Bootstrap은 첫 씬 로드 전에 모든 프레임워크 서비스를 등록하므로, 씬 코드에서 프레임워크 서비스는 항상 사용할 수 있습니다. 하지만 여러분의 코드가 나중에 — 예를 들어 어떤 MonoBehaviour의
Awake에서 — 추가 서비스를 등록한다면, 그 시점 이전에 실행되는 코드는 아직 그것을 resolve할 수 없습니다.ServiceNotRegisteredException의 메시지가 흔한 순서 문제의 원인들을 짚어 줍니다. -
Domain Reload와 IL2CPP. 레지스트리는 에디터의 Domain Reload 활성화 여부와 무관하게 매 Play 진입 시 지워지고 다시 초기화됩니다. IL2CPP(Unity의 AOT 컴파일 백엔드)로 빌드한다면, 서비스 호출로 주고받는 커스텀 데이터 타입에는 빌드의 코드 스트리핑이 제거하지 않도록 여러분 프로젝트의
link.xml항목이 필요합니다; 프레임워크 자체의 타입은 이미 보호되어 있습니다.
관련 페이지
- 부트스트랩 — 시작 순서와 전체 23개 서비스 목록
- 로거 — 가장 먼저 등록되는 서비스, 그리고 세션 중간 교체 시의 한 가지 주의점
- 저장/불러오기 — 전형적인 resolve 후 캐시 소비자, 그리고
NullSaveService오프 스위치 - 시작하기 — 첫 resolve 후 캐시 스크립트
- 매뉴얼: 문제 해결 —
ServiceNotRegisteredException진단