Quick Flow
의존성 역전은 interface를 많이 만드는 일이 아니라, high-level game rule이 구현을 new, singleton, Find...로 직접 고르지 않게 하는 구조입니다. caller가 필요한 capability를 계약으로 받고, scene entry 또는 bootstrapper가 그 구현과 lifetime을 한 번 연결합니다.
public interface IDamagePopup { void Show(int amount, Vector3 worldPosition); }
public sealed class CombatResolver
{
private readonly IDamagePopup popup;
public CombatResolver(IDamagePopup popup) => this.popup = popup;
public void ResolveHit(Hit hit)
{
hit.Target.ApplyDamage(hit.Amount);
popup.Show(hit.Amount, hit.Position);
}
}| 의존 대상 | gameplay code가 알 것 | bootstrap이 알 것 |
|---|---|---|
| damage popup | IDamagePopup.Show | Canvas 구현·scene reference |
| audio | play capability와 event | mixer·pool·addressable asset |
| 설정 | 읽기 전용 config contract | ScriptableObject asset 선택 |
| cross-scene service | request·snapshot·event contract | 생성·ready·dispose·scene owner |
계약과 조립 지점
interface는 관련 기능의 contract이고, caller는 그 contract만 사용합니다. 그러나 interface를 만든 뒤 각 class가 AudioManager.Instance를 찾으면 concrete lookup이 그대로 남아 의존성이 숨습니다. scene bootstrapper, installer, composition root처럼 조립을 맡은 곳에서 구현을 찾고 연결하면 gameplay rule은 initialization 조회 책임을 지지 않습니다.
Unity에서는 MonoBehaviour가 scene object와 lifecycle을 가지므로 plain C# service와 Component 구현을 구분하면 test가 쉬워집니다. bootstrapper는 scene load 때 reference를 받고, required dependency가 없는 경우 fail fast하거나 시작을 중단할 정책을 가집니다. asynchronous load가 있으면 constructor injection만으로 ready를 보장하지 않으므로 handle 완료·취소·unload owner까지 명시합니다.
도구를 목적과 바꾸지 않기
ScriptableObject는 공유 definition·config를 authoring asset으로 제공하는 수단이지 모든 runtime service의 대체가 아닙니다. event channel은 여러 listener에 notification을 보낼 수 있지만, request의 return value·순서·error 처리까지 숨기면 direct contract보다 오히려 흐려집니다. service locator는 호출부가 dependency를 찾게 하므로 작은 prototype에서는 편해도 dependency graph가 커질수록 test와 lifecycle을 추적하기 어렵습니다.
도입 기준
구현 교체·scene별 wiring·test double·독립 lifecycle이 실제 요구일 때 dependency boundary를 둡니다. type 하나를 교체할 가능성만으로 모든 field를 interface로 만들 필요는 없습니다. dependency가 하나뿐이고 scene-local이며 test에서도 real implementation이 단순하면 serialized reference가 더 명확할 수 있습니다.
DI container, interface, event channel은 결합을 없애지 않습니다. 누가 구현을 만들고, 언제 ready이며, scene unload에서 누가 dispose·unsubscribe하는지 답하지 못하면 의존성은 단지 다른 파일로 이동한 것입니다.
참고 링크
2 sources