Unity C#/개념 및 문법 정리

[Unity C#] 의존성 주입 (Dependency Injection) DI

routine96 2025. 1. 5. 00:01

DI (Dependency Injection)

의존성 : 객체가 자신의 기능을 수행하기 위해 필요한 다른 객체나 구성 요소

주입 : 객체가 필요한 의존성을 외부에서 제공받는 과정

의존성 주입 : 객체가 자신의 기능을 수행하기 위해 필요한 다른 객체나 구성 요소를 외부에서 제공받는 과정

                     Ex) 컴포넌트를 드래그 앤 드롭으로 스크립트에 연결하는 것도 넓은 의미에서는 의존성 주입이다.

 

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

public class Player : MonoBehaviour
{

    private void Start()
    {

    }

    public void Attack() 
    {
        // Attack 메소드는 BaseAttack()에 의존하고 있다.
        // BaseAttack()이 없다면 Attack()은 기능이 없다.
        BaseAttack();
    }

    private void BaseAttack()
    {
        Debug.Log("Base Attack");
    }
}

 

위 코드의 의존성 관계도를 살펴보면 

 

 

여기서 SpecialAttack, MagicAttack 메소드를 추가해서 enum으로 타입을 지정해 스크립트를 작성한다면 아래 코드 처럼 작성할 수 있다.

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

public class Player : MonoBehaviour
{
    public enum AttackType
    {
        BaseAttack,
        SpecialAttack,
        MagicAttack
    }

    private AttackType attackType;

    private void Start()
    {
        Attack();
    }
    public void Attack() 
    {
        switch (attackType)
        {
            case AttackType.BaseAttack : BaseAttack(); break;
            case AttackType.SpecialAttack : SpecialAttack(); break;
            case AttackType.MagicAttack : MagicAttack(); break;
        }
    }

    private void BaseAttack()
    {
        Debug.Log("Base Attack");
    }
    private void SpecialAttack()
    {
        Debug.Log("Special Attack");
    }
    private void MagicAttack()
    {
        Debug.Log("Magic");
    }
}

위 코드의 의존성 관계도를 살펴본다면

 

 이런 방식의 코드를 작성하다 보면 기능이 추가될 때마다 의존하는 코드가 계속 늘어나고, 코드 변경이 계속 추가로 일어나서 유지 보수가 어려워 진다.

 

 

 위에 코드를 의존성 주입을 해서 코드를 작성한다면 아래 코드로 변형 시킬 수 있다.

namespace DI
{
    public interface IAttackType
    {
        public void Attack();
    }
}
using UnityEngine;

namespace DI
{
    public class BaseAttack : MonoBehaviour, IAttackType
    {
        public void Attack()
        {
            Debug.Log("Base Attack");
        }
    }
}
using UnityEngine;

namespace DI
{
    public  class SpecialAttack : MonoBehaviour, IAttackType
    {
        public void Attack()
        {
            Debug.Log("Special Attack");
        }
    }
}
using UnityEngine;

namespace DI
{
    public class MagicAttack : MonoBehaviour, IAttackType
    {
        public void Attack()
        {
            Debug.Log("Magic Attack");
        }
    }
}
using UnityEngine;

namespace DI
{
    public class AttackDI : MonoBehaviour
    {
        [SerializeField] private BaseAttack baseAttack;
        [SerializeField] private SpecialAttack specialAttack;
        [SerializeField] private MagicAttack magicAttack;
        [SerializeField] private Player player;
        void Start()
        {
            player.SetAttackType(baseAttack);
            player.Attack();
        }
    }
}
using UnityEngine;

namespace DI
{
    public class Player : MonoBehaviour
    {
        private IAttackType attackType;

        public void SetAttackType(IAttackType _attackType)
        {
            attackType = _attackType;
        }

        public void Attack()
        {
            attackType.Attack();
        }
    }
}

 

 

 IAttackType이라는 interface를 상속 받는 BaseAttack, SpecialAttack, MagicAttack 스크립트를 작성한다.

AttackDI 스크립트에서 상속 받은 클래스들을 받아와 Player 스크립트에게 현재 AttackType을 전달해준다.

이렇게 되면 Player는 더 이상 각 공격에 대해 의존하지 않아도(어떤 공격이 있는지 몰라도)  IAttackType에만 의존해

IAttackType을 상속 받는 모든 공격을 사용할 수 있다. 

 

다음은 특정 버튼을 클릭할 때 AttackType이 바뀌는 코드를 작성해보자

using UnityEngine;
using UnityEngine.UI;

namespace DI
{
    public class AttackDI : MonoBehaviour
    {
        [SerializeField] private Button baseAtkButton;
        [SerializeField] private Button specialAtkButton;
        [SerializeField] private Button magicAtkButton;
        
