Reflex 의 씬 주입은 가장 먼저 도는 Awake 하나에서 끝난다
Esc

    Reflex 의 씬 주입은 가장 먼저 도는 Awake 하나에서 끝난다

    –Unity라이브러리DI
    public class Player : MonoBehaviour
    {
        [Inject] IInput _input;
    
        void Awake() => _input.Enable();   // Awake 에서 바로 쓴다
    }

    컨테이너 없이 이 모양을 만들려면 Awake 안에서 FindObjectOfType<InputService>() 로 찾거나 InputService.Instance 가 먼저 채워져 있기를 바라야 합니다. 둘 다 누가 먼저 Awake 를 도느냐에 기댑니다.

    Reflex 는 씬에서 가장 먼저 도는 ContainerScope.Awake 하나에서 컨테이너를 만들고 씬 전체를 주입합니다. 그 뒤에 생기는 것은 스스로 챙기지 않습니다. 기준 판본은 14.3.1 입니다.

    실행 순서 -10억의 Awake 가 컨테이너를 만든다

    씬에 놓는 ContainerScope 는 [DefaultExecutionOrder(-1_000_000_000)] 입니다. 코드 속성이라 .meta 를 잃어도 유지되고, 사용자 스크립트가 이보다 앞설 일은 사실상 없습니다. 이 Awake 가 부르는 것이 씬 주입의 전부입니다.

    • 루트 컨테이너는 늦게 생깁니다. 첫 ContainerScope 가 깨어날 때 만들어지므로, 그 전인 BeforeSceneLoad 에서 Container.RootContainer 는 null 입니다. 씬 스코프가 없는 씬은 주입을 통째로 건너뜁니다.
    • 씬 컨테이너의 부모는 기본이 루트입니다. 씬끼리는 부모 자식이 되지 않습니다.
    • 한 씬에 하나인지는 빌드 뒤에 검사합니다. 둘째 스코프는 설치자와 Eager 를 다 돌린 뒤 예외를 내고, 그 컨테이너는 씬을 내려도 루트에 남습니다. 이 Mac의 세 조건 모두 씬을 내린 뒤 루트의 자식이 1개 남았습니다.

    씬 주입은 꺼진 것까지 모든 MonoBehaviour 를 훑는다

    SceneInjector 는 씬의 루트 오브젝트마다 GetComponentsInChildren<MonoBehaviour>(true) 로 비활성까지 모아 AttributeInjector.Inject 를 부릅니다. 아직 Awake 가 불리지 않은 오브젝트도 이때 주입되니, 씬에 놓인 오브젝트는 자기 Awake 에서 [Inject] 필드를 씁니다.

    [Inject] 가 없는 컴포넌트도 한 번씩 거치므로 비용은 씬의 MonoBehaviour 수를 따릅니다. [Inject] 없는 컴포넌트 N 개를 채운 씬에서 스코프를 켜는 한 줄을 쟀습니다(이 Mac, 개발 빌드 플레이어, 5회 중앙값).

    씬 Mono IL2CPP
    1종 1,000개 0.460 ms 0.619 ms
    1종 10,000개 5.19 ms 6.86 ms
    100종 각 1개, 첫 회 22.8 ms 17.3 ms
    100종 각 1개, 캐시가 찬 뒤 0.094 ms 0.121 ms

    캐시된 타입은 컴포넌트 하나에 0.4~0.7 µs 이고, 처음 보는 타입은 리플렉션으로 멤버를 훑느라 한 종에 0.2 ms 안팎입니다. 이 값은 전부 ContainerScope.Awake 한 칸에 몰립니다.

    실행 중에 만든 오브젝트는 주입이 활성화보다 먼저여야 한다

    Reflex 는 실행 중의 Instantiate 를 알 방법이 없습니다. 직접 GameObjectInjector.InjectRecursive(go, container) 를 부르는데, 활성 프리팹은 Instantiate 가 돌아오기 전에 이미 Awake 를 돕니다.

    이 Mac 의 에디터와 Mono·IL2CPP 개발 빌드 플레이어에서 세 경우 모두 탭과 같은 순서였습니다. 부모 없이 만든 오브젝트는 활성 씬에 들어가므로, 그 씬에 스코프가 없거나 DontDestroyOnLoad 로 옮긴 오브젝트면 gameObject.scene.GetSceneContainer() 가 예외를 던집니다. 스포너가 컨테이너를 주입받아 들고 있다가 넘기는 편이 안전합니다.

    부모를 바꾼 씬은 부모와 함께 비워진다

    씬 컨테이너는 sceneUnloaded 에서, 루트는 Application.quitting 에서 해제됩니다. ContainerScope.OnSceneContainerBuilding 에서 SetParent(Boot 컨테이너) 로 부모를 바꿀 수 있는데, 컨테이너의 Dispose 는 자식부터 해제하고 표를 비웁니다.

    Boot 를 올리고 부모를 Boot 로 건 Lobby 를 추가로 올린 뒤 Boot 만 내렸습니다. 이 Mac 의 세 조건 모두 Lobby 컨테이너에서 Lobby 자신의 서비스를 꺼내도 UnknownContractException 이었고, Lobby 스크립트가 미리 받아 둔 Boot 서비스와 Lobby 서비스가 둘 다 해제돼 있었습니다.

    주의할 점

    1. ContainerScope 는 씬마다 하나, 켜진 오브젝트에 둔다

    안 지키면 생기는 문제. 스코프가 꺼진 오브젝트에 있으면 Awake 가 없어 씬 전체가 주입되지 않고, 경고도 없습니다. 둘을 두면 둘째가 설치자와 Eager 를 다 돌린 뒤 예외를 내고, 그 컨테이너는 루트에 남습니다.

    이렇게 쓴다. 씬마다 GameObject › Reflex › SceneScope 로 만든 오브젝트 하나를 켜 둡니다. 첫 씬에도 빈 스코프를 두어 루트를 가장 먼저 세웁니다.

    2. 런타임에 만든 오브젝트는 꺼 둔 채로 주입한다

    안 지키면 생기는 문제. 활성 프리팹을 Instantiate 한 뒤 주입하면 Awake 와 OnEnable 은 이미 null 필드로 지나갔습니다. 반대로 씬에 놓인 오브젝트에 GameObjectSelfInjector 를 붙이면 씬 주입과 합쳐 [Inject] 메서드가 두 번 불립니다(이 Mac, 세 조건 모두 2회, 예외나 로그 없음).

    이렇게 쓴다. 꺼진 부모 아래 만들고 InjectRecursive 로 주입한 뒤 옮깁니다. GameObjectSelfInjector 는 실행 중에만 생기는 프리팹에 붙이고 모드는 기본값 Recursive 로 둡니다.

    3. 정적 훅은 BeforeSceneLoad 에서 += 로 걸고 쓰고 나면 뺀다

    안 지키면 생기는 문제. OnRootContainerBuilding 과 OnSceneContainerBuilding 은 공개 Action 필드라 = 로 대입하면 다른 구독이 사라집니다. 에디터는 AfterAssembliesLoaded 에서 두 훅을 비우므로, SubsystemRegistration 에서 건 훅은 빌드에서만 살아남습니다(이 Mac, 플레이어 True, 에디터 False). 부모를 바꾸는 씬 훅을 풀지 않으면 그 뒤 모든 씬이 같은 부모를 받습니다.

    이렇게 쓴다. 루트 훅은 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] 에서 += 로 겁니다. 씬 훅은 로드 직전에 +=, 그 로드의 completed 에서 -= 합니다.

    4. 부모를 바꾼 씬은 자식부터 내린다

    안 지키면 생기는 문제. 부모 씬을 먼저 내리면 떠 있는 자식 씬의 컨테이너가 함께 비워지고 서비스가 해제됩니다. 새 조회는 등록을 빠뜨렸을 때와 같은 UnknownContractException 이라 엉뚱한 곳을 찾게 됩니다.

    이렇게 쓴다. 추가로 쌓은 씬은 쌓은 역순으로 내립니다. 부모를 바꿀 필요가 없으면 기본값인 루트로 두고, 씬끼리 나눌 서비스는 루트에 올립니다.

    정리

    Reflex 의 씬 주입은 가장 먼저 도는 ContainerScope.Awake 하나에서 끝납니다.

    • 첫 스코프가 루트를 늦게 만들고, 루트를 부모로 씬 컨테이너를 만든 뒤 씬 전체를 주입합니다.
    • 비활성까지 모든 MonoBehaviour 를 훑어서 씬 오브젝트는 Awake 에서 필드를 쓰고, 비용은 컴포넌트 수를 따릅니다.
    • 실행 중에 만든 오브젝트와 씬 해제 순서는 쓰는 쪽 몫입니다. 꺼 둔 채 주입하고, 부모를 바꾼 씬은 자식부터 내립니다.