DI
글 29개
Reflex 의 비용은 조회가 아니라 스코프 생성과 IL2CPP 생성에 있다
Reflex 의 조회는 컨테이너 깊이와 상관없이 쌉니다. 그 값을 부모 크기에 비례하는 스코프 생성과 IL2CPP 의 생성마다 부르는 리플렉션에서 치르고, 락이 없어 여러 스레드의 첫 조회는 싱글턴을 여럿 만들 수 있습니다.
Reflex 는 실패를 예외로만 알린다
주입이 안 될 때 Reflex 의 로그에는 원인이 없습니다. 로그는 수명 이벤트만 찍고, 조회와 주입의 실패는 겹겹이 감싼 예외로만 나오며, 자세한 진단은 REFLEX_DEBUG 심볼로 컴파일할 때만 붙습니다.
Reflex 의 씬 주입은 가장 먼저 도는 Awake 하나에서 끝난다
씬의 Awake 에서 [Inject] 필드를 바로 쓸 수 있는 것은 실행 순서 -10억인 ContainerScope.Awake 가 씬 전체를 먼저 주입하기 때문입니다. 그 한 번 밖에서 생긴 오브젝트와 씬 해제 순서는 쓰는 쪽이 챙겨야 합니다.
Reflex 는 생성자와 [Inject] 멤버를 두 단계로 채운다
MonoBehaviour 는 생성자로 의존을 받을 수 없습니다. Reflex 는 생성자 단계와 [Inject] 멤버 단계를 나눠 채우고, 소스 제너레이터는 뒤쪽만 대신 씁니다. 두 단계의 규칙이 기본값, 할당, 순환 참조에서 차이를 만듭니다.
Reflex 의 인스턴스는 컨테이너가 아니라 리졸버에 산다
같은 등록인데 자식 컨테이너에서 꺼내면 다른 객체가 나오기도 합니다. Reflex 는 조회를 자기 표 한 번으로 끝내고, 인스턴스는 부모와 자식이 공유하는 리졸버 안에 두며, 해제는 자식부터 만든 역순으로 합니다.
Reflex 의 등록은 그 줄에서 리졸버로 굳는다
Reflex 는 RegisterType 을 부르는 줄에서 수명별 리졸버를 만들고 계약을 검사합니다. Build 는 부모 표를 복사해 그 뒤에 덧붙이고 Eager 를 만들 뿐이라, 같은 계약을 두 번 등록하면 조용히 목록에 쌓입니다.
Reflex 는 조립을 빠르고 작은 등록 표 하나로 바꾼다
싱글턴과 FindObjectOfType 은 Awake 순서에 묶이고, 손으로 new 하는 조립 코드는 클래스 수만큼 길어집니다. Reflex 는 빠르고 작지만 빠진 것 없는 컨테이너를 목표로, 조립을 등록 표로 바꾸고 객체를 만들어 넣는 데서 멈춥니다.
Zenject 의 조회 비용은 딕셔너리가 아니라 파이프라인에 있다
이미 만든 싱글턴 하나를 Zenject 로 꺼내는 데 1 µs 안팎이 듭니다. 딕셔너리 조회는 그중 100 ns 이고 나머지는 컨텍스트 대여, 조상 확정, 거리 계산, 순환 감지입니다. 조회는 프레임 밖으로 빼고 숫자는 목표 빌드에서 잽니다.
Zenject 의 검증은 생성자를 부르지 않고 그래프만 따라간다
빠뜨린 바인딩은 Zenject 에서 처음 조회할 때 터집니다. 검증은 설치자를 돌리고 의존 그래프를 따라가되 생성자 대신 표식을 만듭니다. 기본 설정은 시작 때 닿는 것만 보고, 오류를 예외 대신 로그로 모읍니다.
Zenject 의 풀 반납은 쉬는 객체 수만큼 비싸다
Zenject 의 MemoryPool.Despawn 은 이중 반납을 막으려고 쉬는 객체 스택을 끝까지 훑어, 풀이 클수록 반납이 느려집니다. 이 검사는 기본 설정에서 출시 빌드에도 남고, ZEN_STRIP_ASSERTS_IN_BUILDS 로 뺄 수 있습니다.
Zenject 의 진입점은 MonoKernel 의 Update 안에서 돈다
MonoBehaviour 가 아닌 객체의 Tick 을 Zenject 는 컨테이너마다 붙는 MonoKernel 의 Update 에서 부릅니다. 그래서 순서는 .meta 의 실행 순서와 BindExecutionOrder 숫자가 정하고, Tick 하나가 던지면 그 프레임의 뒤 순서가 전부 건너뜁니다.
Zenject 의 주입은 리플렉션으로 읽은 타입 정보로 돈다
Zenject 는 타입마다 한 번 생성자와 [Inject] 멤버를 리플렉션으로 읽어 캐시하고, 그 정보대로 생성자, 필드, 메서드 순으로 채웁니다. 생성자가 여럿이면 인자가 가장 적은 것을 조용히 고르고, IL2CPP High 스트리핑에서는 필드가 예외 없이 비었습니다.
Zenject 의 수명은 Context 계층이 정한다
게임 전체에서 하나, 씬마다 하나, 적마다 하나는 Zenject 에서 Context 계층으로 정합니다. 자식마다 하나는 바인딩 문장을 자식에서 다시 확정해 만들고, 해제는 IDisposable 계약으로 건 것만 커널의 OnDestroy 에서 역순으로 합니다.
Zenject 의 Provider 는 인스턴스를 먼저 내주고 주입은 나중에 한다
Zenject 에서 필드끼리 서로를 가리키면 풀리고 생성자끼리 가리키면 예외가 나는 이유는 Provider 계약에 있습니다. 인스턴스를 캐시에 먼저 넣고 멤버 주입은 따로 돌려주므로, 필드 순환의 상대는 주입 도중 덜 채워져 있습니다.
Zenject 의 조회는 가까운 컨테이너와 조건 있는 바인딩을 고른다
같은 계약이 부모와 자식에, 또는 한 컨테이너에 둘 있으면 Zenject 는 무엇을 줄까요. 단일 조회는 가까운 컨테이너, 조건 있는 쪽 순으로 하나를 고르고 같은 등급이 둘이면 예외, 목록 조회는 전부 모읍니다.
Zenject 의 Bind 문장은 다음 문장이 시작될 때 확정된다
Zenject 의 Container.Bind<T>().AsSingle() 한 줄은 그 자리에서 등록되지 않고 큐에 쌓입니다. 다음 Bind 가 앞 문장을 Provider 로 확정하므로, 스코프 누락 같은 실수는 잘못이 없는 다음 줄에서 터집니다.
Zenject 는 기능을 다 갖춘 채 new 로 잇던 조립을 대신한다
싱글턴과 FindObjectOfType 은 의존을 숨기고, new 로 조립하면 MonoBehaviour 에 넘길 길이 없습니다. Zenject 는 유니티에 DI 가 없던 때 기능을 다 갖춘 쪽으로 나와, 설치 파일의 바인딩으로 씬 시작 때 조립과 주입을 끝냅니다.
VContainer 의 CreateScope 스코프는 씬이 바뀌어도 닫히지 않는다
자식 스코프는 CreateChild 로 만들면 게임오브젝트라 씬과 함께 닫히고, CreateScope 로 만들면 순수 C# 객체라 씬이 바뀌어도 부모를 닫아도 살아 있습니다. 씬 단위는 CreateChild, 직접 관리할 것만 CreateScope 로 만듭니다.
VContainer 의 수명은 받는 쪽이 정한다
Singleton 이 Scoped 를 생성자로 받으면 첫 인스턴스가 필드에 박혀 전투가 바뀌어도 그대로입니다. 주는 쪽 수명을 바꾸거나 싱글턴 팩토리를 넣어도 안 풀리고, 받는 쪽을 Scoped 로 내려야 풀립니다.
VContainer 는 자기가 처음 만든 것만 해제한다
Transient 로 등록한 IDisposable 은 요청마다 핸들을 흘립니다. VContainer 는 이번에 처음 만들었고, IDisposable 이고, 자기가 만든 것만 해제 목록에 올리고, 그 목록을 생성 역순으로 닫습니다.
VContainer 는 모노비헤이비어를 꺼 둔 채로 주입하고 켠다
프리팹을 복제한 뒤 주입하면 Awake 에서 주입 필드가 널입니다. VContainer 는 새로 만드는 컴포넌트를 꺼진 채 복제해 주입한 뒤 켜고, 씬에 있는 것은 스코프의 Awake 안에서 빌드 직후 찾아 채웁니다.
VContainer 의 진입점은 PlayerLoop 에 꽂힌다
순수 C# 클래스가 MonoBehaviour 없이 매 프레임 호출을 받습니다. 스코프마다 디스패처가 인터페이스별로 진입점을 모아 플레이어 루프에 꽂은 러너에 올리고, 자식 스코프는 부모의 싱글턴 진입점만 걸러 냅니다.
VContainer 에서는 누가 만드느냐가 무엇을 받느냐를 정한다
전투 스코프에 다시 등록했는데 어떤 객체는 새 것을, 어떤 객체는 옛것을 받습니다. VContainer 는 수명에 따라 객체를 만드는 스코프가 정해지고, 생성자 인자도 그 만든 스코프에서 찾기 때문입니다.
VContainer 의 등록은 빌드할 때 굳는다
Register<IWeapon, Sword>() 뒤에 Resolve<Sword>() 가 실패하고, 빠진 등록은 빌드가 알려 주지 않습니다. VContainer 는 빌드 때 등록을 불변 테이블로 굳히고, 그때 검사하는 것은 순환 참조뿐입니다.
VContainer 스코프는 부모를 못 찾으면 조용히 기다린다
애디티브로 자식 씬을 먼저 열면 그 씬의 Awake 와 Start 가 주입 없이 돕니다. 스코프는 부모를 여섯 단계로 찾고, 못 찾으면 로그 없이 대기열에 누웠다가 부모가 빌드될 때 깨어나기 때문입니다.
VContainer 는 스코프의 Awake 안에서 컨테이너를 세운다
등록 두 줄을 적으면 진입점이 불리지만 언제 불리는지 모르면 유니티 Start 와 섞어 씁니다. VContainer 는 스코프의 Awake 안에서 등록을 모아 빌드하고, 진입점은 다음 프레임 플레이어 루프에서 부릅니다.
VContainer 는 유니티 DI 의 네 제약을 빠르고 작게 푼다
직접 new 하고 싱글턴으로 꺼내 쓰는 코드는 바꾸지도 테스트하지도 못합니다. 유니티 DI 는 생성자, 씬 수명, Awake, Update 네 제약을 넘어야 하고, VContainer 는 이것을 빠른 Resolve 와 최소 할당을 목표로 풉니다.
VitalRouter 는 자식 스코프에서도 라우터를 새로 만들지 않는다
씬 스코프에서 RegisterVitalRouter 로 붙인 로그 인터셉터가 씬을 다섯 번 불러오면 다섯 번 돕니다. 부모의 라우터를 공유하는데 스코프를 닫을 때 인터셉터는 떼지 않아서입니다. Isolated 를 켜면 쌓이지 않습니다.
MessagePipe 는 자식 스코프에서 꺼내도 나뉘지 않는다
MessagePipe 브로커의 기본 수명은 싱글턴이라, 자식 스코프에서 꺼낸 발행자와 구독자도 루트의 구독 목록 하나를 씁니다. 스코프 수명으로 등록해야 나뉘고, 그 스코프를 닫으면 구독이 한꺼번에 걷힙니다.