        [SerializeField] private BaseAttack baseAttack;
        [SerializeField] private SpecialAttack specialAttack;
        [SerializeField] private MagicAttack magicAttack;
        [SerializeField] private Player player;
        void Start()
        {
            baseAtkButton.onClick.AddListener(() =>
            {
                player.SetAttackType(baseAttack);
                player.Attack();
            });
            specialAtkButton.onClick.AddListener(() =>
            {
                player.SetAttackType(specialAttack);
                player.Attack();
            });
            magicAtkButton.onClick.AddListener(() =>
            {
                player.SetAttackType(magicAttack);
                player.Attack();
            });
        }
        
        
    }
}

Canvas에 Button 3개를 추가한다음 AttackDI에 할당해주고 각 버튼 클릭시 AttackType을 변경해 실행시키는 코드

 

코드를 작성한 후 내 생각

 솔직히 첫 번째 방식처럼 enum을 사용해 공격 구현과 두 번째 방식 DI 패턴을 활용해서 공격 구현을 할 떄 DI패턴이 더 좋다는 생각을 받지 못했다. 아무리 새로운 공격 기능을 추가한다고해도 DI패턴을 활용해 코드를 구현했을 때 더 복잡하고 구현 방식이 어렵다고 생각을 한다. enum을 사용했을 때 코드가 훨씬 간결하고 작성하기가 쉬워 보인다. 그래서 AI에게 자문을 구했다.

 

DI패턴 장점

1. 확장성이 좋다

  • 새로운 공격 타입을 추가할 때 IAttackType 인터페이스만 구현하면 된다
  • Player 클래스의 코드를 수정할 필요가 없다 -> 개방 폐쇄 원칙 (Open - closed Principle)

2. 유지 보수가 용이하다

  • 각 공격 타입이 독립적인 클래스로 분리되어 있어 수정이 쉽다
  • 공격 로직이 변경되어도 다른 클래스에 영향을 주지 않는다

3. 테스트하기 쉽다

  • 각 공격 타입을 독립적으로 테스트할 수 있다
  • Mock 객체를 사용한 테스트가 용이하다

4. 유연성이 높다

  • 런타임에 동적으로 공격 타입을 변경할 수 있다
  • UI나 다른 시스템과의 결합도가 낮다

 

DI패턴 단점

1. 코드 복잡도가 증가한다

  • 파일 수가 많아져서 프로젝트 구조가 복잡해진다
  • 작은 기능에 비해 많은 클래스와 인터페이스가 필요하다

2. 초기 설정이 번거롭다

  • Unity Inspector에서 각 컴포넌트를 수동으로 연결해야 한다
  • 새로운 공격 타입마다 GameObject에 스크립트를 추가해야 한다.

3. 메모리 사용량이 증가할 수 있다.

  • 각 공격 타입이 별도의 MonoBehaviour로 구현되어 있어 더 많은 리소스를 사용한다
  • 단순한 기능에 비해 오버헤드가 일어날 수 있다

4. 디버깅이 복잡할 수 있다

  • 여러 파일에 걸쳐 로직이 분산되어 흐름 파악이 어려울 수 있다
  • 실행 준 문제가 발생했을 때 원인을 찾기가 더 어려울 수 있다

결론적으로 현재 구현한 기능의 규모와 복잡도를 고려했을 때 enum을 사용하는 방식이 더 적합할 수 있다. 그러나 앞으로 시스템이 확장되거나, 각 공격 타입이 복잡한 로직을 가지게 되면 DI패턴을 사용하는 방식이 더 좋은 선택이 될 수 있다.

 

 

현재 구조를 더 효율적인 방식으로 작성하기 위한 키워드

  • ScriptableObejct
  • Factory Pattern
  • Strategy Pattern
  • Command Pattern

 Scriptable Object와 다른 디자인 패턴, 현재 작성한 DI 패턴을 조합해 더 효율적으로 코드를 작성할 수 있다. 이는 추후에 다른 디자인 패턴에 대해 학습하고 조합해서 더 효율적으로 작성하겠다.

 

마무리

 코드 작성에 정답은 없다. 그러나 효율적인 코드는 존재한다고 생각한다. 앞으로도 코드를 작성할 때 더 효율적인 방법이 없을까를 꾸준히 고민해야겠다. 무작정 디자인 패턴을 사용하는 것이 항상 좋은 정답이 될수는 없다. 프로젝트의 규모와 복잡도를 고려해서 상황과 필요에 따라 적절한 조합을 통해 코드를 작성해야한다.

 

 

https://happy-coding-day.tistory.com/163

 

Mock 객체란 무엇일까? 왜 써야될까?

아래 내용은 위 책에서 말하는 4장 TDD with Mock 에서 내용을 발췌했습니다. TDD를 공부하면서 Mock 이라는 용어는 너무나도 많이 나오고, 실제로 테스트 프레임워크를 사용하면 Mock 객체를 많이 사용

happy-coding-day.tistory.com

https://www.youtube.com/watch?v=WsnkyHVu-Gw&t=154s