
네트워크 프로젝트를 시작함에 있어, SingleTon 을 지양하기 위한 디자인으로 확인한 내용.
정리는 AI 의 도움을 받아 정리함.
유니티 개발 시
Manager.Instance로 대표되는 싱글톤(Singleton)은 구현이 빠르지만, 프로젝트 규모가 커질수록 클래스 간 결합도가 높아지고 단위 테스트가 어려워지는 단점이 있습니다. 이를 해결하기 위해 '객체 자체'가 아닌 '기능(Interface)'을 연결하는 서비스 로케이터(Service Locator) 패턴을 정리합니다.
서비스 로케이터는 중앙 저장소(Registry)를 통해 필요한 서비스(매니저)를 등록하고 반환받는 디자인 패턴입니다.
AudioManager)을 알아야 하지만, 서비스 로케이터는 인터페이스(예: IAudioService)만 알면 됩니다.전체적인 구조는 DNS 서버가 도메인 이름을 IP로 매핑해 주듯, 인터페이스(Key)를 실제 구현체(Value)에 연결하는 형태입니다.
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>();
}
}
호출하는 쪽에서는 AudioManager가 존재하는지 몰라도 됩니다. 오직 IAudioService만 찾습니다.
public class GameController : MonoBehaviour
{
void Start()
{
// 로케이터를 통해 기능(인터페이스)을 가져옴
var audio = ServiceLocator.Get<IAudioService>();
audio?.PlayBGM("Title_Theme");
}
}
Get<T>()을 호출하기 전에 반드시 Register<T>()가 선행되어야 합니다. 유니티의 Project Settings -> Script Execution Order에서 매니저들의 순서를 높이거나, 모든 서비스를 일괄 등록하는 Bootstrapper 클래스를 별도로 운영하는 것이 안전합니다.
MonoBehaviour 기반 서비스를 등록할 경우, 씬이 바뀌어 객체가 파괴되면 로케이터의 참조가 null이 될 수 있습니다.
서버 통신이 필요한 INetworkService의 경우, 실제 서버 연결 없이 데이터만 반환하는 MockNetworkService를 구현하여 로케이터에 갈아 끼우는 것만으로 오프라인 환경 테스트가 가능해집니다.
서비스 로케이터는 "강한 결합을 끊고 인터페이스를 통해 기능을 연결하는 교환기"입니다. 싱글톤의 편의성을 유지하면서도 객체 지향의 유연성을 챙기고 싶을 때 가장 효과적인 대안입니다.