의존성 주입용으로 사용되는 패키지인 Zenject의 사용법을 알아본다.

Singleton은 알다시피 안티패턴으로 분류된다. 이 문제를 해결하기 위해, 의존성 주입(Dependency Injection)도구 패키지인 Zenject가 쓰인다. 어떻게 쓰는지 알아보자


위의 사진에서 상점 객체는 연필 객체에 의존한다. 과목이 없으면 교수는 존재할 수 없다. 이러한 의존 관계를 객체가 아닌 인터페이스로 바꾸는 것이 DI(Dependency Injection)이다.


🚩 Dependency Injection

외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴이다. 장점은 아래와 같다.

  • 코드의 결합도를 낮춘다
  • 코드의 유연성을 높인다
  • 테스트 용이성 증가
  • 코드 재사용성 증가
  • 유지보수성 향상

객체가 스스로 의존성을 생성하는 대신, 외부에서 제공받도록 하여 객체의 책임 범위를 명확히 한다. 필요에 따라 다른 객체로 쉽게 교체할 수 있게 해준다.

📌 정의

  • 객체의 의존성 외부 주입

객체가 다른 객체를 필요로 할 때, 그 객체를 직접 생성하지 않고 외부의 다른 객체(혹은 프레임워크)로부터 주입받아 사용하는 방식이다.

  • 책임 분리

의존성 생성과 관리에 대한 책임을 객체 내부에서 외부로 위임하여, 객체의 역할을 명확하게 분리한다.

  • IoC(Inversion of Control) 컨테이너

제어의 역전. Zenject와 같은 프레임워크에서 DI를 구현하며, IoC 컨테이너가 객체들의 생성 및 의존성 연결을 자동으로 처리한다.

✅ 장점

  • 결합도 감소 및 유연성 향상

객체 간의 의존성을 인터페이스 중심으로 관리하여 클래스 간의 결합도를 낮추고, 런타임 시점에 의존 객체를 동적으로 주입하여 유연성을 확보한다.

  • 테스트 용이성

의존 객체를 실제 구현체가 아닌 Mock 객체 등으로 쉽게 주입할 수 있어 단위 테스트를 효과적으로 수행할 수 있다.

  • 코드 재사용성 및 유지보수성 향상

객체가 특정 구현체에 종속되지 않고 인터페이스에 의존하기 때문에, 필요에 따라 다양한 구현체를 쉽게 교체하여 코드의 재사용성유지보수성을 높일 수 있습니다.

  • 코드 단순화

객체가 의존성 객체를 직접 생성하고 관리하는 복잡한 코드가 사라져 코드의 가독성이 좋아진다.

  • 순환 의존성 방지

의존성 주입을 통해 객체 간의 관계를 명확히 하여, 불필요한 순환 의존성 발생을 방지할 수 있다.


🚩 Zenject


DI Container가 의존성을 전부 들고 있다가, 객체가 생성될 시 해당 객체가 필요로 하는 의존성을 주입하는 방식(IoC)이다.

Zenject에 의해서 발생되는 의존성 주입은 유니티 라이프 사이클의 Awake()보다도 빨리 수행된다.

💡 기존 방식

객체를 참조하는 데에 있어 다음과 같이 작성했다.

의존성 직접 주입(객체 참조) 시 문제점은 아래와 같다.

  1. IAudioService가 없을 경우, 정상 동작하지 않는다.

  2. 다른 기능을 테스트 해보고자 할 때, IAudioService 객체 참조 선언 스크립트를 직접 수정해야 한다.

  3. 이는 다른 모든 IAudioService를 참조하고자 하는 스크립트들도 마찬가지다.

이러한 문제들을 IoC 컨테너이너Zenject 프레임워크를 통해서 해결한다.

💥 Scene Context

ContextInstaller를 통해, 의존성이 필요한 객체들에게 인터페이스를 통해 외부에서 주입한다.

Scene Context는 생성된 현재 씬에서만 유효하다.
Installer를 필요로 한다.

💡 생성


하이어라키 창에서 위의 경로를 통해 생성이 가능하다.

💥 MonoInstaller

의존성 주입 도구이다. MonoBehaviour로 구성된 Installer이다. 즉, 컴포넌트로 추가가 가능하다.

public class GameInstaller : MonoInstaller
{
	public override void InstallBindings()
    {
    	Container.Bind<IAudioService>() // 컨테이너에 바인딩
        	.To<AudioService>() // 오디오서비스를
            .AsSingle(); // 유일 객체(싱글톤)로
    }
}

이런식으로 작성된 IAudioService 인터페이스를 통해서 주입할 수 있다. 기존의 스크립트에서는 이렇게 쓰면 된다.

