서비스 로케이터

프레임워크와 게임의 어떤 서비스든 가져올 때 호출하는 단 하나의 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를 반환하고 servicenull로 설정합니다. 예외 없음. 선택적 서비스에 사용하십시오.

  • bool IsRegistered<T>() — 인스턴스를 가져오지 않고 존재 여부만 확인합니다.

  • void Unregister<T>() — 등록을 제거합니다. 아무것도 등록되어 있지 않아도 안전하게 호출할 수 있습니다(두 번 호출해도 괜찮습니다).

디버그 및 테스트 헬퍼

  • void Reset() — 모든 등록을 지웁니다. 에디터와 Development 빌드에서만 제공됩니다; Release 빌드에서는 컴파일 시 제거되어 프로덕션 코드 경로에서는 호출할 수 없습니다.
  • IReadOnlyDictionary<Type, object> GetRegistrySnapshot() — 등록된 모든 서비스의 읽기 전용 사본. 에디터 전용.
  • int RegisteredServiceCount — 등록 항목의 실시간 개수. 에디터 전용.

예외

  • ServiceLocatorException — 모든 서비스 로케이터 오류의 기반 타입; 이것을 catch하면 전부 처리할 수 있습니다.
  • ServiceAlreadyRegisteredException — 키가 이미 존재해 Register가 실패했습니다; .ServiceType 프로퍼티를 노출합니다.
  • ServiceNotRegisteredExceptionResolve가 실패했습니다; .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()에서 호출하면 프레임마다 시간을 낭비합니다. AwakeStart에서 한 번 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 진단