Patrick's Devlog

[Unity] Zenject : 개요 본문

Project/Unity Engine

[Unity] Zenject : 개요

Patrick_ 2023. 6. 16. 23:05

1. Zenject?

Unity 3D를 대상으로 제작된 DI 프레임워크(Dependency Injection Framework)이다. 이는 확장이 가능하고, 유연한 방법을 통해 코드를 쉽게 작성, 재사용, 리팩토링, 테스트를 진행할 수 있다. 이에 대한 특징으로는 의존성 주입, 조건부 바인딩, 컨테이너 중첩, 하나의 Scene에서 다음 Scene으로 정보를 전달하기 위해 여러 Unity Scene에 삽입가능, 한 Scene이 다른 Scene에서 바인딩을 상속할 수 있도록 하는 Parent Scene지정 등 여러 가지가 존재한다. 


2. Tutorial

설명으로는 사실 와닿지 않아서, 간단하게 설치해보고 예시를 확인해보려고 한다. 설치는 쉽게 에셋 스토어에서 할 수 있다. 프로젝트에 설치를 했다면 간단하게 코드를 통해 이해를 해보자. Zenject의 목적은 당연히 의존성 주입(Dependency Iinjection)이다. 의존성 주입에 대해 이해하고자 이 곳에서 따로 정독하였다. 기본적인 튜토리얼은 공식 깃허브를 참고하여 진행하였다. 

 

에셋 스토어에서 설치를 완료한 후 Hierarchy 탭에서 Zenject -> Scene Context를 선택한다. 그럼 SceneContext가 생성된다. 

후에 Project 탭에서 Create -> Zenject -> MonoInstaller를 선택한다. 파일 이름은 알아보기 쉽게 뒤에 -Installer를 붙여주면 된다. 본인은 튜토리얼에 나온 것처럼 TestInstaller.cs로 지정해주었다. 그리고 이를 Scene에 추가해준다. GameObject를 생성해서 붙여줘도 좋고, SceneContext 안에 둬도 상관없다. 그리고 TestInstaller.cs를 SceneContext 안에 Mono installers에 추가해준다. 

그리고 TestInstaller.cs에 아래의 코드를 넣어준다.

using UnityEngine;
using Zenject;

public class TestInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<string>().FromInstance("Hello Zenject!");
        Container.Bind<Greeter>().AsSingle().NonLazy();
    }
}

public class Greeter
{
    public Greeter(string message)
    {
        Debug.Log(message);
    }
}

필수는 아니나, 후에 Edit -> Zenject -> Validate Current Scene을 이용하여 검증을 진행한다. 그럼 아래와 같이 로그가 뜬다. 

지금까지 잘 따라오지 못했으면 아마 성공하지 못했다고 떴을 것이다. SceneContext는 Zenject가 Scene을 시작하기 전 다양한 종속성을 설정하는 시작지점이다. Zenject Scene에 콘텐츠를 추가하기 위해 Zeneject에서 Installer라고 하는 항목을 작성해주어야 하고, 이를 통해 모든 종속성과 상호 관계를 선언해야 한다. NonLazy로 표시된 모든 종속성은 프로그램이 실행된 후 자동으로 생성되므로, 우리가 위에서 추가한 Greeter 클래스가 시작시 생성된다. 여기까지 완료한 후 Scene을 실행하면 아래처럼 로그가 뜰 것이다.

 

자, 이제 주입을 진행해보자. 컨테이너에 대한 종속성(Dependency)을 선언하는 방법은 여러가지가 존재한다. 이러한 종속성을 클래스에 주입하는 방법은 생성자 주입, 필드 주입, 속성 주입, 메소드 주입 등이 존재한다. 

  • 생성자 주입
public class Foo {
	IBar _bar;
    public Foo(IBar bar) {
    	_bar = bar;
    }
}
  • 필드 주입
public class Foo {
	[Inject]
	IBar _bar;
}

필드 주입은 생성자가 호출된 직후 발생한다. 속성으로 표시된 모든 필드는 [Inject] 컨테이너에서 조회되고 값이 제공된다. 이러한 필드는 private, public 일 수 있다. 

  • 속성 주입
public class Foo {
	[Inject]
    public IBar Bar {
    	get;
        private set;
    }
}

속성 주입은 C# 속성에 적용된다는 점을 제외하면 필드 주입과 동일하게 작동한다. 

  • 메소드 주입
public vlass Foo {
	IBar _bar;
	Qux _qux;
    
    [Inject]
    public void Init(IBar bar, Qux qux) {
    	_bar = bar;
        _qux = qux;
    }
}

메소드 주입은 생성자 주입과 유사하게 동작한다. 

 

주입을 진행했으니, DI 컨테이너에 매핑을 등록해보자. Zenject에서는 이를 Binding이라고 지칭한다. 추상화와 구체적인 유형 사이에 바인딩을 생성하기 때문이다. Foo라는 생성자를 우선 주입했다고 생각해보자. 그럼 이 클래스를 아래와 같이 연결 할 수 있다. 

Container.Bind<Foo>().AsSingle();
Container.Bind<IBar>().To<Bar>().AsSingle();

이는 Zenject에게 Foo 유형의 종속성이 필요한 모든 클래스가 동일한 인스턴스를 사용해야 하며, 필요할 때 자동으로 생성할 것임을 알려준다. 마찬가지로 IBar 인터페이스가 필요한 모든 클래스에는 Bar 유형의 동일한 인스턴스가 제공된다. 바인드 명령어의 형식 및 문은 깃허브 튜토리얼에 아주 잘 나와있으므로 한번씩 참고하면 된다. 

 

이번엔 아까와 다르게 Inspector를 사용해 설정을 구성해보자. 우선 아래와 같이 Foo 클래스를 생성하고 코드를 작성한다.

using System;
using UnityEngine;
using Zenject;

public class Foo : ITickable
{
    private readonly Settings _settings;

    public Foo(Settings settings)
    {
        _settings = settings;
    }
    public void Tick()
    {
        Debug.Log("Speed : " + _settings.Speed);
    }

    [Serializable]
    public class Settings
    {
        public float Speed;
    }
}

이를 Installer에서 아래와 같이 수행해 사용해준다. 

using Zenject;

public class TestInstaller : MonoInstaller
{
    public Foo.Settings FooSettings;
    public override void InstallBindings()
    {
        Container.BindInstance(FooSettings);
        Container.BindInterfacesTo<Foo>().AsSingle();
    }
}

자, 이 코드를 작성한 Scene을 실행해보면 Speed가 로그에 계속해서 뜰 것이다. Installer에서 바꿔준 값은 곧바로 적용되어 로그에도 확인이 된다. 


우선은 Zenject에 대해 기본적인 문법을 이해하기 위하여 공식 깃허브를 통해서 무작정 코드를 작성하고, 테스트를 진행해보았다. 다음 챕터에서는 본격적으로 Zenject를 사용하여 각종 실습을 진행해볼 예정이다. 본 실습을 통해서 Zenject의 함수들과 관련된 파라미터에 대해 제대로 알아보고자 한다.