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

위의 사진에서 상점 객체는 연필 객체에 의존한다. 과목이 없으면 교수는 존재할 수 없다. 이러한 의존 관계를 객체가 아닌 인터페이스로 바꾸는 것이 DI(Dependency Injection)이다.
외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴이다. 장점은 아래와 같다.
객체가 스스로 의존성을 생성하는 대신, 외부에서 제공받도록 하여 객체의 책임 범위를 명확히 한다. 필요에 따라 다른 객체로 쉽게 교체할 수 있게 해준다.
객체가 다른 객체를 필요로 할 때, 그 객체를 직접 생성하지 않고 외부의 다른 객체(혹은 프레임워크)로부터 주입받아 사용하는 방식이다.
의존성 생성과 관리에 대한 책임을 객체 내부에서 외부로 위임하여, 객체의 역할을 명확하게 분리한다.
제어의 역전. Zenject와 같은 프레임워크에서 DI를 구현하며, IoC 컨테이너가 객체들의 생성 및 의존성 연결을 자동으로 처리한다.
객체 간의 의존성을 인터페이스 중심으로 관리하여 클래스 간의 결합도를 낮추고, 런타임 시점에 의존 객체를 동적으로 주입하여 유연성을 확보한다.
의존 객체를 실제 구현체가 아닌 Mock 객체 등으로 쉽게 주입할 수 있어 단위 테스트를 효과적으로 수행할 수 있다.
객체가 특정 구현체에 종속되지 않고 인터페이스에 의존하기 때문에, 필요에 따라 다양한 구현체를 쉽게 교체하여 코드의 재사용성과 유지보수성을 높일 수 있습니다.
객체가 의존성 객체를 직접 생성하고 관리하는 복잡한 코드가 사라져 코드의 가독성이 좋아진다.
의존성 주입을 통해 객체 간의 관계를 명확히 하여, 불필요한 순환 의존성 발생을 방지할 수 있다.

DI Container가 의존성을 전부 들고 있다가, 객체가 생성될 시 해당 객체가 필요로 하는 의존성을 주입하는 방식(IoC)이다.
Zenject에 의해서 발생되는 의존성 주입은 유니티 라이프 사이클의Awake()보다도 빨리 수행된다.
객체를 참조하는 데에 있어 다음과 같이 작성했다.

의존성 직접 주입(객체 참조) 시 문제점은 아래와 같다.
IAudioService가 없을 경우, 정상 동작하지 않는다.다른 기능을 테스트 해보고자 할 때,
IAudioService객체 참조 선언 스크립트를 직접 수정해야 한다.이는 다른 모든
IAudioService를 참조하고자 하는 스크립트들도 마찬가지다.
이러한 문제들을 IoC 컨테너이너인 Zenject 프레임워크를 통해서 해결한다.
Context는 Installer를 통해, 의존성이 필요한 객체들에게 인터페이스를 통해 외부에서 주입한다.
Scene Context는 생성된 현재 씬에서만 유효하다.
Installer를 필요로 한다.

하이어라키 창에서 위의 경로를 통해 생성이 가능하다.
의존성 주입 도구이다. 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)에 의해서 의존성이 주입된다.
모노비헤이비어를 상속받는 스크립트의 경우에는 [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);
}
}
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을 가지고 있다.
현재 프로젝트 또는 현재 씬에서 Zenject에 의한 의존성 주입에 문제가 없는지 체크하는 기능이 있다.

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

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