숏컷 코드
high-level 흐름
-> 구체 구현 직접 new 하지 않음
-> 인터페이스 또는 계약에 의존
-> 조립은 bootstrapper / installer에서문법과 예시
DIP의 핵심은 "상위 흐름이 하위 구현을 직접 고르지 않는 것"이다
전투 시스템이 DamagePopupMono, UnityAudioService, InventoryManager를 직접 생성하거나 찾기 시작하면, high-level 규칙이 low-level 구현에 고정됩니다.
DIP는 상위 흐름이 "무엇을 해야 하는가"만 알고, 실제 구현 선택은 조립 지점으로 미루는 원칙입니다.
나쁜 예
BattleSystem -> new DamagePopupMono()
더 나은 예
BattleSystem -> IDamagePopup
조립 지점 -> UnityDamagePopup 연결Unity에서는 조립 지점을 따로 두지 않으면 DIP가 금방 무너진다
씬 로딩 직후 FindObjectOfType, singleton 접근, Inspector 직접 연결로 문제를 빨리 푸는 경우가 많습니다. 하지만 이 방식은 구현 교체와 테스트 분리를 어렵게 만듭니다.
그래서 boot scene, bootstrapper, installer, context 같은 조립 지점이 필요합니다.
service locator보다 bootstrapper가 더 선명한 경우가 많다
둘 다 "어딘가에서 구현을 찾는다"는 점은 비슷해 보이지만, service locator는 호출부가 여전히 찾기 동작을 직접 수행한다는 점에서 의존성이 숨기 쉽습니다. 반대로 bootstrapper는 조립을 앞단에서 끝내 두므로 high-level 흐름이 조회 책임까지 지지 않아도 됩니다.
인터페이스만 만든다고 DIP가 되는 것은 아니다
인터페이스를 써도 조립이 여기저기 흩어져 있으면 구조는 여전히 약합니다. DIP는 계약 자체보다 "누가 구현을 연결하는가"가 더 중요합니다. 게임 코드에서는 특히 초기화 순서와 수명주기가 있으므로, 조립 지점을 먼저 고정하는 편이 안전합니다.
ScriptableObject와 이벤트 채널도 DIP를 돕는 도구가 될 수 있다
모든 DIP가 전통적인 DI 컨테이너를 뜻하는 것은 아닙니다. 읽기 전용 설정은 ScriptableObject 자산으로 넘기고, 느슨한 알림은 event channel이나 observer로 분리하면 구체 MonoBehaviour 결합을 줄일 수 있습니다. 핵심은 구현을 바꿔도 high-level 규칙이 거의 흔들리지 않는가입니다.
DIP를 볼 때 핵심
| 상황 | 적합한 선택 |
|---|---|
| gameplay 흐름이 특정 Unity 구현을 직접 호출할 때 | 계약 분리 + 조립 지점 도입 |
| 씬마다 다른 구현을 끼워 넣어야 할 때 | bootstrapper / installer |
| 읽기 전용 설정만 공유하면 될 때 | ScriptableObject 자산 |
| 단순 알림 전달이 필요할 때 | observer / event channel |
| 구현 교체보다 초기화 순서가 더 큰 문제일 때 | 조립 시점부터 고정 |
특이 케이스와 주의할 점
흔한 실패는 인터페이스만 만든 뒤 실제 연결은 singleton이나 FindObjectOfType로 계속 해결하는 것입니다.
이러면 타입 이름만 바뀌고 의존성 구조는 그대로 남습니다.
실패 예시
- IAudioService를 만들었지만
- 실제로는 각 클래스가 AudioManager.Instance를 다시 참조
결과
- 교체 가능성은 생기지 않음
- 테스트도 여전히 전역 상태에 묶임참고 링크
1 sources