public class EnemySpawner
{
	private readonly enemyPrefab;
	private readonly IAudioService audioService;
    
    public EnemySpawner(GameObject enemyPrefab, IAudioService audioService)
    {
    	this.enemyPrefab = enemyPrefab;
        this.audioService = audioService; // DI
    }
}

이렇게 하면 외부(MonoInstaller)에 의해서 의존성이 주입된다.

🔧 MonoBehaviour의 경우

모노비헤이비어를 상속받는 스크립트의 경우에는 [Inject] 어트리뷰트를 작성해서 주입한다.

  • 기존 스크립트
public class Player : MonoBehaviour
{
	private IAudioService audioService;
    
    private void Awake()
    {
    	audioService = new AudioService();
    }
}
  • 의존성 주입
public class Player : MonoBehaviour
{
	private IAudioService audioService;
    
    [Inject]
    public void Construct(IAudioService audioService) // 메서드 주입
    {
    	this.audioService = audioService;
    }
}

📌 목적지 설정

각 스테이지마다 몬스터들의 목적지를 설정해준다고 했을 때, 다음과 같이 Installer를 작성할 수 있다.

public class Stage1SceneInstaller : MonoInstaller
{
	[SerializeField] GameObject target;
    
    public override void InstallBindings()
    {
    	Container.Bind<GameObject>().FromInstance(target);
    }
}

이 컴포넌트를 씬에 추가하고, target에 몬스터의 WayPoint를 넣으면 된다.

FromInstance
에디터 상에서 직접 값을 입력해서 만들어지는 객체를 통해 세팅이 가능하다. 즉, 테스트 상에서 쓰기 좋다.

  • 몬스터 스크립트
public class Monster : MonoBehaviour
{
	[Inject] public GameObject target; // 프로퍼티 주입
    
    private void Update()
    {
		MoveToTarget(target.transform);
    }
    
    private void MoveToTarget(Transform target)
    {
    	Vector3 dir = target.position - transform.position;
        dir.Normalize();
        transform.Translate(dir * 5 * Time.deltaTime);
    }
}

💥 Project Context

Context의 한 종류다. 싱글톤에서 게임 매니저에 해당하는 기능을 위한 Installer가 필요하다면 이걸 이용해야 한다. 전역적이고, 모든 씬에서 쓸 수 있다.

💡 생성


프로젝트 창에서 위의 경로를 통해 프리팹 생성이 가능하다.

생성은 Assets\Resources 폴더에서만 가능하다.

💫 시작

Play를 들어가면 자동으로 프리팹을 통해 생성하고, DontDestroyOnLoad에 들어간다. 즉, 전역적으로 존재하게 된다.

🎫 플레이어 정보

현재 플레이 중인 플레이어의 정보를 담기 위해서 다음과 같이 작성해본다.

public class ProjectInstaller : MonoInstaller
{
	[SerializeField] PlayerData playerData;
    
    public override void InstallBindings()
    {
    	Container.Bind<PlayerData>().FromInstance(playerData);
    }
}

[System.Serializable]
public class PlayerData
{
	public int gold;
    public int exp;
    public string playerName;
}

작성된 인스톨러는 ProjectContext에 Installer로써 추가한다.

🎮 플레이어 컨트롤러

의존성을 주입받을 플레이어 컨트롤러 스크립트를 작성한다.

public class PlayerController : MonoBehaviour
{
	[Inject] PlayerData playerData;
    
    private void Update()
    {
    	if(Input.GetKeyDown(KeyCode.Space))
        {
        	GetGold(100);
        }
    }
    
    public void GetGold(int amount)
    {
    	playerData.gold += amount;
    }
}

이렇게 작성된 플레이어를 씬 1씬 2에 각각 배치하면, 서로 다른 플레이어 객체임에도 불구하고 동일한 데이터를 계속 유지하게 된다.

씬 1에서 전투 승리 후 골드가 200, 경험치가 50이 된 상태에서 씬 2로 이동한다.
새로이 플레이어 객체를 생성하고 다시 ProjectContext에 의해서 의존성이 주입되어도 동일하게 골드 200, 경험치 50을 가지고 있다.

✅ Validate

현재 프로젝트 또는 현재 씬에서 Zenject에 의한 의존성 주입에 문제가 없는지 체크하는 기능이 있다.


해당 경로를 통해 테스트를 돌릴 수 있음. 문제가 있든 없든 결과를 로그로 띄워준다.

💡 그 외

Zenject에는 이외에도 팩토리, 풀링 등 기능이 상당히 많기 때문에, 추후에 심화 학습이 필요하다.

profile
개발 박살내자

0개의 댓글