유니티 / / 2026. 4. 8. 12:26

디자인 패턴 - 서비스 로케이터 패턴(Service Locator Pattern)

블로그 포스팅 형식에 맞게 서비스 로케이터 패턴을 작성해드릴게요!

블로그 글 형식에 맞게 서비스 로케이터 패턴 포스팅을 작성해드릴게요!


서비스 로케이터 패턴(Service Locator Pattern) — Unity 게임 개발에서의 활용


예전에 플레이어블 광고 게임 프로젝트를 진행하면서, SoundManager, ObjectPool, GameEventBus 등 여러 매니저 클래스들을 각 시스템이 직접 참조해야 하는 상황이 생겼는데요. 그때마다 FindObjectOfType<>()이나 싱글톤 남용으로 코드가 지저분해지는 걸 느꼈어요. 이 문제를 깔끔하게 해결해준 게 바로 서비스 로케이터 패턴(Service Locator Pattern) 이었습니다!


1. 서비스 로케이터 패턴이란?

서비스(의존성)를 어디서든 등록하고 꺼내 쓸 수 있는 중앙 레지스트리를 두는 디자인 패턴입니다.

이 구조에서 등장하는 세 가지 역할이 있어요.

  • Service Interface : 시스템이 사용할 기능의 추상 계약 (인터페이스 또는 추상 클래스)
  • ServiceLocator : 서비스를 등록하고, 요청하면 꺼내주는 중앙 레지스트리
  • Client : ServiceLocator에 서비스를 요청해서 사용하는 객체

비유하자면, 호텔 컨시어지와 똑같아요. 손님(Client)은 컨시어지(ServiceLocator)에게 "렌터카 업체 알려줘!"라고 요청하면, 컨시어지가 적절한 업체(Service)를 연결해주는 구조입니다!


2. 왜 서비스 로케이터 패턴을 쓸까?

제가 이 패턴을 쓰게 된 이유는 크게 네 가지입니다.

  • 의존성 역전 → 클라이언트는 구체 구현체가 아닌 인터페이스만 알면 됩니다.
  • 교체 용이성 → 테스트 시 Mock 서비스로 교체하는 게 아주 간단해집니다.
  • 전역 접근 단순화 → 싱글톤처럼 FindObjectOfType<>() 없이 어디서든 서비스에 접근할 수 있습니다.
  • 런타임 바인딩 → 앱 실행 중에도 서비스를 교체하거나 재등록할 수 있어 유연한 구조를 만들 수 있습니다.

즉, 의존성을 코드 안에 "박아넣는" 대신 중앙에서 관리하게 되어, 규모가 커질수록 진가가 드러나는 패턴입니다!


3. 사용 방법

Unity C# 기준으로 간단하게 구현하는 방법을 소개할게요.

① 서비스 인터페이스 정의

 
 
csharp
public interface IAudioService
{
    void Play(string clipName);
    void Stop();
}

② ServiceLocator 작성

 
 
csharp
public static class ServiceLocator
{
    private static readonly Dictionary<Type, object> _services = new();

    public static void Register<T>(T service)
    {
        _services[typeof(T)] = service;
    }

    public static T Get<T>()
    {
        if (_services.TryGetValue(typeof(T), out var service))
            return (T)service;

        throw new Exception($"Service {typeof(T).Name} not registered!");
    }

    public static void Unregister<T>()
    {
        _services.Remove(typeof(T));
    }
}

③ 구현체 작성

 
 
csharp
public class SoundManager : MonoBehaviour, IAudioService
{
    public void Play(string clipName)
    {
        Debug.Log($"사운드 재생: {clipName}");
        // 실제 AudioSource 처리
    }

    public void Stop()
    {
        Debug.Log("사운드 정지");
    }
}

④ 등록 및 사용

 
 
csharp
// 초기화 시점 (e.g. GameBootstrapper)
ServiceLocator.Register<IAudioService>(soundManager);

// 어느 클래스에서든
ServiceLocator.Get<IAudioService>().Play("bgm_main");

Unity에서는 Awake()나 별도의 Bootstrapper 스크립트에서 서비스를 등록해두면, 씬 어디서든 깔끔하게 꺼내 쓸 수 있어요!


4. 실제 적용 예시

플레이어블 광고 게임에서 NPCStateMachine이 사운드와 이벤트 버스를 필요로 하는 상황을 가정해볼게요.

적용 전 : NPCStateMachine 안에서 SoundManager.Instance, GameEventBus.Instance를 직접 참조 → 싱글톤 남용, 테스트 불가, 교체 어려움

적용 후 : ServiceLocator.Get<IAudioService>().Play("npc_idle") 처럼 인터페이스로만 접근 → NPC 코드는 구현 세부사항을 전혀 모름

나중에 사운드 시스템을 DummyAudioService로 교체해서 테스트할 때도, 등록만 바꾸면 NPC 코드는 한 줄도 건드릴 필요가 없어요!


5. 옵저버 패턴과의 비교

둘 다 "결합도를 낮추는 패턴"이지만 역할이 달라요.

아래 다이어그램으로 한눈에 비교해볼게요.

 
 
 

 


6. 주의할 점 — 안티패턴이라는 오해

서비스 로케이터는 종종 안티패턴으로 불리기도 해요. 의존성이 숨겨져서 "이 클래스가 뭘 필요로 하는지" 겉으로 보이지 않기 때문입니다.

그래서 실제 프로젝트에서 쓸 때는 이런 점을 챙기면 좋아요.

  • 인터페이스로만 등록 → 구현체를 직접 꺼내 쓰면 의존성 역전의 의미가 없어집니다.
  • Null Object 패턴 병행 → 등록되지 않은 서비스 요청 시 예외 대신 무동작 구현체를 반환하면 안전합니다.
  • 씬 전환 시 재등록 → Unity에서는 씬이 바뀌면 등록된 서비스가 파괴될 수 있으니 DontDestroyOnLoad나 Bootstrapper로 관리해야 합니다.

7. 마무리

서비스 로케이터 패턴은 단순히 "전역에서 뭔가를 꺼내 쓰는 패턴"처럼 보이지만, 올바르게 쓰면 의존성 역전 + 교체 가능성 + 테스트 용이성 세 가지를 동시에 챙길 수 있는 강력한 패턴입니다.

특히 SoundManager, EventBus, ObjectPool처럼 여러 시스템에서 공유되는 서비스가 많은 Unity 게임 프로젝트에서는, 이 패턴을 익혀두면 싱글톤 지옥에서 벗어날 수 있으니 꼭 한 번 써보시길 추천드립니다!

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유