Zenject 의 진입점은 MonoKernel 의 Update 안에서 돈다
public class Spawner : IInitializable, ITickable, IDisposable
{
public void Initialize() { /* 첫 웨이브 준비 */ }
public void Tick() { /* 매 프레임 타이머 */ }
public void Dispose() { /* 구독 해제 */ }
}
Container.BindInterfacesAndSelfTo<Spawner>().AsSingle();
Spawner 는 MonoBehaviour 가 아닌데 매 프레임 Tick() 이 불립니다. 그러면 다른 MonoBehaviour.Update 보다 먼저일까, 나중일까요. 그리고 Tick 하나가 예외를 던지면 나머지는 어떻게 될까요.
Zenject 의 진입점은 컨테이너마다 붙는 MonoKernel 이라는 MonoBehaviour 의 콜백 안에서 돕니다. 새 루프 단계가 아니라 평범한 Update 하나라서, 앞뒤는 스크립트 실행 순서가 정하고 예외는 그 Update 를 빠져나갑니다.
커널은 유니티 콜백을 매니저에 넘긴다
MonoKernel 의 본문은 유니티 콜백 다섯 개를 매니저에 넘기는 것이 전부입니다.
| 커널의 콜백 | 부르는 것 | 넘겨받는 인터페이스 |
|---|---|---|
Start |
InitializableManager.Initialize() |
IInitializable |
Update |
TickableManager.Update() |
ITickable |
FixedUpdate |
TickableManager.FixedUpdate() |
IFixedTickable |
LateUpdate |
TickableManager.LateUpdate() |
ILateTickable |
OnDestroy |
DisposableManager.Dispose(), LateDispose() |
IDisposable, ILateDisposable |
매니저는 List<ITickable> 같은 목록을 주입받고, 진입점 객체는 그 목록이 모일 때 만들어집니다. 그래서 IInitializable 이나 ITickable 을 계약으로 걸어야 목록에 들어갑니다. Bind<Spawner>().AsSingle() 처럼 구체 타입만 걸면 Tick() 도 Initialize() 도 불리지 않습니다.
유니티 콜백은 커널 하나에 한 번이고 나머지는 인터페이스 호출이라, 진입점 하나는 Update 하나보다 쌉니다.
| 하나당 | Mono | IL2CPP |
|---|---|---|
커널이 부르는 빈 Tick() |
25 ns | 22 ns |
유니티가 부르는 빈 Update() |
261 ns | 143 ns |
Tick 목록은 다음 프레임부터 우선순위 순으로 선다
TickableManager 는 연결 리스트 하나와 대기 목록 하나를 둡니다. 새 진입점은 대기 목록에 섰다가 다음 프레임 처음에 우선순위 자리로 끼워지고, 지우기는 표시만 했다가 루프 뒤에 뺍니다.
숫자는 Container.BindExecutionOrder<T>(숫자) 로 줍니다. 숫자 하나가 세 매니저의 순서를 함께 정하는데, 해제만 방향이 반대입니다.
| 매니저 | SaveSystem(-10), Spawner(0), Hud(10) 일 때 |
|---|---|
InitializableManager |
SaveSystem → Spawner → Hud |
TickableManager |
SaveSystem → Spawner → Hud |
DisposableManager |
Hud → Spawner → SaveSystem |
작은 숫자는 “먼저 켜지고 먼저 돌고 나중에 꺼진다” 입니다. 등록 순서는 숫자 앞에서 아무 힘이 없습니다. IsPaused 는 그 컨테이너의 Tick 계열만 멈추고, 다른 컨테이너의 커널은 계속 돕니다.
MonoBehaviour 와의 앞뒤는 .meta 의 실행 순서가 정한다
Zenject 소스에는 [DefaultExecutionOrder] 가 없습니다. 커널의 실행 순서는 스크립트 옆 .meta 파일에 있습니다.
# SceneContext.cs.meta executionOrder: -9999
# ProjectKernel.cs.meta executionOrder: -9998
# SceneKernel.cs.meta executionOrder: -9997
# DefaultGameObjectKernel.cs.meta executionOrder: -9996
두 경우 모두 실제 SceneContext 씬에서 네 프레임을 적은 순서입니다(에디터, Mono·IL2CPP 개발 빌드 플레이어 모두 같음). 기본 설정에서는 진입점이 사용자 MonoBehaviour 를 감싸지만, 그 보장은 코드가 아니라 .meta 의 숫자에 있습니다.
예외 하나가 뒤 순서를 멈춘다
InitializableManager 는 하나가 던지면 타입 이름을 붙여 다시 던지고, 목록의 나머지 Initialize 는 불리지 않습니다. 커널은 이미 _hasInitialized = true 를 세웠으므로 다시 시도하지 않는데, Update 는 매니저가 있는지만 보므로 다음 프레임부터 모든 Tick 이 평소처럼 돕니다. 초기화를 못 받은 객체의 Tick 까지 포함해서입니다.
Tick 쪽 루프에는 try 가 없습니다. A(-1), Boom(0), B(1) 을 세우고 Boom.Tick() 이 매 프레임 던지게 하자, 5프레임 동안 A.Tick 과 Boom.Tick 은 5번, B.Tick 은 한 번도 불리지 않았습니다(에디터, Mono·IL2CPP 개발 빌드 플레이어 모두). 콘솔에는 Boom 의 예외뿐이고 B 가 멈췄다는 표시는 없었습니다. LateTick 은 별도 루프라 셋 다 불렸습니다.
주의할 점
1. 커널의 실행 순서를 .meta 째로 지킨다
안 지키면 생기는 문제. Zenject 를 옮기거나 .meta 를 커밋에서 빠뜨려 값이 0 이 되면, 기본 순서 0 인 스크립트와 숫자가 같아져 Tick 과 Update, Initialize 와 Start 의 앞뒤가 보장되지 않습니다. SceneKernel 을 0 으로 바꾼 이 Mac 의 Mono 빌드에서는 그래도 커널이 먼저였고, 시험용 커널로 잰 옛 Windows 측정에서는 뒤집혔습니다. 코드에는 아무 변화가 없어 원인을 찾기 어렵습니다.
이렇게 쓴다. Zenject 폴더는 .meta 와 함께 버전 관리에 넣고, 업그레이드 뒤에는 Project Settings > Script Execution Order 에서 네 값(-9999, -9998, -9997, -9996)이 남아 있는지 봅니다. 순서가 필요한 MonoBehaviour 의 일은 ITickable 로 옮기고 BindExecutionOrder 로 세워, 순서를 정하는 수단을 하나로 모읍니다.
2. Initialize 안의 실패는 스스로 잡는다
안 지키면 생기는 문제. 한 진입점의 Initialize() 가 던지면 뒤 순서의 초기화는 건너뛰고 다시 시도되지 않는데, Tick() 은 다음 프레임부터 모두 돕니다. 초기화에서 채웠어야 할 필드가 null 인 객체들이 매 프레임 Tick 을 받아, 원래 예외 하나가 프레임마다 쏟아지는 널 참조 예외로 번집니다.
이렇게 쓴다. Initialize() 본문에서 실패할 수 있는 일은 스스로 try/catch 로 감싸고, 실패했으면 자기 Tick() 이 아무것도 하지 않도록 상태를 남깁니다. Zenject 에는 진입점 예외를 받는 훅이 없습니다.
3. Tick 안에서 던지지 않는다
안 지키면 생기는 문제. Tick() 하나가 던지면 그 프레임에 뒤에 줄 선 모든 진입점이 건너뜁니다. 같은 오류가 매 프레임 나면 우선순위가 뒤인 진입점은 영영 돌지 않고, 콘솔에는 던진 쪽만 보입니다.
이렇게 쓴다. 외부 입력이나 네트워크처럼 실패가 예상되는 Tick() 은 본문을 스스로 감쌉니다. 우선순위가 앞인 진입점일수록 뒤의 것을 모두 막으니 더 조심합니다.
정리
Zenject 의 진입점은 컨테이너마다 붙는 MonoKernel 의 Start, Update, OnDestroy 안에서 돕니다.
- 진입점은 인터페이스를 계약으로 걸어야 매니저 목록에 들고, 하나를 부르는 값은 빈
Update의 7분의 1에서 10분의 1이었습니다. BindExecutionOrder숫자가 초기화, Tick, 해제 순서를 함께 정하고,MonoBehaviour와의 앞뒤는.meta의 실행 순서가 정합니다.Initialize예외는 뒤의 초기화만 막고Tick은 계속 돌리며,Tick예외는 그 프레임의 뒤 순서를 모두 건너뜁니다.