클래스 의존관계가 얽히는 건 초보 개발자(저요)를 포함한 많은 개발자들에게 치명적인 부담과 고뇌를 선사한다, 이를 막기 위한 근본적인 해결책 중 하나가 바로 "의존성 주입"이다
그럼 그게 무엇이냐 하면
클래스가 필요로 하는 다른 객체(의존성)을 직접 생산하지 않고, 외부에서 주입받는 방식
객체가 자신의 의존성을 직접 만들거나 찾지 않고 누군가에게 받아와서 사용
결론적으로 결합도 줄이고, 테스트 가능성과 재사용성을 높이는 설계 방식
FindObjectOfType이라는 이 악마같은 코드를 쓰지 않으려는 목적이기도 함
예를 들어
public class Player : MonoBehavior{
private AudioManager audioManger;
private void Start()
{
audioManager = FindObjectOfType<AudioManager>();
}
public void Jump()
{
audioManager.Play("jump");
}
이런 식일 때 당연히 AudioManager가 없으면 에러가 날거고
이 오디오매니저를 다른 클래스로 바꾸려면 플레이어 코드까지 수정해야함
테스트환경에서 무조건 이거 포함되어있어야함
결론적으로 강한 결합도, 유지보수와 유연성을 해친다
public class Player : MonoBehaviour
{
private IAudioService audioService;
public void Initialize(IAudioService audioService)
{
this.audioService = audioService;
}
public void Jump()
{
audioService.Play("jump");
}
이런식으로 한다 쳐보자
오디오서비스라는 인터페이스를 받아온다고 했을 때 AudioManager을 알 필요는 없고
수정도 오디오서비스라는 인터페이스에서 일괄적으로 진행할 수 있게 된다.
의존성 주입 중 가장 중요한 것은 "구현 클래스가 아닌 인터페이스를 주입하는 것"이다
직접 주입을 하면
어떤 함수에 기능이 계속 추가될 때마다 플레이어 클래스도 그에 맞게 수정해야 하고, 테스트도 불편하다
그러나 인터페이스 형식 자체를 주입해버리면 인터페이스만 알고 있어도 문제가 없기 때문에 느슨한 결합을 추구할 수 있다.
일반적으로 Initialize() 메소드를 사용한 주입이 가장 많음
실전 예시를 따오자면
//인터페이스 정의
public interface IInventoryService
{
void AddItem(Item item);
List<Item> GetItems();
}
//구현체
public class InventorySystem : IInventoryService
{
private List<Item> items = new();
public void AddItem(Item item) => items.Add(item);
public List<Item> GetItems() => items;
}
//UI 클래스
public class InventoryUI : MonoBehaviour
{
private IInventoryService inventory;
public void Initialize(IInventoryService inventory)
{
this.inventory = inventory;
}
public void RefreshUI()
{
foreach (var item in inventory.GetItems())
{
// UI에 아이템 정보 표시
}
}
}
이런 식으로 UI 자체에 인벤토리시스템의 구조를 알 필요가 없게 한다
의존성 주입은 미래를 위한 투자임