Reflex 는 조립을 빠르고 작은 등록 표 하나로 바꾼다
Esc

    Reflex 는 조립을 빠르고 작은 등록 표 하나로 바꾼다

    2026년 9월 30일 수정–Unity라이브러리DI
    public class Player : MonoBehaviour
    {
        void OnHit() => ScoreService.Instance.Add(10);   // Instance 가 아직 null 이면?
    }

    ScoreService 의 Awake 가 Player 보다 늦게 돌면 Instance 는 비어 있습니다. FindObjectOfType 으로 바꿔도 부를 때마다 씬을 뒤지고, 같은 타입이 둘이면 어느 쪽이 잡힐지 모릅니다.

    Reflex 는 “무엇을 무엇으로 만들지” 를 등록 표로 굳혀 두고, 필요한 자리에 객체를 만들어 넣는 데서 멈추는 유니티용 의존성 주입(DI) 컨테이너입니다. 빠르고 작게, 그래도 DI 에 필요한 것은 빠짐없이 갖추는 것이 목표입니다. 기준 판본은 14.3.1(커밋 5542338)입니다.

    컨테이너 없이 잇는 세 방법은 조립 순서에 묶인다

    같은 연결을 세 방식과 Reflex 로 나란히 놓았습니다.

    싱글턴 정적 필드와 FindObjectOfType 은 쓰는 쪽을 구체 클래스와 Awake 순서에 묶고, MonoBehaviour 가 아닌 평범한 C# 클래스에는 쓸 수 없습니다. 손으로 쓴 생성자 주입은 이 문제를 다 풀지만, 클래스가 수십 개가 되면 순서를 맞춰 new 를 늘어놓는 조립 코드가 그 자체로 짐이 됩니다. 씬에 놓인 MonoBehaviour 는 유니티가 만들어서 생성자로 받을 수도 없습니다.

    Reflex 는 그 조립을 등록 한 줄로 바꿉니다. 생성자 인자는 표에서 찾아 채우고, 씬의 MonoBehaviour 는 [Inject] 를 붙인 필드·속성·메서드로 받게 합니다.

    부품은 넷이고 매 프레임 도는 것은 없다

    등록 한 줄이 필드에 들어간 값이 되기까지 지나는 타입은 넷입니다.

    부품 하는 일
    ContainerBuilder 등록을 받는다. 호출 한 번마다 Binding 하나
    IResolver 무엇을, 어떤 수명으로, 어떻게 만드는지 든 전략 객체. 구현이 일곱 개
    Container 계약 → 리졸버 목록 표
    ConstructorInjector, AttributeInjector 생성자를 부르고 [Inject] 멤버를 채운다

    리졸버는 등록하는 순간 만들어지고, Build() 는 그것을 표로 옮겨 담기만 합니다. 조회는 자기 표 하나만 봅니다. 그리고 매 프레임 불리는 인터페이스, 시작할 때 한 번 불리는 진입점, 오브젝트 풀, 등록 검증 단계가 없습니다. 컨테이너는 객체를 만들어 넘기는 데서 손을 뗍니다.

    빠르고 작지만 빠진 것 없는 쪽을 내건다

    README 는 Reflex 를 “아주 빠르고, 작지만 빠진 것 없는 유니티용 DI 라이브러리” 라고 소개합니다. 목적으로는 읽기 쉬운 코드, 테스트하기 쉬운 구조, 커져도 버티는 구성을 들고, 가장 앞에 속도와 할당을 내세웁니다. 그 밖의 주장은 소스에서 이런 모양입니다.

    주장 소스에서 나타난 모양
    Contract Table 조회 표가 Dictionary<Type, List<IResolver>> 다. 계약 하나에 리졸버 목록이 붙는다
    Immutable Container Build() 가 표를 한 번 만들고 빌더를 비운다. 컨테이너에는 등록 API 가 없다
    AOT Support 생성자 호출기가 백엔드마다 다르다. IL2CPP 에서는 ConstructorInfo.Invoke, Mono 에서는 식 트리 컴파일
    Source Generated [SourceGeneratorInjectable] 을 붙인 partial 클래스의 [Inject] 멤버 채우기만 대신 쓴다

    “작다” 는 앞 절의 없는 것들입니다. 진입점, 풀, 검증 단계를 빼서 조회가 표 한 번과 리졸버 호출로 끝납니다. 실제로 이미 만든 싱글턴 조회는 컨테이너 깊이 0 이든 8 이든 Mono 163 ns, IL2CPP 243 ns 로 같았습니다.

    계약 표의 키는 Type 하나이고 값은 목록입니다. Resolve<T>() 는 목록의 마지막을, All<T>() 는 전부를, Single<T>() 는 하나가 아니면 예외를 줍니다. 이름이나 키를 붙일 자리는 없습니다. “불변” 은 표의 구조까지이고, 싱글턴을 담는 리졸버 필드와 타입 캐시는 락 없이 바뀝니다.

    가장 짧은 예제

    컨테이너는 두 층입니다. 앱 전체에 하나인 루트 컨테이너와, ContainerScope 가 놓인 씬마다 하나씩 생기는 씬 컨테이너. 씬 컨테이너는 루트를 부모로 삼습니다.

    public interface IScoreService { int Score { get; } void Add(int value); }
    
    public class ScoreService : IScoreService
    {
        public int Score { get; private set; }
        public void Add(int value) => Score += value;
    }
    
    // RootScope 프리팹에 붙인다
    public class RootInstaller : MonoBehaviour, IInstaller
    {
        public void InstallBindings(ContainerBuilder builder)
        {
            builder.RegisterType(typeof(ScoreService),
                new[] { typeof(IScoreService) },
                Lifetime.Singleton, Resolution.Lazy);
        }
    }
    
    public class ScoreView : MonoBehaviour
    {
        [Inject] IScoreService _score;
    
        void Start()
        {
            _score.Add(10);
            Debug.Log(_score.Score);   // 10
        }
    }
    • Create → Reflex → RootScope 로 만든 프리팹에 RootInstaller 를 붙이고, Resources/ReflexSettings 에셋의 RootScopes 목록에 넣습니다
    • 씬에는 GameObject → Reflex → SceneScope 로 씬 스코프를 하나 둡니다
    • 씬이 로드되면 씬 스코프의 Awake 가 가장 먼저 돌아 컨테이너를 만들고, 씬의 모든 MonoBehaviour 에 [Inject] 를 채웁니다. 그래서 ScoreView 는 Start 에서 바로 씁니다

    순수 C# 으로도 씁니다.

    using var container = new ContainerBuilder()
        .RegisterType(typeof(ScoreService), new[] { typeof(IScoreService) }, Lifetime.Singleton, Resolution.Lazy)
        .RegisterType(typeof(Player), Lifetime.Transient, Resolution.Lazy)
        .Build();
    
    var player = container.Resolve<Player>();   // Player(IScoreService) 에 ScoreService 가 들어간다

    등록 메서드는 RegisterType, RegisterValue, RegisterFactory 셋이고, 수명(Lifetime)과 생성 시점(Resolution)은 매번 인자로 적습니다. 기본값이 없습니다.

    주의할 점

    1. 이름으로 골라 받을 수 없다

    안 지키면 생기는 문제. 같은 계약으로 두 번 등록하면 목록에 쌓이고, 생성자 주입과 [Inject] 는 목록의 마지막을 받습니다. 계약 옆에 이름이나 키를 붙이는 자리가 없어서 “첫 번째 것” 을 골라 받을 방법이 없습니다.

    이렇게 쓴다. 구분해 받아야 하는 것은 README 안내대로 감싼 타입을 따로 만들어 서로 다른 계약으로 등록합니다.

    2. 시작이나 매 프레임 할 일을 컨테이너에 맡길 수 없다

    안 지키면 생기는 문제. Reflex 에는 컨테이너가 불러 주는 초기화 인터페이스나 Tick 같은 진입점이 없습니다. 서비스를 등록만 해 두면 누가 꺼내기 전까지 만들어지지도 않습니다.

    이렇게 쓴다. 시작할 때 한 번 할 일은 Resolution.Eager 로 등록해 Build() 안에서 만들어지게 하고, 매 프레임 할 일은 씬의 MonoBehaviour.Update 에서 주입받은 서비스를 부릅니다.

    정리

    Reflex 는 조립 코드를 등록 표 하나로 바꾸고, 객체를 만들어 넣는 데서 멈추는 빠르고 작은 DI 컨테이너입니다.

    • 싱글턴 정적 필드와 FindObjectOfType 은 Awake 순서에, 손으로 쓴 생성자 주입은 길어지는 조립 코드에 묶입니다. Reflex 는 그 조립을 등록 한 줄로 바꿉니다.
    • 부품은 빌더, 리졸버, 컨테이너, 주입기 넷입니다. 진입점도 풀도 검증 단계도 없습니다.
    • 계약 표는 Type → 리졸버 목록이라 Resolve 는 마지막, All 은 전부를 줍니다. 이름으로 고르는 기능은 없습니다.