[2026-04-24] Service Locater

SmartBear·2026년 4월 24일
post-thumbnail

네트워크 프로젝트를 시작함에 있어, SingleTon 을 지양하기 위한 디자인으로 확인한 내용.
정리는 AI 의 도움을 받아 정리함.


유니티 개발 시 Manager.Instance로 대표되는 싱글톤(Singleton)은 구현이 빠르지만, 프로젝트 규모가 커질수록 클래스 간 결합도가 높아지고 단위 테스트가 어려워지는 단점이 있습니다. 이를 해결하기 위해 '객체 자체'가 아닌 '기능(Interface)'을 연결하는 서비스 로케이터(Service Locator) 패턴을 정리합니다.

1. 서비스 로케이터란?

서비스 로케이터는 중앙 저장소(Registry)를 통해 필요한 서비스(매니저)를 등록하고 반환받는 디자인 패턴입니다.

  • 싱글톤과의 차이점: 싱글톤은 호출자가 구체적인 클래스 이름(예: AudioManager)을 알아야 하지만, 서비스 로케이터는 인터페이스(예: IAudioService)만 알면 됩니다.
  • 핵심 철학: "누가 이 일을 하는가(Who)"보다 "어떤 기능을 제공하는가(What)"에 집중하여 객체 간의 의존성을 분리합니다.

2. 아키텍처 구조

전체적인 구조는 DNS 서버가 도메인 이름을 IP로 매핑해 주듯, 인터페이스(Key)실제 구현체(Value)에 연결하는 형태입니다.

  • "Service Interface": 서비스가 수행할 기능을 정의한 약속.
  • "Concrete Service": 인터페이스를 실제로 구현한 클래스 (예: SoundManager, NetworkManager).
  • "Service Locator": 서비스들을 등록(Register)하고 검색(Get)하는 중앙 레지스트리.

3. 예제 코드 구현

서비스 로케이터 (중앙 보관소)

Dictionary를 활용하여 타입별로 서비스를 관리합니다.

using System;
using System.Collections.Generic;
using UnityEngine;

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

    // 서비스 등록
    public static void Register<T>(T service)
    {
        var type = typeof(T);
        if (!_services.ContainsKey(type))
        {
            _services.Add(type, service);
        }
        else
        {
            _services[type] = service;
            Debug.LogWarning($"{type} 서비스가 이미 존재하여 교체되었습니다.");
        }
    }

    // 서비스 해제
    public static void Unregister<T>()
    {
        var type = typeof(T);
        if (_services.ContainsKey(type))
        {
            _services.Remove(type);
        }
    }

    // 서비스 획득
    public static T Get<T>()
    {
        var type = typeof(T);
        if (_services.TryGetValue(type, out var service))
        {
            return (T)service;
        }

        Debug.LogError($"{type} 서비스가 로케이터에 등록되지 않았습니다.");
        return default;
    }
}

서비스 정의 및 구현

구체적인 클래스 대신 인터페이스를 먼저 설계합니다.

// 1. 기능(역할) 정의
public interface IAudioService
{
    void PlayBGM(string clipName);
    void StopBGM();
}

// 2. 실제 구현체
public class AudioManager : MonoBehaviour, IAudioService
{
    private void Awake()
    {
        // 로케이터에 자신을 등록
        ServiceLocator.Register<IAudioService>(this);
        DontDestroyOnLoad(gameObject);
    }

    public void PlayBGM(string clipName) => Debug.Log($"BGM 재생: {clipName}");
    public void StopBGM() => Debug.Log("BGM 정지");

    private void OnDestroy()
    {
        // 객체 파괴 시 해제 (Dangling Pointer 방지)
        ServiceLocator.Unregister<IAudioService>();
    }
}

사용 예시 (Client)

호출하는 쪽에서는 AudioManager가 존재하는지 몰라도 됩니다. 오직 IAudioService만 찾습니다.

public class GameController : MonoBehaviour
{
    void Start()
    {
        // 로케이터를 통해 기능(인터페이스)을 가져옴
        var audio = ServiceLocator.Get<IAudioService>();
        audio?.PlayBGM("Title_Theme");
    }
}

4. 주의사항 및 운영 팁

초기화 순서 (Execution Order)

Get<T>()을 호출하기 전에 반드시 Register<T>()가 선행되어야 합니다. 유니티의 Project Settings -> Script Execution Order에서 매니저들의 순서를 높이거나, 모든 서비스를 일괄 등록하는 Bootstrapper 클래스를 별도로 운영하는 것이 안전합니다.

씬 전환 시 참조 관리

MonoBehaviour 기반 서비스를 등록할 경우, 씬이 바뀌어 객체가 파괴되면 로케이터의 참조가 null이 될 수 있습니다.

  • 전역 서비스: DontDestroyOnLoad를 적용하여 유지.
  • 지역 서비스: OnDestroy에서 반드시 Unregister를 수행하여 메모리 누수 및 에러 방지.

테스트 용이성

서버 통신이 필요한 INetworkService의 경우, 실제 서버 연결 없이 데이터만 반환하는 MockNetworkService를 구현하여 로케이터에 갈아 끼우는 것만으로 오프라인 환경 테스트가 가능해집니다.

5. 요약

서비스 로케이터는 "강한 결합을 끊고 인터페이스를 통해 기능을 연결하는 교환기"입니다. 싱글톤의 편의성을 유지하면서도 객체 지향의 유연성을 챙기고 싶을 때 가장 효과적인 대안입니다.

profile
Python Dev with Infra -> Game Programmer

0개의 댓글