Zenject 는 기능을 다 갖춘 채 new 로 잇던 조립을 대신한다
Esc

    Zenject 는 기능을 다 갖춘 채 new 로 잇던 조립을 대신한다

    2026년 9월 30일 수정–Unity라이브러리DI
    public class ScoreLabel : MonoBehaviour
    {
        void Awake()
        {
            ScoreService.Instance.Add(0);   // Awake 순서가 어긋나면 Instance 가 아직 null
        }
    }

    점수 서비스 하나를 여러 곳에서 쓰려면 정적 필드에 걸거나, FindObjectOfType 으로 찾거나, 한 곳에서 new 로 만들어 생성자로 넘겼습니다. 정적 필드는 Awake 순서에 걸리고, new 로 조립하면 유니티가 만드는 MonoBehaviour 에는 넘길 길이 없습니다.

    Zenject 는 new 로 하던 조립을 설치 파일의 바인딩 문장으로 바꾸고, 씬이 시작될 때 컨테이너가 조립과 주입을 대신 끝내는 유니티용 의존성 주입 프레임워크입니다. 유니티에 DI 프레임워크가 없던 때 나와서, 설계는 강요하지 않고 쓸 만한 기능을 전부 넣어 두는 쪽을 골랐습니다. 기준 판본은 master 31f06cf(2021-04-06, 9.1.0)이고, 그 뒤로 새 커밋이 없습니다.

    조립은 컨테이너가 하고 의존은 선언에 드러난다

    같은 일을 네 방식으로 나란히 놓으면 차이는 “누가 잇느냐” 와 “의존이 어디에 보이느냐” 입니다.

    손 조립이 가장 정직하지만 객체가 늘수록 조립 코드가 길어지고 MonoBehaviour 에서 막힙니다. Zenject 는 그 조립 코드를 바인딩 문장으로 바꿔 기계에 맡기고, 생성자로 못 받는 컴포넌트에는 [Inject] 멤버로 넣습니다.

    유니티에 DI 가 없던 때 기능을 다 갖춘 쪽으로 나왔다

    README 의 내력 절은 동기를 짧게 적습니다. 유니티가 권하는 방식은 크고 유연한 코드베이스를 짜기에 맞지 않고, 게임 컴포넌트 사이 의존을 다루는 기본 방법이 어색하고 틀리기 쉽다는 것. 그리고 그때 유니티용 DI 프레임워크가 하나도 없어서 유니티 밖에서 DI 프레임워크를 써 본 저자가 만들었다는 것입니다.

    설계 철학은 아무것도 강요하지 않는다입니다. 가정은 객체 지향으로 짠다는 것 하나뿐이고, 기능은 많지만 전부 골라 쓰는 것입니다. 모든 클래스를 MonoBehaviour 로 짜고 [SerializeField] 자리에 [Inject] 만 써도 되고, MonoBehaviour 를 버리고 ITickable, IInitializable 같은 인터페이스로 흐름을 짜도 됩니다. README 의 기능 목록을 묶으면 이렇습니다.

    묶음 README 가 꼽는 기능
    주입 일반 클래스와 MonoBehaviour 모두, 생성자 · 필드 · 속성 · 메서드 주입
    바인딩 타입과 이름에 따른 조건부 바인딩, 선택적 의존, 규칙 기반 바인딩, 데코레이터, 열린 제네릭
    컨테이너 구조 하위 컨테이너, 씬 부모, 프로젝트 전역 바인딩, 씬 사이 데이터 전달
    실행 중 생성 팩토리, 메모리 풀, ZenAutoInjecter
    검증과 테스트 에디터에서 객체 그래프 검증, 단위 · 통합 · 씬 테스트, Moq 자동 목
    비용 줄이기 빌드된 어셈블리를 고쳐 리플렉션을 없애는 리플렉션 베이킹

    대가도 README 가 직접 적습니다. 정해진 길이 없으니 새로 온 개발자는 앞사람이 고른 구조부터 익혀야 합니다. 그리고 이 기능들을 받치는 단계를 쓰지 않는 조회도 매번 지나갑니다. 이미 만든 싱글턴 하나를 꺼내는 데 Mono 개발 빌드 플레이어 1,161 ns, IL2CPP 958 ns 가 들었고, 그중 딕셔너리 조회는 100 ns 안팎이었습니다.

    설치와 주입은 SceneContext 의 Awake 안에서 끝난다

    씬에는 SceneContext 가 하나 놓입니다. 이 컴포넌트의 실행 순서가 .meta 에 -9999 로 적혀 있어 같은 씬의 다른 스크립트보다 먼저 Awake 가 돌고, 그 한 번 안에서 컨테이너를 만들고 설치하고 주입까지 끝냅니다.

    그래서 실행 순서 0 인 컴포넌트는 자기 Awake 에서 이미 주입된 필드를 씁니다. 아래 예제의 ScoreLabel 에 Awake 를 더해 _score == null 을 찍자 False 였습니다.

    가장 짧은 예제

    생성자로 받는 일반 클래스와 필드로 받는 씬 컴포넌트를 함께 둡니다.

    using UnityEngine;
    using Zenject;
    
    public class ScoreService
    {
        public int Score { get; private set; }
        public void Add(int v) => Score += v;
    }
    
    public class GameFlow : IInitializable
    {
        readonly ScoreService _score;
        public GameFlow(ScoreService score) => _score = score;   // 생성자 주입
        public void Initialize() => _score.Add(10);              // SceneKernel 의 Start 에서 불린다
    }
    
    public class ScoreLabel : MonoBehaviour
    {
        [Inject] ScoreService _score;                             // 필드 주입
        void Start() => Debug.Log($"점수 {_score.Score}");
    }
    
    public class GameInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
            Container.Bind<ScoreService>().AsSingle();
            Container.BindInterfacesAndSelfTo<GameFlow>().AsSingle();
        }
    }

    씬에 GameObject > Zenject > Scene Context 로 SceneContext 를 만들고, GameInstaller 를 붙인 뒤 인스펙터의 Installers 목록에 넣고, 아무 오브젝트에 ScoreLabel 을 붙입니다.

    • Bind<ScoreService>().AsSingle() 은 “요구하면 하나를 만들어 두고 계속 그것을 준다” 입니다.
    • BindInterfacesAndSelfTo<GameFlow>() 는 GameFlow 와 그것이 구현한 IInitializable 을 한 문장에 묶습니다. 커널이 IInitializable 목록을 모을 때 GameFlow 가 만들어집니다.
    • ScoreLabel 은 설치 전에 대기열에 들어가 ResolveRoots 에서 채워집니다.

    로그는 점수 10 입니다. SceneKernel 의 실행 순서가 -9997 이라 Initialize 를 부르는 커널의 Start 가 ScoreLabel.Start 보다 먼저 돕니다.

    주의할 점

    1. 고쳐 줄 원본이 없다는 것을 전제로 둔다

    안 지키면 생기는 문제. master 의 마지막 커밋은 2021-04-06 입니다. 새 유니티 판에서 버그를 만나도 원본에서 고친 판이 나오지 않습니다. 소스를 복사해 넣은 프로젝트에서 누군가 기록 없이 한 줄을 고치면 프로젝트의 Zenject 가 9.1.0 이 아닌 무언가가 됩니다.

    이렇게 쓴다. 넣을 때 기준 커밋(31f06cf)과 뺀 OptionalExtras 폴더를 기록합니다. 사본을 고쳐야 하면 그 수정만 따로 커밋해 원본과의 차이를 언제든 볼 수 있게 합니다.

    2. 실행 중에 Instantiate 한 프리팹은 주입되지 않는다

    안 지키면 생기는 문제. 씬 시작 때 주입되는 것은 SceneContext 가 설치 전에 모아 둔 씬 오브젝트뿐입니다. 실행 중에 GameObject.Instantiate 로 만든 프리팹의 [Inject] 필드는 아무도 채우지 않고, 예외 없이 null 로 돌다가 나중에 NullReferenceException 으로 드러납니다.

    이렇게 쓴다. 실행 중에 만드는 프리팹은 주입받은 DiContainer 의 InstantiatePrefab 이나 팩토리로 만듭니다. 프리팹에 ZenAutoInjecter 컴포넌트를 붙이는 방법도 있습니다.

    3. 아무도 요구하지 않는 바인딩은 만들어지지 않는다

    안 지키면 생기는 문제. ResolveRoots 가 미리 만드는 것은 NonLazy 바인딩뿐입니다. 시작할 때 일을 하려고 Bind<Greeter>().AsSingle() 만 적으면, 아무도 Greeter 를 요구하지 않는 한 생성자가 한 번도 불리지 않습니다.

    이렇게 쓴다. 시작할 때 할 일은 IInitializable.Initialize 에 두고 BindInterfacesAndSelfTo 로 바인딩합니다. 생성 자체가 목적이면 .NonLazy() 를 붙입니다.

    정리

    Zenject 는 new 로 하던 조립을 바인딩 문장으로 바꾸고, SceneContext.Awake 안에서 컨테이너가 조립과 주입을 끝내는 프레임워크이고, 필요할 만한 기능을 전부 갖춰 두었습니다.

    • 싱글턴 정적 필드와 FindObjectOfType 이 숨기던 의존이 생성자와 [Inject] 멤버로 드러나고, new 로 못 만드는 컴포넌트에도 넣을 길이 생깁니다.
    • 설계는 강요하지 않고 조건부 바인딩, 하위 컨테이너, 풀, 검증까지 다 넣었습니다. 그 대가로 쓰지 않는 기능의 단계도 조회마다 지나갑니다.
    • 씬 하나의 설치와 주입은 실행 순서 -9999 인 SceneContext.Awake 에서 끝나고, 진입점은 SceneKernel 의 Start 와 Update 가 부릅니다.
    • 실행 중에 만든 프리팹과 아무도 요구하지 않는 바인딩은 저절로 채워지거나 만들어지지 않습니다.