Reflex 의 등록은 그 줄에서 리졸버로 굳는다
Esc

    Reflex 의 등록은 그 줄에서 리졸버로 굳는다

    –Unity라이브러리DI
    builder.RegisterType(typeof(Sword), new[] { typeof(IWeapon) }, Lifetime.Singleton, Resolution.Lazy);
    builder.RegisterType(typeof(Bow),   new[] { typeof(IWeapon) }, Lifetime.Singleton, Resolution.Lazy);
    // 둘 중 무엇이 주입될까? 틀린 등록은 언제 드러날까?

    설치자가 하는 일은 이런 줄을 늘어놓는 것이 전부입니다. 그런데 이 줄이 끝나는 순간 이미 많은 것이 정해집니다.

    Reflex 의 등록은 호출한 그 줄에서 리졸버 하나로 굳고, Build() 는 그것을 부모 표 뒤에 덧붙일 뿐입니다. 기준 판본은 14.3.1 입니다.

    등록 한 줄이 리졸버와 계약 검사를 끝낸다

    등록 메서드는 RegisterType, RegisterValue, RegisterFactory 셋입니다. 수명은 Lifetime(Singleton, Transient, Scoped), 생성 시점은 Resolution(Lazy, Eager) 인자로 매번 적습니다. 값 등록만 늘 싱글턴이라 둘 다 받지 않습니다.

    호출한 줄에서 수명별 리졸버를 new 하고, 계약마다 IsAssignableFrom 으로 구현이 맞는지 봅니다. 통과하면 리졸버와 계약 배열 둘만 담은 Binding 이 목록에 붙습니다.

    틀린 계약은 Build() 를 기다리지 않고 그 줄에서 멈춥니다. 대신 검사가 IsAssignableFrom 하나라서 만들 수 있는 타입인지는 보지 않습니다. 어느 생성자를 쓸지도 처음 만들 때 정합니다. Binding 에 이름, 조건, 생성자 인자를 적을 자리가 없으니 그런 기능도 없습니다.

    Build 는 부모 표를 복사하고 그 뒤에 덧붙인다

    Build() 는 빈 표를 만들고, 부모가 있으면 부모 표의 계약 칸마다 목록을 새로 복사한 뒤, 자기 등록을 목록 뒤에 붙입니다. 그다음 컨테이너를 만들어 부모의 자식 목록에 넣고, Eager 를 만들고, 빌더를 비웁니다.

    • 복사하는 것은 목록이고 리졸버 객체는 부모와 자식이 공유합니다. 부모의 싱글턴을 자식에서 꺼내도 같은 인스턴스가 나오는 이유입니다.
    • Resolution 을 읽는 곳은 Build() 뿐입니다. Eager 는 빌드가 대신 한 번 불러 주는 것이고, Scoped + Eager 는 자손을 빌드할 때마다 그 자손 몫이 하나씩 만들어집니다.
    • 빌드가 끝나면 표는 바뀌지 않습니다. 같은 빌더에 등록을 더하고 다시 Build() 하면 새 컨테이너가 하나 더 생길 뿐입니다.

    같은 계약을 두 번 등록하면 목록에 쌓인다

    덧붙일 때 이미 있는지 보지 않습니다. 그래서 꺼내는 방법마다 결과가 갈립니다.

    생성자 주입과 [Inject] 는 Resolve 를 쓰므로 경고 없이 마지막 등록이 들어갑니다. 반대로 등록 하나에 계약을 여러 개 적으면 그 계약들이 리졸버 하나를 나눠 써서, Sword 를 { IWeapon, IDamager } 싱글턴으로 등록하면 두 계약이 같은 인스턴스를 줍니다.

    주의할 점

    1. 같은 계약을 두 설치자에 흩어 두지 않는다

    안 지키면 생기는 문제. 한 스코프 안의 설치자는 하이어라키 순서대로 불립니다. 두 설치자가 같은 계약을 등록하면 나중 것이 조용히 이기니, 씬에서 오브젝트 순서만 바꿔도 다른 구현이 주입됩니다. 부모에 있는 계약을 자식이 다시 등록해도 같습니다.

    이렇게 쓴다. 하나만 있어야 하는 서비스는 Single<T>() 로 꺼내 둘째 등록을 바로 잡습니다. 덮어쓰는 것이 의도라면 그 등록을 한 설치자에 모읍니다.

    2. 등록 검사가 릴리스 빌드에도 남는다고 보지 않는다

    안 지키면 생기는 문제. 빈 계약 배열과 Transient + Eager 를 막는 검사는 UnityEngine.Assertions.Assert 라서 비개발 빌드에서 빠집니다. 에디터에서 AssertionException 이던 실수가 릴리스에서는 조용히 지나가, 빈 계약 배열로 등록한 타입은 UnknownContractException 으로 끝나고 Transient + Eager 는 Lazy 로 돕니다. 계약 불일치 검사는 평범한 throw 라 남습니다.

    이렇게 쓴다. 등록 코드는 에디터 플레이나 EditMode 테스트에서 한 번씩 다 돌려 봅니다. 계약 배열을 코드로 만들어 넘기면 넘기기 전에 직접 비었는지 봅니다.

    3. Eager 생성자에서 실패할 수 있는 일을 하지 않는다

    안 지키면 생기는 문제. Build() 는 컨테이너를 부모의 자식 목록에 넣은 뒤에 Eager 를 만듭니다. Eager 생성자가 던지면 호출한 쪽은 컨테이너를 받지 못하는데, 부모에는 반쯤 만든 컨테이너가 매달린 채 남습니다. 에디터와 Mono·IL2CPP 개발 빌드 플레이어 모두 그랬고, 그 컨테이너는 부모가 해제될 때에야 치워졌습니다.

    이렇게 쓴다. Eager 타입의 생성자는 받은 의존을 필드에 두는 데서 그치고, 파일이나 네트워크처럼 실패할 수 있는 일은 따로 부르는 초기화 메서드로 옮깁니다.

    정리

    Reflex 의 등록은 그 줄에서 리졸버로 굳고, Build() 는 부모 표 뒤에 덧붙입니다.

    • 등록 한 줄이 수명별 리졸버를 만들고 계약을 검사합니다. 만들 수 있는 타입인지는 보지 않습니다.
    • Build() 는 부모 표를 칸마다 복사하고 자기 등록을 뒤에 붙입니다. 리졸버는 부모와 자식이 공유하고, Eager 는 빌드가 대신 부르는 것입니다.
    • 중복 등록은 목록에 쌓여 Resolve 는 조용히 마지막, Single 은 예외, All 은 전부입니다.