<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://nightwish-0827.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://nightwish-0827.github.io/" rel="alternate" type="text/html" /><updated>2026-04-03T17:23:15+00:00</updated><id>https://nightwish-0827.github.io/feed.xml</id><title type="html">Hwang Jun Hyuk | Unity Engineer</title><subtitle>기술 블로그 및 포트폴리오입니다.</subtitle><author><name>황준혁</name></author><entry><title type="html">UNInject</title><link href="https://nightwish-0827.github.io/sdks/uninject/" rel="alternate" type="text/html" title="UNInject" /><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/sdks/uninject</id><content type="html" xml:base="https://nightwish-0827.github.io/sdks/uninject/"><![CDATA[<h1 id="uninject--high-performance-unity-dependency-injection-sdk">UNInject – High-Performance Unity Dependency Injection SDK</h1>
<h2 id="current-version--210">Current Version : 2.1.0</h2>

<p><img src="https://img.shields.io/badge/unity-2021.3%2B-black" alt="" />
<img src="https://img.shields.io/badge/license-MIT-blue" alt="" />
<img src="https://img.shields.io/badge/IL2CPP-supported-brightgreen" alt="" />
<img src="https://img.shields.io/badge/[A.O.T]-supported-brightgreen" alt="" /></p>

<blockquote>
  <p><strong>UNInject</strong>는 Unity를 위해 설계된 고성능 의존성 주입 프레임워크임.<br />
<strong>editor-time baking</strong>, <strong>Roslyn source generation</strong>, <strong>three-tier scoping</strong>,<br />
그리고 타입이 <code class="language-plaintext highlighter-rouge">partial</code>로 선언된 경우 <strong>reflection 없는 런타임 경로</strong>를 목표로 설계되었음.</p>
</blockquote>

<p>UNInject는 <strong>editor-time dependency baking</strong>, <strong>Roslyn Source Generation</strong>,<br />
그리고 <strong>hierarchical scoping</strong>을 결합하여 전통적인 DI 구현에서 흔히 발생하는 <strong>reflection 병목</strong>이나<br />
<strong>runtime lookup 오버헤드 없이</strong> 매우 효율적인 runtime dependency injection을 가능하게 함.</p>

<p>기존 DI 시스템들이 <code class="language-plaintext highlighter-rouge">FindObjectsOfType</code> 호출이나 무거운 reflection 초기화에 의존하는 것과 달리
UNInject는 <strong>성능</strong>,<br />
<strong>결정론적 dependency resolution</strong>, <strong>개발자 사용성</strong>을 중심으로 설계된 <strong>완전한 injection architecture</strong>를 제공함.</p>

<p><img width="797" height="86" alt="Image" src="https://github.com/user-attachments/assets/fbba5f3c-6803-41bc-84b9-3ecf957ec7d0" /></p>

<p><code class="language-plaintext highlighter-rouge">https://github.com/NightWish-0827/UNInject.git?path=/com.nightwishlab.uninject</code><br />
UPM Add package from git URL</p>

<hr />

<h1 id="table-of-contents">Table of Contents</h1>

<ul>
  <li><a href="#core-features">Core Features</a></li>
  <li><a href="#pure-c-service-layer">Pure C# service layer</a></li>
  <li><a href="#api-reference--usage">API Reference &amp; Usage</a></li>
  <li><a href="#usage-patterns-concrete-types--monobehaviour">Usage patterns: concrete types &amp; MonoBehaviour</a></li>
  <li><a href="#iscope-registry--named-bindings">IScope, Registry &amp; Named Bindings</a></li>
  <li><a href="#per-installer-api-surface">Per-installer API surface</a></li>
  <li><a href="#constructor-injection--createt">Constructor Injection &amp; <code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code></a></li>
  <li><a href="#tickables--scope-teardown">Tickables &amp; Scope Teardown</a></li>
  <li><a href="#user-callbacks--reference">User Callbacks — Reference</a></li>
  <li><a href="#lifecycle--internal-architecture">Lifecycle &amp; Internal Architecture</a></li>
  <li><a href="#dynamic-object-support">Dynamic Object Support</a></li>
  <li><a href="#performance--injection-paths">Performance &amp; Injection Paths</a></li>
  <li><a href="#editor-supports">Editor Supports</a></li>
  <li><a href="#common-editor-warnings">Common Editor Warnings</a></li>
</ul>

<hr />

<h1 id="core-features">Core Features</h1>

<h3 id="editor-backed-bake-architecture">Editor-Backed Bake Architecture</h3>

<p><code class="language-plaintext highlighter-rouge">ObjectInstaller</code>는 <code class="language-plaintext highlighter-rouge">[Inject]</code> 필드를 <strong>편집 단계에서</strong> hierarchy에 대해 resolve함 (컨텍스트 메뉴 <strong>Bake Dependencies</strong>).<br />
연결 정보는 serialize되어 저장되므로, 해당 필드에 대해 <strong>runtime hierarchy 스캔이 발생하지 않음</strong>.</p>

<hr />

<h3 id="-roslyn-source-generator--il2cpp-완전-지원">✦ Roslyn Source Generator — IL2CPP 완전 지원</h3>

<p>UNInject는 <strong>Roslyn Source Generator</strong> 기반의 코드 생성 파이프라인을 도입하여<br />
기존 Expression Tree 방식이 갖고 있던 <strong>IL2CPP/AOT 환경에서의 제약</strong>을 완전히 해소했음.</p>

<p><strong>동작 방식:</strong></p>

<p>Generator는 <code class="language-plaintext highlighter-rouge">[GlobalInject]</code> / <code class="language-plaintext highlighter-rouge">[SceneInject]</code> 필드를 가진 클래스를 컴파일 타임에 자동 탐지하고,<br />
두 종류의 소스를 <strong>메모리 내에서 직접 생성하여 함께 컴파일</strong>함 (디스크에 사용자 파일로 저장되지 않음).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>코드 저장 → Unity 컴파일 시작
  → UNInjectGenerator 실행 (저장 시마다 자동)
  → ① {TypeName}.UNInject.g.cs  — partial 클래스 확장 + AggressiveInlining setter
  → ② UNInjectPlanRegistry.g.cs — AfterAssembliesLoaded 등록기
  → 함께 컴파일 완료
</code></pre></div></div>

<p>생성된 코드는 디스크에 저장되지 않으며, <code class="language-plaintext highlighter-rouge">Expression.Compile()</code> 을 일절 사용하지 않음.<br />
단순 캐스트와 직접 대입만으로 구성되어 <strong>모든 AOT 환경에서 안전하게 동작함</strong>.</p>

<p><strong>사용자에게 요구되는 유일한 변경사항:</strong></p>

<p><code class="language-plaintext highlighter-rouge">[GlobalInject]</code> 또는 <code class="language-plaintext highlighter-rouge">[SceneInject]</code> 필드를 가진 클래스 (또는 생성된 생성자 플랜이 필요한 타입)에<br />
<code class="language-plaintext highlighter-rouge">public partial class</code> 선언 추가.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 변경 전</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">PlayerController</span> <span class="p">:</span> <span class="n">MonoBehaviour</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>

<span class="c1">// 변경 후</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">PlayerController</span> <span class="p">:</span> <span class="n">MonoBehaviour</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">partial</code> 없이 Play 진입 시 <code class="language-plaintext highlighter-rouge">UNInjectFallbackGuard</code>가 즉시 경고를 출력함.<br />
또한 Roslyn 컴파일 타임에 <strong>UNI001 경고</strong>가 발행되어, IDE 레벨에서도 인지할 수 있음.</p>

<hr />

<h3 id="runtime-injection-tiers">Runtime Injection Tiers</h3>

<p>dependency injection 수행 시 런타임 주입 경로는 우선순위 순으로 세 단계로 구성됨.</p>

<table>
  <thead>
    <tr>
      <th>우선순위</th>
      <th>경로</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Roslyn 생성 플랜</td>
      <td>Dictionary 조회 + AggressiveInlining setter</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Expression Tree 폴백</td>
      <td>캐싱된 delegate; <strong>Mono 지향</strong></td>
    </tr>
    <tr>
      <td>3</td>
      <td><code class="language-plaintext highlighter-rouge">FieldInfo.SetValue</code></td>
      <td>최후 수단; 핫 경로에서 <strong>IL2CPP 위험</strong></td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="three-tier-deterministic-scoping">Three-Tier Deterministic Scoping</h3>

<p>전통적인 DI 시스템들은 lifecycle 관리가 모호해지는 문제가 자주 발생함.</p>

<p>UNInject는 이를 해결하기 위해
<strong>세 가지 명확한 scope 계층을 강제함</strong>.</p>

<table>
  <thead>
    <tr>
      <th>Scope</th>
      <th>컴포넌트</th>
      <th>Lifetime</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Global</td>
      <td><code class="language-plaintext highlighter-rouge">MasterInstaller</code></td>
      <td><code class="language-plaintext highlighter-rouge">DontDestroyOnLoad</code></td>
    </tr>
    <tr>
      <td>Scene</td>
      <td><code class="language-plaintext highlighter-rouge">SceneInstaller</code></td>
      <td>현재 scene (아래 정책 참조)</td>
    </tr>
    <tr>
      <td>Local</td>
      <td><code class="language-plaintext highlighter-rouge">ObjectInstaller</code></td>
      <td>installer root 하위 서브트리</td>
    </tr>
  </tbody>
</table>

<p>Scene 언로드 동작은 <code class="language-plaintext highlighter-rouge">SceneInstaller</code>의 <strong><code class="language-plaintext highlighter-rouge">SceneExitPolicy</code></strong> 로 제어됨.</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Clear</code></strong> — 소멸 시 registry를 비움 (기본값).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Preserve</code></strong> — 언로드 이후에도 registry 항목을 유지함 (예: additive 로딩).<br />
소유권 맵은 초기화되므로, stale한 <code class="language-plaintext highlighter-rouge">UnregisterByOwner</code> 경로가 preserved 바인딩을 제거하지 않음.</li>
</ul>

<hr />

<h3 id="playmode-guard-protection">PlayMode Guard Protection</h3>

<p>Baked 기반 시스템에서 발생할 수 있는 잠재적 문제들을 <strong>두 개의 독립적인 가드</strong>로 방어함.</p>

<p><strong>① MasterInstallerPlayModeGuard</strong> — 레지스트리 공백 감지</p>

<p>개발자가 PlayMode 진입 전에 global registry를 갱신하지 않았을 때 발생하는<br />
<strong>empty registry 문제</strong>를 방지함.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">MasterInstaller</code>가 scene에 존재하고 <code class="language-plaintext highlighter-rouge">_globalReferrals.arraySize == 0</code> 이면 즉시 warning 로그를 출력함.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[MasterInstaller] Global registry is empty. Did you forget to click 'Refresh Global Registry' before Play?
</code></pre></div></div>

<p><strong>② UNInjectFallbackGuard</strong> — IL2CPP 폴백 타입 감지</p>

<p><code class="language-plaintext highlighter-rouge">partial</code> 선언 없이 Expression Tree 폴백으로 동작할 타입을 Play 진입 시점에 탐지함.<br />
IL2CPP 빌드에서 <code class="language-plaintext highlighter-rouge">FieldInfo.SetValue</code> 폴백까지 내려갈 수 있는 타입 목록을 명시적으로 출력함.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[UNInject] 다음 타입은 'partial' 선언이 없어 Roslyn 생성 플랜 대신
Expression Tree 폴백으로 동작합니다.
  • MyGame.PlayerController
  • MyGame.EnemyAI
</code></pre></div></div>

<p>두 가드는 <strong>감시 대상과 검사 방식이 완전히 독립적</strong>이므로 별도 파일로 분리되어 있으며,<br />
동일한 Play 진입 시 두 가드가 모두 발동할 수 있음.</p>

<hr />

<h3 id="runtime-registry-shape">Runtime Registry Shape</h3>

<p>Registry는 <strong><code class="language-plaintext highlighter-rouge">Dictionary&lt;RegistryKey, Component&gt;</code></strong> 구조를 사용하며, <strong><code class="language-plaintext highlighter-rouge">RegistryKey</code> = (Type, Id)</strong> 임.</p>

<p><code class="language-plaintext highlighter-rouge">Id == string.Empty</code>는 <strong>v1.x 방식의 키 없는</strong> 등록과 매칭됨.<br />
Named referral은 <strong><code class="language-plaintext highlighter-rouge">[Referral]</code> / <code class="language-plaintext highlighter-rouge">[SceneReferral]</code></strong> 및 <strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code> / <code class="language-plaintext highlighter-rouge">[SceneInject]</code></strong><br />
(필드와 생성자 파라미터 모두)에서 동일한 <strong><code class="language-plaintext highlighter-rouge">Id</code></strong> 를 사용해 매칭됨.</p>

<hr />

<h3 id="cache-friendly-injection-execution">Cache-Friendly Injection Execution</h3>

<p>UNInject는 내부적으로 <strong>cached structural mapping 구조</strong>를 사용함.</p>

<p>global 및 scene registry는
미리 baked된 리스트 구조를 기반으로 생성되며<br />
<code class="language-plaintext highlighter-rouge">Awake</code> 시점에 빠른 <code class="language-plaintext highlighter-rouge">Dictionary&lt;Type, Component&gt;</code> lookup 구조로 변환됨.</p>

<p><code class="language-plaintext highlighter-rouge">TypeDataCache</code>는 생성 플랜 캐시와 Reflection 캐시를 분리하여 관리함.<br />
생성 플랜이 등록된 타입은 <code class="language-plaintext highlighter-rouge">Dictionary</code> 조회 한 번으로 처리가 완료되며, <code class="language-plaintext highlighter-rouge">Warmup()</code> 호출조차 <strong>사실상 무비용</strong>으로 동작함.</p>

<p>이러한 <strong>data-oriented 구조</strong>는
scene hierarchy를 직접 순회하는 방식보다 훨씬 높은 성능을 제공함.</p>

<hr />

<h1 id="pure-c-service-layer">Pure C# service layer</h1>

<p>UNInject는 두 가지 런타임 역할을 명확히 구분함.</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>정의</th>
      <th>등록 방식</th>
      <th>일반적 용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Manager layer</strong></td>
      <td><code class="language-plaintext highlighter-rouge">[Referral]</code> / <code class="language-plaintext highlighter-rouge">[SceneReferral]</code>을 가진 <code class="language-plaintext highlighter-rouge">MonoBehaviour</code></td>
      <td>Editor refresh → baked lists → <code class="language-plaintext highlighter-rouge">Awake</code> registry 구축</td>
      <td><code class="language-plaintext highlighter-rouge">UnityEngine.Object</code> lifetime, scene 또는 DDOL</td>
    </tr>
    <tr>
      <td><strong>Service layer</strong></td>
      <td><strong>plain C# <code class="language-plaintext highlighter-rouge">class</code></strong> (<code class="language-plaintext highlighter-rouge">MonoBehaviour</code> 없음), inject 필드를 위해 <strong><code class="language-plaintext highlighter-rouge">partial</code></strong> 선언</td>
      <td>hierarchy에 없음 — <strong><code class="language-plaintext highlighter-rouge">IScope.Create&lt;T&gt;()</code></strong> 를 통해서만 생성</td>
      <td><code class="language-plaintext highlighter-rouge">[GlobalInject]</code> / <code class="language-plaintext highlighter-rouge">[SceneInject]</code>로 manager를 소비하는 application/domain 로직, facade, 시스템</td>
    </tr>
  </tbody>
</table>

<p><strong>Service layer</strong> 인스턴스의 특징:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">[InjectConstructor]</code></strong> (또는 <code class="language-plaintext highlighter-rouge">public</code> 생성자가 정확히 <strong>하나</strong>인 경우 attribute 없이도 동작)로 구성되는<br />
<strong><code class="language-plaintext highlighter-rouge">ReferenceType</code></strong> 객체이며, zero-reflection 생성을 위한 <strong>Roslyn <code class="language-plaintext highlighter-rouge">TryGetGeneratedFactory</code></strong> 경로를 선택적으로 사용함.</li>
  <li><code class="language-plaintext highlighter-rouge">MonoBehaviour</code>와 <strong>동일한</strong> <code class="language-plaintext highlighter-rouge">RegistryKey</code> 규칙으로 <strong><code class="language-plaintext highlighter-rouge">Component</code></strong> 의존성을 resolve함: 생성자 파라미터에<br />
<strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code> / <code class="language-plaintext highlighter-rouge">[SceneInject]</code></strong> (named <code class="language-plaintext highlighter-rouge">Id</code>와 <code class="language-plaintext highlighter-rouge">optional</code> 포함)를 선언할 수 있음.</li>
  <li>Unity 메시지를 받지 않음 (<code class="language-plaintext highlighter-rouge">Update</code>/<code class="language-plaintext highlighter-rouge">OnDestroy</code> 없음). 참여는 명시적으로 이루어짐:
    <ul>
      <li><strong><code class="language-plaintext highlighter-rouge">ITickable</code> / <code class="language-plaintext highlighter-rouge">IFixedTickable</code> / <code class="language-plaintext highlighter-rouge">ILateTickable</code></strong> → <code class="language-plaintext highlighter-rouge">Create</code>를 호출한 <strong><code class="language-plaintext highlighter-rouge">MonoBehaviour</code> installer</strong>가<br />
(<code class="language-plaintext highlighter-rouge">Update</code> / <code class="language-plaintext highlighter-rouge">FixedUpdate</code> / <code class="language-plaintext highlighter-rouge">LateUpdate</code>를 포워딩) 구동함.</li>
      <li><strong><code class="language-plaintext highlighter-rouge">IScopeDestroyable</code></strong> → 해당 installer의 <code class="language-plaintext highlighter-rouge">OnDestroy</code> 실행 시 teardown됨 (<a href="#tickables--scope-teardown">Tickables &amp; Scope Teardown</a> 참조).</li>
    </ul>
  </li>
  <li><strong>Lifetime</strong>은 <strong>소유 scope의</strong> <code class="language-plaintext highlighter-rouge">GameObject</code>에 바인딩됨: <code class="language-plaintext highlighter-rouge">MasterInstaller</code> / <code class="language-plaintext highlighter-rouge">SceneInstaller</code> / <code class="language-plaintext highlighter-rouge">ObjectInstaller</code>가 소멸되면 <code class="language-plaintext highlighter-rouge">TickableRegistry.ClearWithDestroy()</code>가 등록된 destroyable에 대해 <strong><code class="language-plaintext highlighter-rouge">OnScopeDestroy()</code></strong> 를 실행한 뒤 tick 목록을 초기화.</li>
</ul>

<p><strong><code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code>에 사용할 scope 선택 기준</strong></p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">MasterInstaller.Create&lt;T&gt;()</code></strong> — resolver가 <strong>global</strong> 먼저 탐색 후 <strong><code class="language-plaintext highlighter-rouge">SceneInstaller</code></strong> fallthrough.<br />
scene 로드 이후에도 살아남아야 하는 game-wide 서비스에 사용 (installer가 DDOL).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">SceneInstaller.Create&lt;T&gt;()</code></strong> — resolver가 <strong>scene</strong> 먼저 탐색 후 <strong><code class="language-plaintext highlighter-rouge">MasterInstaller</code></strong>. session/scene 단위 로직에 사용.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">ObjectInstaller.Create&lt;T&gt;()</code></strong> — resolver가 <strong>local registry</strong> → <code class="language-plaintext highlighter-rouge">_parentScope</code> 체인(선택적) → scene → global 순 탐색.<br />
서브트리 로컬 서비스 (예: 하나의 UI root 또는 풀링된 서브시스템 하위)에 사용.</li>
</ul>

<p><strong><code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code>에서 지원하지 않는 타입</strong></p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">MonoBehaviour</code></strong> — Unity가 생성자 기반 구성을 허용하지 않음.<br />
대신 <strong><code class="language-plaintext highlighter-rouge">SpawnInjected</code></strong> / <strong><code class="language-plaintext highlighter-rouge">InjectTarget</code></strong> / <strong><code class="language-plaintext highlighter-rouge">InjectGameObject</code></strong> 를 사용할 것.</li>
</ul>

<blockquote>
  <p><strong><code class="language-plaintext highlighter-rouge">ScriptableObject</code> 관련 주의:</strong> <code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code>는 생성자 호출(또는 생성된 factory)을 사용함.<br />
Unity <code class="language-plaintext highlighter-rouge">ScriptableObject</code> 인스턴스는 일반적으로 <code class="language-plaintext highlighter-rouge">ScriptableObject.CreateInstance</code>로 생성하므로,<br />
<code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code>는 <strong>non-<code class="language-plaintext highlighter-rouge">UnityEngine.Object</code></strong> service 타입을 대상으로 하는 것으로 간주해야 함.</p>
</blockquote>

<hr />

<h1 id="api-reference--usage">API Reference &amp; Usage</h1>

<p>Resolve는 항상 <strong><code class="language-plaintext highlighter-rouge">RegistryKey(Type, Id)</code></strong> 를 사용함. <strong>선언된 필드</strong> (또는 생성자 파라미터)의 <strong><code class="language-plaintext highlighter-rouge">Type</code></strong> 이 registry가 보유한 키와 일치해야 함.</p>

<p>해당 키는 <strong>concrete</strong> <code class="language-plaintext highlighter-rouge">MonoBehaviour</code> 등록 (“wide” auto-mapping) 또는 <strong>narrow</strong> <strong><code class="language-plaintext highlighter-rouge">[Referral(BindType)]</code></strong> (“expert” 단일 키)에서 비롯될 수 있음. <strong>초보자와 전문가 모두 동일한 <code class="language-plaintext highlighter-rouge">Resolve</code></strong> 를 사용하며, 차이는 등록 방식에 있음.</p>

<hr />

<h2 id="usage-patterns-concrete-types--monobehaviour">Usage patterns: concrete types &amp; MonoBehaviour</h2>

<h3 id="pattern-1--global-manager-concrete-type만-사용-interface-불필요">Pattern 1 — Global manager: concrete type만 사용 (interface 불필요)</h3>

<p>많은 팀이 <strong><code class="language-plaintext highlighter-rouge">AudioManager</code></strong> 나 <strong><code class="language-plaintext highlighter-rouge">GameSettings</code></strong> 를 직접 참조함. interface를 반드시 도입할 필요는 없음.</p>

<p><strong>Provider (MasterInstaller에 bake됨):</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// [Referral] with no BindType → wide registration: concrete + mappable interfaces + bases</span>
<span class="p">[</span><span class="n">Referral</span><span class="p">]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">AudioManager</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">PlaySfx</span><span class="p">()</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Consumer (<code class="language-plaintext highlighter-rouge">partial</code> + generator):</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">PlayerController</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">GlobalInject</span><span class="p">]</span> <span class="k">private</span> <span class="n">AudioManager</span> <span class="n">_audio</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>동등한 “interface-first” 스타일</strong> (동일한 <code class="language-plaintext highlighter-rouge">Resolve</code>, 테스트 seam 명확):</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IAudioManager</span> <span class="p">{</span> <span class="k">void</span> <span class="nf">PlaySfx</span><span class="p">();</span> <span class="p">}</span>

<span class="p">[</span><span class="nf">Referral</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">IAudioManager</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">AudioManager</span> <span class="p">:</span> <span class="n">MonoBehaviour</span><span class="p">,</span> <span class="n">IAudioManager</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">PlaySfx</span><span class="p">()</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">PlayerController</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">GlobalInject</span><span class="p">]</span> <span class="k">private</span> <span class="n">IAudioManager</span> <span class="n">_audio</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">Refresh Global Registry</code></strong> 이후, scene에 <code class="language-plaintext highlighter-rouge">AudioManager</code>가 하나 존재하면 두 consumer 모두 동일한 인스턴스를 받음.</p>

<hr />

<h3 id="pattern-2--scene-manager-concrete-vs-scenereferralbindtype">Pattern 2 — Scene manager: concrete vs <code class="language-plaintext highlighter-rouge">SceneReferral(BindType)</code></h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">SceneReferral</span><span class="p">]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">WaveSpawner</span> <span class="p">:</span> <span class="n">MonoBehaviour</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>

<span class="c1">// 동일 scene의 ObjectInstaller 하위 Consumer</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">HordeDirector</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">SceneInject</span><span class="p">]</span> <span class="k">private</span> <span class="n">WaveSpawner</span> <span class="n">_waves</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>추상화를 사용하는 경우:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IWaveSpawner</span> <span class="p">{</span> <span class="p">}</span>

<span class="p">[</span><span class="nf">SceneReferral</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">IWaveSpawner</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">WaveSpawner</span> <span class="p">:</span> <span class="n">MonoBehaviour</span><span class="p">,</span> <span class="n">IWaveSpawner</span> <span class="p">{</span> <span class="p">}</span>

<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">HordeDirector</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">SceneInject</span><span class="p">]</span> <span class="k">private</span> <span class="n">IWaveSpawner</span> <span class="n">_waves</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>활성 <strong><code class="language-plaintext highlighter-rouge">SceneInstaller</code></strong> 에서 <strong><code class="language-plaintext highlighter-rouge">Refresh Scene Registry</code></strong> 를 실행할 것.</p>

<hr />

<h3 id="pattern-3--local-subtree-inject--sibling--child-monobehaviour-global-registry-없음">Pattern 3 — Local subtree: <code class="language-plaintext highlighter-rouge">[Inject]</code> + sibling / child <code class="language-plaintext highlighter-rouge">MonoBehaviour</code> (global registry 없음)</h3>

<p>전형적인 “beginner” hierarchy: 하나의 <strong><code class="language-plaintext highlighter-rouge">ObjectInstaller</code></strong> root 하위에 <strong>concrete</strong> <code class="language-plaintext highlighter-rouge">MonoBehaviour</code> 참조만 사용하는 구성.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">HUD</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">Inject</span><span class="p">]</span> <span class="p">[</span><span class="n">SerializeField</span><span class="p">]</span> <span class="k">private</span> <span class="n">HealthBar</span> <span class="n">_healthBar</span><span class="p">;</span>    <span class="c1">// 동일 root 하위 child 또는 sibling</span>
    <span class="p">[</span><span class="n">Inject</span><span class="p">]</span> <span class="p">[</span><span class="n">SerializeField</span><span class="p">]</span> <span class="k">private</span> <span class="n">PlayerController</span> <span class="n">_player</span><span class="p">;</span> <span class="c1">// SerializeField + Bake Dependencies</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">[Inject]</code></strong> 는 <strong>editor에서</strong> resolve됨 (<strong>Bake Dependencies</strong>); runtime에는 Unity native deserialization이 자동으로<br />
참조를 복원함 — <strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code></strong> 불필요.</li>
  <li>동일 타입에 <code class="language-plaintext highlighter-rouge">[GlobalInject]</code> / <code class="language-plaintext highlighter-rouge">[SceneInject]</code> 필드가 있다면 <strong><code class="language-plaintext highlighter-rouge">public partial class</code></strong> 선언 필요.</li>
</ul>

<hr />

<h3 id="pattern-4--named-bindings-동일-field-type을-공유하는-여러-인스턴스">Pattern 4 — Named bindings: 동일 field type을 공유하는 여러 인스턴스</h3>

<p><strong><code class="language-plaintext highlighter-rouge">Id</code></strong> 없이는 키당 <strong>하나</strong>의 등록만 유효함 (중복 시 경고 로그). <code class="language-plaintext highlighter-rouge">AudioManager</code> 오브젝트가 둘 이상이라면 <strong>named</strong> referral과<br />
매칭 inject <strong><code class="language-plaintext highlighter-rouge">Id</code></strong> 를 사용해야 함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Referral</span><span class="p">(</span><span class="s">"music"</span><span class="p">,</span> <span class="k">typeof</span><span class="p">(</span><span class="n">AudioManager</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">MusicManager</span> <span class="p">:</span> <span class="n">AudioManager</span> <span class="p">{</span> <span class="p">}</span>

<span class="p">[</span><span class="nf">Referral</span><span class="p">(</span><span class="s">"sfx"</span><span class="p">,</span> <span class="k">typeof</span><span class="p">(</span><span class="n">AudioManager</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">SfxManager</span> <span class="p">:</span> <span class="n">AudioManager</span> <span class="p">{</span> <span class="p">}</span>

<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">MixerHub</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">GlobalInject</span><span class="p">(</span><span class="s">"music"</span><span class="p">)]</span> <span class="k">private</span> <span class="n">AudioManager</span> <span class="n">_music</span><span class="p">;</span>
    <span class="p">[</span><span class="nf">GlobalInject</span><span class="p">(</span><span class="s">"sfx"</span><span class="p">)]</span>  <span class="k">private</span> <span class="n">AudioManager</span> <span class="n">_sfx</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">BindType</code></strong> 을 interface로 지정할 수도 있음; <strong><code class="language-plaintext highlighter-rouge">[Referral]</code></strong> 과 <strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code></strong> 의 <strong><code class="language-plaintext highlighter-rouge">Id</code></strong> 는 여전히 쌍으로 일치해야 함.</p>

<hr />

<h3 id="pattern-5--runtime-spawned-monobehaviour-concrete-register-vs-registeribind">Pattern 5 — Runtime-spawned <code class="language-plaintext highlighter-rouge">MonoBehaviour</code> (concrete <code class="language-plaintext highlighter-rouge">Register</code> vs <code class="language-plaintext highlighter-rouge">Register&lt;IBind&gt;</code>)</h3>

<p>bake-time global discovery에 의존하지 않는 enemy prefab 스폰:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">EnemyView</span> <span class="p">:</span> <span class="n">MonoBehaviour</span><span class="p">,</span> <span class="n">IEnemyView</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>

<span class="c1">// Instantiate 이후 —</span>
<span class="kt">var</span> <span class="n">scope</span> <span class="p">=</span> <span class="n">GetComponent</span><span class="p">&lt;</span><span class="n">ObjectInstaller</span><span class="p">&gt;();</span> <span class="c1">// 또는 캐싱된 참조</span>
<span class="n">scope</span><span class="p">.</span><span class="n">Register</span><span class="p">&lt;</span><span class="n">EnemyView</span><span class="p">&gt;(</span><span class="n">enemyView</span><span class="p">,</span> <span class="n">owner</span><span class="p">:</span> <span class="k">this</span><span class="p">);</span>  <span class="c1">// expert: EnemyView 키만 바인딩</span>
<span class="n">scope</span><span class="p">.</span><span class="nf">Register</span><span class="p">(</span><span class="n">enemyView</span><span class="p">,</span> <span class="n">owner</span><span class="p">:</span> <span class="k">this</span><span class="p">);</span>             <span class="c1">// beginner-friendly: inspector [Referral]과 동일</span>
</code></pre></div></div>

<p>prefab 클래스에 <strong><code class="language-plaintext highlighter-rouge">[Referral(typeof(IEnemyView))]</code></strong> 가 있다면 <strong><code class="language-plaintext highlighter-rouge">Register(enemyView)</code></strong> 가 <strong><code class="language-plaintext highlighter-rouge">BindType</code></strong> 을 자동으로 반영함.</p>

<p>concrete class만 있는 경우에도 <code class="language-plaintext highlighter-rouge">typeof(EnemyView)</code> 키로 등록되므로,<br />
다른 타입에서 <strong><code class="language-plaintext highlighter-rouge">[GlobalInject] private EnemyView _x</code></strong> 를 interface 없이도 사용할 수 있음.</p>

<hr />

<h3 id="pattern-6--objectinstaller-참조-방식">Pattern 6 — <code class="language-plaintext highlighter-rouge">ObjectInstaller</code> 참조 방식</h3>

<p>다음 방식들 중 어느 것이든, <strong>동일한</strong> 인스턴스를 호출하면 유효함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">SerializeField</span><span class="p">]</span> <span class="k">private</span> <span class="n">ObjectInstaller</span> <span class="n">_scope</span><span class="p">;</span>  <span class="c1">// inspector에서 드래그 (초보자에게 일반적)</span>

<span class="c1">// 또는</span>
<span class="k">private</span> <span class="n">ObjectInstaller</span> <span class="n">Scope</span> <span class="p">=&gt;</span> <span class="n">GetComponentInParent</span><span class="p">&lt;</span><span class="n">ObjectInstaller</span><span class="p">&gt;();</span>

<span class="c1">// 또는 (scene-local singleton 경로)</span>
<span class="n">SceneInstaller</span><span class="p">.</span><span class="n">Instance</span><span class="p">.</span><span class="n">Create</span><span class="p">&lt;</span><span class="n">MyService</span><span class="p">&gt;();</span> <span class="c1">// Scene scope에서 서비스 생성 — ObjectInstaller 필드 불필요</span>
</code></pre></div></div>

<p><strong>local registry + <code class="language-plaintext highlighter-rouge">_parentScope</code></strong> 를 고려한 injection이 필요하면 <strong><code class="language-plaintext highlighter-rouge">ObjectInstaller</code></strong> 를 사용하고,<br />
서비스가 해당 installer의 native chain만 사용해야 한다면 <strong><code class="language-plaintext highlighter-rouge">MasterInstaller</code></strong> / <strong><code class="language-plaintext highlighter-rouge">SceneInstaller</code></strong> 를 사용할 것.</p>

<hr />

<h3 id="pattern-7--createt와-concrete-생성자-파라미터-service-layer">Pattern 7 — <code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code>와 concrete 생성자 파라미터 (service layer)</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">SessionStats</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">GlobalInject</span><span class="p">]</span> <span class="k">private</span> <span class="n">IAnalytics</span> <span class="n">_analytics</span><span class="p">;</span> <span class="c1">// ctor body 실행 후 주입됨</span>

    <span class="p">[</span><span class="n">InjectConstructor</span><span class="p">]</span>
    <span class="k">public</span> <span class="nf">SessionStats</span><span class="p">([</span><span class="n">GlobalInject</span><span class="p">]</span> <span class="n">AudioManager</span> <span class="n">audio</span><span class="p">,</span> <span class="n">WaveSpawner</span> <span class="n">waves</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// [GlobalInject] / [SceneInject] 필드에서 얻는 것과 동일한 인스턴스</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// var stats = sceneInstaller.Create&lt;SessionStats&gt;();</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AudioManager</code> / <code class="language-plaintext highlighter-rouge">WaveSpawner</code>는 <strong>GlobalInject 필드와 완전히 동일하게</strong> registry에서 resolve되는 <strong>concrete</strong> <code class="language-plaintext highlighter-rouge">Component</code> 타입임. 해당 타입들이 interface 키로 등록되어 있다면 interface도 동일하게 동작함.</p>

<hr />

<h2 id="provider-attributes-registration">Provider attributes (registration)</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Expert: 단일 bind type (BindType + Id 키만 등록됨)</span>
<span class="p">[</span><span class="nf">Referral</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">IInputService</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">DesktopInputManager</span> <span class="p">:</span> <span class="n">MonoBehaviour</span><span class="p">,</span> <span class="n">IInputService</span> <span class="p">{</span> <span class="p">}</span>

<span class="c1">// Named expert binding</span>
<span class="p">[</span><span class="nf">Referral</span><span class="p">(</span><span class="s">"profileA"</span><span class="p">,</span> <span class="k">typeof</span><span class="p">(</span><span class="n">IProfileService</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">ProfileServiceA</span> <span class="p">:</span> <span class="n">MonoBehaviour</span><span class="p">,</span> <span class="n">IProfileService</span> <span class="p">{</span> <span class="p">}</span>

<span class="c1">// Scene expert binding</span>
<span class="p">[</span><span class="nf">SceneReferral</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">ILevelRules</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">LevelRules</span> <span class="p">:</span> <span class="n">MonoBehaviour</span><span class="p">,</span> <span class="n">ILevelRules</span> <span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Beginner-friendly: 최소 attribute; Refresh Global Registry가 이 타입을 자동 수집</span>
<span class="p">[</span><span class="n">Referral</span><span class="p">]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">AudioManager</span> <span class="p">:</span> <span class="n">MonoBehaviour</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
</code></pre></div></div>

<p>(<code class="language-plaintext highlighter-rouge">Refresh</code>는 <strong>component class</strong>에 <strong><code class="language-plaintext highlighter-rouge">[Referral]</code></strong> / <strong><code class="language-plaintext highlighter-rouge">[SceneReferral]</code></strong> 이 있는 타입만 탐지함.<br />
순수 런타임 오브젝트는 attribute 없이 <strong><code class="language-plaintext highlighter-rouge">Register</code></strong> 를 직접 사용할 수 있음.)</p>

<p>editor <strong>Refresh Global Registry</strong> / <strong>Refresh Scene Registry</strong> 는 scene을 스캔하여 serialize된 리스트를 채우고,<br />
<code class="language-plaintext highlighter-rouge">Awake</code>에서 빠른 dictionary를 재구성함.</p>

<h2 id="consumer-attributes">Consumer attributes</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">PlayerController</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">Inject</span><span class="p">]</span> <span class="p">[</span><span class="n">SerializeField</span><span class="p">]</span> <span class="k">private</span> <span class="n">Animator</span> <span class="n">_animator</span><span class="p">;</span>

    <span class="p">[</span><span class="n">GlobalInject</span><span class="p">]</span> <span class="k">private</span> <span class="n">IInputService</span> <span class="n">_input</span><span class="p">;</span>
    <span class="p">[</span><span class="n">GlobalInject</span><span class="p">]</span> <span class="k">private</span> <span class="n">AudioManager</span> <span class="n">_audioConcrete</span><span class="p">;</span>   <span class="c1">// AudioManager가 해당 Type으로 등록된 경우 유효</span>

    <span class="p">[</span><span class="nf">GlobalInject</span><span class="p">(</span><span class="s">"profileA"</span><span class="p">)]</span> <span class="k">private</span> <span class="n">IProfileService</span> <span class="n">_profile</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">SceneInject</span><span class="p">(</span><span class="n">optional</span><span class="p">:</span> <span class="k">true</span><span class="p">)]</span> <span class="k">private</span> <span class="n">LevelManager</span> <span class="n">_level</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Optional:</strong> <code class="language-plaintext highlighter-rouge">[GlobalInject(optional: true)]</code> / <code class="language-plaintext highlighter-rouge">[SceneInject(optional: true)]</code><br />
바인딩이 없어도 injection이 실패하지 않으며, 해당 필드에 대한 경고도 출력되지 않음.</p>

<p>Inspector 표기 정책: optional 미바인딩 필드는 <strong>회색</strong>, 필수 미등록 바인딩은 에러/경고로 표시됨.</p>

<hr />

<h2 id="injected-state-alarm">Injected State Alarm</h2>

<p>각종 <code class="language-plaintext highlighter-rouge">Function</code> 이라 불리우는 <strong>Start</strong>, <strong>Awake</strong> 와 마찬가지로, <strong>UNInject</strong> 또한 명시적인 주입 완료 시점 <code class="language-plaintext highlighter-rouge">Function</code>을 제공함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">PlayerController</span> <span class="p">:</span> <span class="n">MonoBehaviour</span><span class="p">,</span> <span class="n">IInjected</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">GlobalInject</span><span class="p">]</span> <span class="k">private</span> <span class="n">IInputService</span> <span class="n">_input</span><span class="p">;</span>
    <span class="p">[</span><span class="nf">SceneInject</span><span class="p">(</span><span class="k">true</span><span class="p">)]</span> <span class="k">private</span> <span class="n">IStageContext</span> <span class="n">_stageContext</span><span class="p">;</span>    

    <span class="k">public</span> <span class="k">void</span> <span class="nf">OnInjected</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// ObjectInstaller가 이 컴포넌트에 대해 주입을 수행했고,</span>
        <span class="c1">// "필수 의존성" 주입이 모두 성공했을 때 자동으로 호출됩니다.</span>

        <span class="c1">// 호출 주체: ObjectInstaller.TryInjectTarget() 내부에서 success == true이고  </span>
        <span class="c1">// 대상이 IInjected를 구현한 경우 OnInjected()를 호출합니다.</span>
        
        <span class="c1">// 호출 조건: 필수(Required) 의존성 주입이 모두 성공했을 때만 호출됩니다.  </span>
        <span class="c1">// (Optional로 표시된 의존성 누락은 성공/실패에 영향 없음)</span>
        
        <span class="c1">// 호출 시점/횟수: InjectTarget() / InjectGameObject() / Awake()에서  </span>
        <span class="c1">// 주입이 실행될 때마다(즉, 주입을 여러 번 호출하면) 그때마다 호출될 수 있습니다.</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>(현재는 <code class="language-plaintext highlighter-rouge">IInjected</code> 인터페이스를 상속받아야만 해당 <code class="language-plaintext highlighter-rouge">OnInjected</code> 멤버를 사용 가능하지만,<br />
추후 Native 영역으로 이를 추출하여, 별도의 상속 없이도 <code class="language-plaintext highlighter-rouge">OnInjected</code> 멤버를 사용할 수 있도록 구조를 변경할 예정임.)</p>

<hr />

<h1 id="iscope-registry--named-bindings">IScope, Registry &amp; Named Bindings</h1>

<p><strong><code class="language-plaintext highlighter-rouge">IScope</code></strong> 는 <strong><code class="language-plaintext highlighter-rouge">MasterInstaller</code></strong>, <strong><code class="language-plaintext highlighter-rouge">SceneInstaller</code></strong>, <strong><code class="language-plaintext highlighter-rouge">ObjectInstaller</code></strong> 모두가 구현함.</p>

<h3 id="register">Register</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Register(Component comp)</code></strong> — <code class="language-plaintext highlighter-rouge">comp</code> 타입의 <code class="language-plaintext highlighter-rouge">[Referral]</code> / <code class="language-plaintext highlighter-rouge">[SceneReferral]</code> 메타데이터(bind type + id)를 사용하여 등록.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Register(Component comp, MonoBehaviour owner)</code></strong> — 동일; <strong><code class="language-plaintext highlighter-rouge">owner</code></strong> 가 소멸되면 해당 owner로 등록된 모든 component가 자동 unregister됨 (<code class="language-plaintext highlighter-rouge">ScopeOwnerTracker</code>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Register&lt;TBind&gt;(Component comp, MonoBehaviour owner = null)</code></strong> — <strong>코드 우선</strong> 바인딩: <strong><code class="language-plaintext highlighter-rouge">comp</code></strong> 를 <strong><code class="language-plaintext highlighter-rouge">TBind</code></strong> 로 등록  (attribute bind type 선택 사항).</li>
</ul>

<p>중복 키는 경고 로그를 출력하고 <strong>최초</strong> 등록을 유지함.</p>

<h3 id="unregister">Unregister</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Unregister(Type type)</code></strong> — 키 없는 제거 (<code class="language-plaintext highlighter-rouge">Id == default</code>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Unregister(Type type, string id)</code></strong> — named 키 제거.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Unregister(Component comp)</code></strong> — <strong><code class="language-plaintext highlighter-rouge">MasterInstaller</code></strong> / <strong><code class="language-plaintext highlighter-rouge">SceneInstaller</code> 전용</strong>: 값이 <strong><code class="language-plaintext highlighter-rouge">comp</code></strong> 인 <strong>모든 registry 키</strong>를 제거. (<code class="language-plaintext highlighter-rouge">ObjectInstaller</code>에는 해당 오버로드 없음; type/id 또는 owner 기반 정리를 사용할 것.)</li>
</ul>

<h3 id="resolve">Resolve</h3>

<p>키 없는 방식:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Resolve(Type type)</code></strong>, <strong><code class="language-plaintext highlighter-rouge">TryResolve(Type, out Component)</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Resolve&lt;T&gt;() where T : Component</code></strong>, <strong><code class="language-plaintext highlighter-rouge">ResolveAs&lt;T&gt;() where T : class</code></strong></li>
</ul>

<p>Named 방식:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Resolve(Type type, string id)</code></strong>, <strong><code class="language-plaintext highlighter-rouge">TryResolve(Type, string id, out Component)</code></strong>, <strong><code class="language-plaintext highlighter-rouge">ResolveAs&lt;T&gt;(string id)</code></strong></li>
</ul>

<p><strong><code class="language-plaintext highlighter-rouge">MasterInstaller</code></strong> 는 <strong><code class="language-plaintext highlighter-rouge">Instance</code></strong> 위에 <strong><code class="language-plaintext highlighter-rouge">ResolveStatic&lt;T&gt;()</code></strong> 도 편의용으로 제공함.</p>

<h3 id="objectinstaller-resolve-chain">ObjectInstaller resolve chain</h3>

<p><strong><code class="language-plaintext highlighter-rouge">ObjectInstaller</code></strong> 는 다음 순서로 resolve함.</p>

<ol>
  <li>Local registry (<code class="language-plaintext highlighter-rouge">RegistryKey</code>)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">_parentScope</code></strong> 가 설정된 경우 — 해당 installer의 체인 (재귀)</li>
  <li>그 외 — <strong><code class="language-plaintext highlighter-rouge">SceneInstaller.Instance</code></strong> 이후 <strong><code class="language-plaintext highlighter-rouge">MasterInstaller.Instance</code></strong></li>
</ol>

<p><code class="language-plaintext highlighter-rouge">_parentScope</code> 는 선택 사항이며, 중첩 installer 그래프(풀, 격리 서브트리)를 위해 <strong>serialize됨</strong>.</p>

<hr />

<h1 id="per-installer-api-surface">Per-installer API surface</h1>

<p><strong><code class="language-plaintext highlighter-rouge">IScope</code></strong> 외에 존재하는 public 진입점 정리 (editor API는 명시된 경우만 포함):</p>

<h3 id="masterinstaller"><code class="language-plaintext highlighter-rouge">MasterInstaller</code></h3>

<table>
  <thead>
    <tr>
      <th>분류</th>
      <th>Members</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>접근</td>
      <td><strong><code class="language-plaintext highlighter-rouge">Instance</code></strong>, <strong><code class="language-plaintext highlighter-rouge">ResolveStatic&lt;T&gt;()</code></strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IScope</code></td>
      <td><strong><code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code></strong>, <strong><code class="language-plaintext highlighter-rouge">UnregisterTickable</code></strong> 변형 포함 전체 계약</td>
    </tr>
    <tr>
      <td>Registry 정리</td>
      <td><strong><code class="language-plaintext highlighter-rouge">Unregister(Component comp)</code></strong> (해당 인스턴스의 모든 키)</td>
    </tr>
    <tr>
      <td>Try 패턴</td>
      <td><strong><code class="language-plaintext highlighter-rouge">TryResolve&lt;T&gt;(out T)</code></strong>, <strong><code class="language-plaintext highlighter-rouge">TryResolveAs&lt;T&gt;(out T)</code></strong> (<code class="language-plaintext highlighter-rouge">ObjectInstaller</code>에는 없음)</td>
    </tr>
    <tr>
      <td>Editor</td>
      <td><strong><code class="language-plaintext highlighter-rouge">RefreshRegistry()</code></strong>, <strong><code class="language-plaintext highlighter-rouge">GetGlobalComponent</code> / <code class="language-plaintext highlighter-rouge">GetGlobalComponent&lt;T&gt;</code></strong></td>
    </tr>
  </tbody>
</table>

<h3 id="sceneinstaller"><code class="language-plaintext highlighter-rouge">SceneInstaller</code></h3>

<table>
  <thead>
    <tr>
      <th>분류</th>
      <th>Members</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>접근</td>
      <td><strong><code class="language-plaintext highlighter-rouge">Instance</code></strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IScope</code></td>
      <td>전체 계약</td>
    </tr>
    <tr>
      <td>정책</td>
      <td><strong><code class="language-plaintext highlighter-rouge">SceneExitPolicy</code></strong> 필드 (Clear / Preserve)</td>
    </tr>
    <tr>
      <td>Registry 정리</td>
      <td><strong><code class="language-plaintext highlighter-rouge">Unregister(Component comp)</code></strong></td>
    </tr>
    <tr>
      <td>Try 패턴</td>
      <td><strong><code class="language-plaintext highlighter-rouge">TryResolve&lt;T&gt;(out T)</code></strong>, <strong><code class="language-plaintext highlighter-rouge">TryResolveAs&lt;T&gt;(out T)</code></strong></td>
    </tr>
    <tr>
      <td>Editor</td>
      <td><strong><code class="language-plaintext highlighter-rouge">RefreshSceneRegistry()</code></strong></td>
    </tr>
  </tbody>
</table>

<h3 id="objectinstaller"><code class="language-plaintext highlighter-rouge">ObjectInstaller</code></h3>

<table>
  <thead>
    <tr>
      <th>분류</th>
      <th>Members</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IScope</code></td>
      <td><strong><code class="language-plaintext highlighter-rouge">Register</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Register&lt;TBind&gt;</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Unregister(Type[, id])</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Resolve</code> / <code class="language-plaintext highlighter-rouge">TryResolve</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code></strong>, <strong><code class="language-plaintext highlighter-rouge">UnregisterTickable</code></strong> — <strong><code class="language-plaintext highlighter-rouge">Unregister(Component)</code></strong> 없음, generic <strong><code class="language-plaintext highlighter-rouge">TryResolve&lt;T&gt;(out T)</code></strong> 없음</td>
    </tr>
    <tr>
      <td>Hierarchy injection</td>
      <td><strong><code class="language-plaintext highlighter-rouge">InjectTarget</code></strong>, <strong><code class="language-plaintext highlighter-rouge">TryInjectTarget</code></strong>, <strong><code class="language-plaintext highlighter-rouge">InjectGameObject</code></strong>, <strong><code class="language-plaintext highlighter-rouge">SpawnInjected</code></strong> 오버로드들</td>
    </tr>
    <tr>
      <td>Pooling</td>
      <td><strong><code class="language-plaintext highlighter-rouge">InjectTargetFromPool</code></strong>, <strong><code class="language-plaintext highlighter-rouge">ReleaseTargetToPool</code></strong></td>
    </tr>
    <tr>
      <td>Scope graph</td>
      <td><strong><code class="language-plaintext highlighter-rouge">_parentScope</code></strong> (serialize됨)</td>
    </tr>
    <tr>
      <td>Editor</td>
      <td><strong><code class="language-plaintext highlighter-rouge">BakeDependencies()</code></strong></td>
    </tr>
  </tbody>
</table>

<h3 id="objectinstaller--injection-spawn-pooling-동작-참조"><code class="language-plaintext highlighter-rouge">ObjectInstaller</code> — injection, spawn, pooling 동작 참조</h3>

<p>이 메서드들은 런타임 injection과 동일한 <strong><code class="language-plaintext highlighter-rouge">TypeDataCache</code></strong> 필드 플랜을 사용함.<br />
<strong><code class="language-plaintext highlighter-rouge">[Inject]</code></strong> (local bake)는 여기에서 적용되지 않으며, <strong><code class="language-plaintext highlighter-rouge">[SceneInject]</code></strong> 이후 <strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code></strong> 순으로 처리됨.</p>

<table>
  <thead>
    <tr>
      <th>메서드</th>
      <th>시그니처 / 비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">InjectTarget</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">void InjectTarget(MonoBehaviour target)</code></strong> — 얇은 래퍼: <strong><code class="language-plaintext highlighter-rouge">TryInjectTarget(target, logWarnings: true, isReinjection: false)</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">TryInjectTarget</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">bool TryInjectTarget(MonoBehaviour target, bool logWarnings = true, bool isReinjection = false)</code></strong> — 각 inject 필드를 installer chain (local → parent / scene / master)의 <strong><code class="language-plaintext highlighter-rouge">Resolve(field.FieldType, field.Id)</code></strong> 로 resolve. <strong>반환값:</strong> 필수 필드가 모두 resolve된 경우에만 <code class="language-plaintext highlighter-rouge">true</code>; optional miss는 <code class="language-plaintext highlighter-rouge">false</code>를 강제하지 않음. <strong><code class="language-plaintext highlighter-rouge">logWarnings</code>:</strong> <code class="language-plaintext highlighter-rouge">true</code> 시 필수 실패에 <code class="language-plaintext highlighter-rouge">Debug.LogWarning</code> 출력. <strong><code class="language-plaintext highlighter-rouge">isReinjection</code>:</strong> <code class="language-plaintext highlighter-rouge">true</code> 시 <strong><code class="language-plaintext highlighter-rouge">IInjected.OnInjected</code></strong> 를 건너뜀; 성공 시 <strong><code class="language-plaintext highlighter-rouge">IPoolInjectionTarget.OnPoolGet</code></strong> 이 대신 실행됨 (구현된 경우).</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">InjectGameObject</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">void InjectGameObject(GameObject root, bool includeInactive = true)</code></strong> — <strong><code class="language-plaintext highlighter-rouge">GetComponentsInChildren&lt;MonoBehaviour&gt;</code></strong> 이후 각 인스턴스에 <strong><code class="language-plaintext highlighter-rouge">InjectTarget</code></strong> 적용.</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">SpawnInjected</code></strong> (prefab)</td>
      <td><strong><code class="language-plaintext highlighter-rouge">GameObject SpawnInjected(GameObject prefab)</code></strong> — identity에서 <code class="language-plaintext highlighter-rouge">Instantiate</code>; <strong><code class="language-plaintext highlighter-rouge">InjectGameObject(instance)</code></strong> 실행.</td>
    </tr>
    <tr>
      <td> </td>
      <td><strong><code class="language-plaintext highlighter-rouge">GameObject SpawnInjected(GameObject prefab, Transform parent)</code></strong> — parent 지정 버전.</td>
    </tr>
    <tr>
      <td> </td>
      <td><strong><code class="language-plaintext highlighter-rouge">GameObject SpawnInjected(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null)</code></strong> — 위치 지정 instantiate + <strong><code class="language-plaintext highlighter-rouge">InjectGameObject</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">SpawnInjected</code></strong> (<code class="language-plaintext highlighter-rouge">MonoBehaviour</code>)</td>
      <td><strong><code class="language-plaintext highlighter-rouge">T SpawnInjected&lt;T&gt;(T prefab) where T : MonoBehaviour</code></strong> — <code class="language-plaintext highlighter-rouge">Instantiate(prefab)</code> + <strong><code class="language-plaintext highlighter-rouge">InjectTarget(instance)</code></strong> (단일 컴포넌트).</td>
    </tr>
    <tr>
      <td> </td>
      <td><strong><code class="language-plaintext highlighter-rouge">T SpawnInjected&lt;T&gt;(T prefab, Vector3, Quaternion, Transform parent = null)</code></strong> — 위치 지정 버전 + <strong><code class="language-plaintext highlighter-rouge">InjectTarget</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">InjectTargetFromPool</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">void InjectTargetFromPool(MonoBehaviour target)</code></strong> → <strong><code class="language-plaintext highlighter-rouge">TryInjectTarget(target, logWarnings: true, isReinjection: true)</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">ReleaseTargetToPool</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">void ReleaseTargetToPool(MonoBehaviour target)</code></strong> — <strong><code class="language-plaintext highlighter-rouge">IPoolInjectionTarget.OnPoolRelease</code></strong> 호출(있는 경우); 이후 해당 타입의 <strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code> / <code class="language-plaintext highlighter-rouge">[SceneInject]</code></strong> 필드 전체를 <code class="language-plaintext highlighter-rouge">TypeDataCache</code> setter를 통해 <strong><code class="language-plaintext highlighter-rouge">null</code></strong> 로 설정.</td>
    </tr>
  </tbody>
</table>

<p><strong><code class="language-plaintext highlighter-rouge">Register</code> / <code class="language-plaintext highlighter-rouge">Register&lt;TBind&gt;</code> 요점 (local scope):</strong> 매핑은 <strong>concrete</strong> <code class="language-plaintext highlighter-rouge">Component</code> 타입의 <strong><code class="language-plaintext highlighter-rouge">ReferralAttribute</code></strong> / <strong><code class="language-plaintext highlighter-rouge">SceneReferralAttribute</code></strong> 에서 <strong><code class="language-plaintext highlighter-rouge">BindType</code></strong> 과 <strong><code class="language-plaintext highlighter-rouge">Id</code></strong> 를 가져오며, <strong><code class="language-plaintext highlighter-rouge">Register&lt;TBind&gt;</code></strong> 가 지정되면 bind type을 재정의함.</p>

<p><strong><code class="language-plaintext highlighter-rouge">InstallerRegistryHelper</code></strong> 는 concrete 타입, 모든 <strong>mappable</strong> interface (<code class="language-plaintext highlighter-rouge">System.*</code>, <code class="language-plaintext highlighter-rouge">UnityEngine.*</code> 등 제외),<br />
 <strong><code class="language-plaintext highlighter-rouge">MonoBehaviour</code></strong> 직전까지의 base 타입 모두를 동일한 <strong><code class="language-plaintext highlighter-rouge">RegistryKey</code></strong> 로 등록함.</p>

<h3 id="typedatacache-진단--warmup"><code class="language-plaintext highlighter-rouge">TypeDataCache</code> (진단 / warmup)</h3>

<p>startup 또는 테스트에서 호출 가능한 public 헬퍼:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Warmup(Type)</code> / <code class="language-plaintext highlighter-rouge">Warmup(params Type[])</code></strong> — 필드 및 생성자 캐시를 미리 채움.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">HasAnyInjectField</code></strong>, <strong><code class="language-plaintext highlighter-rouge">HasGeneratedGlobalPlan</code></strong>, <strong><code class="language-plaintext highlighter-rouge">HasGeneratedScenePlan</code></strong>, <strong><code class="language-plaintext highlighter-rouge">HasGeneratedFactory</code></strong> — 툴링용 introspection.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">GetGlobalInjectFields</code> / <code class="language-plaintext highlighter-rouge">GetSceneInjectFields</code></strong> — 캐싱된 <strong><code class="language-plaintext highlighter-rouge">CachedInjectField</code></strong> 목록 반환.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">RegisterGenerated*</code> API는 <strong>generator 전용</strong> (<code class="language-plaintext highlighter-rouge">[EditorBrowsable(Never)]</code>).</p>

<hr />

<h1 id="constructor-injection--createt">Constructor Injection &amp; <code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code></h1>

<p><strong><code class="language-plaintext highlighter-rouge">IScope.Create&lt;T&gt;() where T : class</code></strong> 는 다음 정확한 순서로 <strong><code class="language-plaintext highlighter-rouge">InstallerRegistryHelper.CreateAndInject&lt;T&gt;</code></strong> 를 실행함.</p>

<ol>
  <li><strong>Construction</strong> — <code class="language-plaintext highlighter-rouge">TypeDataCache.TryGetGeneratedFactory</code> <strong>또는</strong> <code class="language-plaintext highlighter-rouge">[InjectConstructor]</code> / 단일 public <code class="language-plaintext highlighter-rouge">ctor</code> + <strong><code class="language-plaintext highlighter-rouge">ConstructorInfo.Invoke</code></strong>. resolve 불가한 필수 ctor 파라미터 → <strong><code class="language-plaintext highlighter-rouge">InvalidOperationException</code></strong>.</li>
  <li><strong>Field injection</strong> — 모든 <strong><code class="language-plaintext highlighter-rouge">[SceneInject]</code></strong> 필드, 이후 <strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code></strong> 필드 (component와 동일한 optional/required 규칙). 필수 미스 시 내부 <code class="language-plaintext highlighter-rouge">success</code>를 false로 설정하고 경고를 출력함.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">IInjected.OnInjected()</code></strong> — 모든 <strong>필수</strong> inject 필드가 성공한 경우 field injection <strong>내부</strong>에서 호출됨<br />
(<strong>optional</strong> 미스는 성공을 막지 않음).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">RegisterTickables(object)</code></strong> — <code class="language-plaintext highlighter-rouge">T</code>가 <strong><code class="language-plaintext highlighter-rouge">ITickable</code> / <code class="language-plaintext highlighter-rouge">IFixedTickable</code> / <code class="language-plaintext highlighter-rouge">ILateTickable</code> / <code class="language-plaintext highlighter-rouge">IScopeDestroyable</code></strong> 를 구현하면, <code class="language-plaintext highlighter-rouge">OnInjected</code> <strong>이후</strong> 호출 installer의 <strong><code class="language-plaintext highlighter-rouge">TickableRegistry</code></strong> 에 등록됨.</li>
</ol>

<p>ctor/field의 <strong>Resolve fallthrough</strong> 는 각 installer에 매칭됨 (<strong>Master</strong> → scene; <strong>Scene</strong> → master; <strong>Object</strong> → local → parent → scene → master).</p>

<hr />

<h1 id="tickables--scope-teardown">Tickables &amp; Scope Teardown</h1>

<p><strong><code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code></strong> 로 생성된 plain C# 서비스는 <code class="language-plaintext highlighter-rouge">PlayerLoop</code> 조작 없이 Unity 프레임 콜백을 받을 수 있음.</p>

<table>
  <thead>
    <tr>
      <th>Interface</th>
      <th>호출 출처</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">ITickable</code></strong></td>
      <td>Host <strong><code class="language-plaintext highlighter-rouge">Update</code></strong> → <strong><code class="language-plaintext highlighter-rouge">Tick()</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">IFixedTickable</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">FixedUpdate</code></strong> → <strong><code class="language-plaintext highlighter-rouge">FixedTick()</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">ILateTickable</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">LateUpdate</code></strong> → <strong><code class="language-plaintext highlighter-rouge">LateTick()</code></strong></td>
    </tr>
  </tbody>
</table>

<p><strong>규칙</strong></p>

<ul>
  <li><strong>host</strong>는 항상 <strong><code class="language-plaintext highlighter-rouge">Create</code></strong> 를 호출한 <strong><code class="language-plaintext highlighter-rouge">MonoBehaviour</code></strong> <strong><code class="language-plaintext highlighter-rouge">IScope</code></strong> 임.</li>
  <li>Tick은 <strong>부모 scope로 전파되지 않음</strong>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">UnregisterTickable(...)</code></strong> 은 해당 scope에서 제거함; <strong><code class="language-plaintext highlighter-rouge">Tick()</code></strong> 은 <strong>스냅샷</strong>을 사용하므로,<br />
이터레이션 중 제거는 <strong>다음 프레임</strong>에 적용됨.</li>
  <li>하나의 오브젝트가 <strong><code class="language-plaintext highlighter-rouge">ITickable</code> / <code class="language-plaintext highlighter-rouge">IFixedTickable</code> / <code class="language-plaintext highlighter-rouge">ILateTickable</code></strong> 중 여럿을 구현할 수 있으며,<br />
각 매칭 목록에 모두 등록됨.</li>
</ul>

<p><strong>Teardown 시 <code class="language-plaintext highlighter-rouge">IScopeDestroyable</code></strong></p>

<p>installer <strong><code class="language-plaintext highlighter-rouge">OnDestroy</code></strong> 에서 <strong><code class="language-plaintext highlighter-rouge">TickableRegistry.ClearWithDestroy()</code></strong> 가 실행됨.</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">_destroyables</code></strong> 를 순회하며 <strong><code class="language-plaintext highlighter-rouge">OnScopeDestroy()</code></strong> 를 호출함 (항목별 <strong><code class="language-plaintext highlighter-rouge">try/catch</code></strong> — 하나의 실패가 다른 항목을 건너뛰지 않음).</li>
  <li>tick 목록 및 내부 스냅샷 상태를 초기화함.</li>
</ol>

<p>따라서 <strong><code class="language-plaintext highlighter-rouge">OnScopeDestroy</code></strong> 는 tick 목록이 초기화되기 <strong>전에</strong> 실행되며, 프레임 단위 tick 종료 시에는 실행되지 않음.</p>

<hr />

<h1 id="user-callbacks--reference">User Callbacks — Reference</h1>

<table>
  <thead>
    <tr>
      <th>Callback</th>
      <th>대상</th>
      <th>실행 시점</th>
      <th>호출자 / 경로</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">IInjected.OnInjected()</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">IInjected</code></strong> 를 구현한 <strong><code class="language-plaintext highlighter-rouge">MonoBehaviour</code></strong> 또는 <strong><code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code></strong> 인스턴스</td>
      <td>해당 패스에서 <strong>필수</strong> <code class="language-plaintext highlighter-rouge">[GlobalInject]</code>/<code class="language-plaintext highlighter-rouge">[SceneInject]</code> 필드가 모두 적용된 후</td>
      <td><strong><code class="language-plaintext highlighter-rouge">TryInjectTarget</code></strong> (<code class="language-plaintext highlighter-rouge">!isReinjection</code>) 또는 <strong><code class="language-plaintext highlighter-rouge">CreateAndInject</code></strong> 내부 <strong><code class="language-plaintext highlighter-rouge">InjectFields</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">IPoolInjectionTarget.OnPoolGet()</code></strong></td>
      <td>pool interface를 구현한 <strong><code class="language-plaintext highlighter-rouge">MonoBehaviour</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">TryInjectTarget(..., isReinjection: true)</code></strong> 성공 후</td>
      <td><strong><code class="language-plaintext highlighter-rouge">InjectTargetFromPool</code></strong> 전용 — 동일 성공 분기에서 <strong><code class="language-plaintext highlighter-rouge">OnInjected</code></strong> 와 함께 호출되지 않음</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">IPoolInjectionTarget.OnPoolRelease()</code></strong></td>
      <td>동일</td>
      <td><strong><code class="language-plaintext highlighter-rouge">ReleaseTargetToPool</code></strong> <strong>시작</strong> 시, inject 필드를 <code class="language-plaintext highlighter-rouge">null</code> 로 초기화하기 <strong>전</strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">ReleaseTargetToPool</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">IScopeDestroyable.OnScopeDestroy()</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">Create&lt;T&gt;()</code></strong> 인스턴스 (<code class="language-plaintext highlighter-rouge">MonoBehaviour</code> 아님)</td>
      <td>installer <strong><code class="language-plaintext highlighter-rouge">OnDestroy</code></strong>, <strong><code class="language-plaintext highlighter-rouge">ClearWithDestroy</code></strong> 중</td>
      <td><strong><code class="language-plaintext highlighter-rouge">TickableRegistry</code></strong></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">IUnregistered.OnUnregistered()</code></strong></td>
      <td>임의 (interface 존재)</td>
      <td><em>현재 런타임에서 호출되지 않음</em></td>
      <td>예약됨; 현재 SDK 자동 dispatch 없음</td>
    </tr>
  </tbody>
</table>

<h3 id="iinjected--상세"><code class="language-plaintext highlighter-rouge">IInjected</code> — 상세</h3>

<ul>
  <li><strong>Required vs optional:</strong> <strong>optional</strong> 의존성 누락은 <code class="language-plaintext highlighter-rouge">success</code>를 false로 만들지 않음.<br />
모든 <strong>required</strong> 필드가 resolve된 경우 <strong><code class="language-plaintext highlighter-rouge">OnInjected</code></strong> 는 정상 실행됨.</li>
  <li><strong>Re-injection:</strong> <strong><code class="language-plaintext highlighter-rouge">isReinjection: true</code></strong> 는 <strong><code class="language-plaintext highlighter-rouge">OnInjected</code></strong> 를 <strong>건너뜀</strong>. 대상이 <strong><code class="language-plaintext highlighter-rouge">IPoolInjectionTarget</code></strong> 을 구현하면  <br />
<strong><code class="language-plaintext highlighter-rouge">OnPoolGet</code></strong> 이 대신 실행됨. 그 외에는 해당 패스에서 inject-completion 콜백이 발동되지 않음.</li>
  <li><strong>재진입성:</strong> 성공한 <strong><code class="language-plaintext highlighter-rouge">TryInjectTarget</code></strong> / 성공한 <strong><code class="language-plaintext highlighter-rouge">Create</code></strong> field phase가 발생할 때마다 <strong><code class="language-plaintext highlighter-rouge">OnInjected</code></strong> 가 다시 발동될 수 있음 — “인스턴스당 전역 1회”가 아님.</li>
</ul>

<h3 id="ipoolinjectiontarget--field-semantics"><code class="language-plaintext highlighter-rouge">IPoolInjectionTarget</code> — field semantics</h3>

<p><strong><code class="language-plaintext highlighter-rouge">OnPoolRelease</code></strong> 이후 <strong><code class="language-plaintext highlighter-rouge">ReleaseTargetToPool</code></strong> 은 <code class="language-plaintext highlighter-rouge">TypeDataCache</code> setter를 통해 해당 타입의 <strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code></strong> 및<br />
<strong><code class="language-plaintext highlighter-rouge">[SceneInject]</code></strong> 필드 전체를 <strong><code class="language-plaintext highlighter-rouge">null</code></strong> 로 할당함 — 풀 인스턴스가 stale <code class="language-plaintext highlighter-rouge">Component</code> 참조를 보유하지 않도록 함.</p>

<h3 id="iscopedestroyable--tick과의-조합"><code class="language-plaintext highlighter-rouge">IScopeDestroyable</code> — tick과의 조합</h3>

<p><code class="language-plaintext highlighter-rouge">UnityEngine.Object</code> 소멸에 의존하지 않는 plain C#의 <strong>해제</strong> (이벤트 구독 취소, 핸들 닫기 등)에 사용함.</p>

<p>동일 서비스에서 <strong><code class="language-plaintext highlighter-rouge">ITickable</code></strong> 과 함께 구현할 수 있으며,<br />
destroy 콜백은 <strong><code class="language-plaintext highlighter-rouge">ClearWithDestroy</code></strong> 에서 실행되므로 프레임별 tick 스냅샷 규칙과 독립적임.</p>

<hr />

<h1 id="lifecycle--internal-architecture">Lifecycle &amp; Internal Architecture</h1>

<p>UNInject는 <strong>엄격하게 정렬된 실행 파이프라인</strong>을 기반으로
object lifecycle을 관리함.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Compile]
UNInjectGenerator → partial setter + 플랜 registry 등록

[RuntimeInitialize SubsystemRegistration]
TypeDataCache 정적 캐시 전체 초기화

[AfterAssembliesLoaded]
생성된 플랜을 TypeDataCache에 등록

[Order -1000] MasterInstaller.Awake  → baked 리스트로부터 Global Registry 재구성
[Order -900]  SceneInstaller.Awake   → baked 리스트로부터 Scene Registry 재구성
[Order -500]  ObjectInstaller.Awake  → InjectGlobalDependencies (TypeDataCache 플랜 / 폴백)
</code></pre></div></div>

<p><strong>Safety net (Master / Scene):</strong> registry 빌드 후 첫 번째 조회 miss 시 <strong>1회</strong> 조용한 재빌드를 트리거할 수 있음.<br />
플래그는 명시적 <strong><code class="language-plaintext highlighter-rouge">Register</code></strong> / editor refresh / <strong><code class="language-plaintext highlighter-rouge">Rebuild*Registry</code></strong> 에서 다시 활성화됨.</p>

<hr />

<h1 id="dynamic-object-support">Dynamic Object Support</h1>

<p>runtime-spawned 오브젝트는 정적 오브젝트와 동일한 <strong><code class="language-plaintext highlighter-rouge">TypeDataCache</code></strong> 경로를 사용함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ObjectInstaller</span> <span class="n">scope</span> <span class="p">=</span> <span class="p">...;</span>

<span class="kt">var</span> <span class="n">instance</span> <span class="p">=</span> <span class="nf">Instantiate</span><span class="p">(</span><span class="n">enemyPrefab</span><span class="p">);</span>
<span class="n">scope</span><span class="p">.</span><span class="nf">InjectTarget</span><span class="p">(</span><span class="n">instance</span><span class="p">);</span>

<span class="n">scope</span><span class="p">.</span><span class="nf">InjectGameObject</span><span class="p">(</span><span class="n">instanceRoot</span><span class="p">,</span> <span class="n">includeInactive</span><span class="p">:</span> <span class="k">true</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">partial</code> 타입은 런타임에도 <strong>생성된</strong> 경로를 유지하므로 (generator가 실행된 경우 IL2CPP-safe),<br />
대량의 동적 생성 객체에서도 <strong>allocation 없이 매우 빠른 주입</strong>이 가능함.</p>

<hr />

<h1 id="performance--injection-paths">Performance &amp; Injection Paths</h1>

<p><strong><code class="language-plaintext highlighter-rouge">[Inject]</code> / bake:</strong> 해당 참조에 대해 런타임에서는 deserialization만 발생함.</p>

<table>
  <thead>
    <tr>
      <th>경로</th>
      <th>조건</th>
      <th>IL2CPP</th>
      <th>비용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Roslyn 생성 플랜</td>
      <td><code class="language-plaintext highlighter-rouge">partial</code> 선언 있음</td>
      <td>✅ 안전</td>
      <td>Dictionary 조회 + 직접 호출</td>
    </tr>
    <tr>
      <td>Expression Tree</td>
      <td><code class="language-plaintext highlighter-rouge">partial</code> 없음, Mono</td>
      <td>⚠️ 출시용 비권장</td>
      <td>1회 컴파일 후 캐싱</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FieldInfo.SetValue</code></td>
      <td>폴백</td>
      <td>⚠️ 위험</td>
      <td>매 호출마다 boxing 발생</td>
    </tr>
  </tbody>
</table>

<p>생성된 setter는 <code class="language-plaintext highlighter-rouge">[MethodImpl(AggressiveInlining)]</code> 이 적용되어<br />
<strong>JIT/AOT 모두에서 캐스트 + 대입 수준의 비용</strong>으로 동작함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">MethodImpl</span><span class="p">(</span><span class="n">MethodImplOptions</span><span class="p">.</span><span class="n">AggressiveInlining</span><span class="p">)]</span>
<span class="k">internal</span> <span class="k">void</span> <span class="nf">__UNInject_Global_input</span><span class="p">(</span><span class="kt">object</span> <span class="n">__v</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">_input</span> <span class="p">=</span> <span class="p">(</span><span class="n">IInputService</span><span class="p">)</span><span class="n">__v</span><span class="p">;</span>
</code></pre></div></div>

<p><img width="787" height="846" alt="image" src="https://github.com/user-attachments/assets/1d2c472b-cd44-4df9-9630-610be6236103" /></p>

<p>한 번 캐싱되면
10,000개의 객체 injection도 <strong>마이크로초 단위로 수행 가능함</strong>.</p>

<p>이는 복잡한 UI 생성이나
대규모 레벨 인스턴스 생성 시 발생하는
<strong>GC spike 및 frame drop 문제를 방지함</strong>.</p>

<hr />

<h1 id="editor-supports">Editor Supports</h1>

<blockquote>
  <p>dependency graph를 관리하고 시각화하기 위한 직관적인 Inspector 도구를 제공함.</p>
</blockquote>

<h2 id="inspector-tooling">Inspector tooling</h2>

<p>커스텀 <strong><code class="language-plaintext highlighter-rouge">Editor</code></strong> 스크립트가 <strong><code class="language-plaintext highlighter-rouge">MasterInstaller</code></strong>, <strong><code class="language-plaintext highlighter-rouge">SceneInstaller</code></strong>, <strong><code class="language-plaintext highlighter-rouge">ObjectInstaller</code></strong> inspector를 구동함<br />
(registry refresh 버튼, 의존성 시각화, 읽기 전용 <strong><code class="language-plaintext highlighter-rouge">[Inject]</code></strong> drawer, optional 필드 색상 표기).</p>

<table>
  <thead>
    <tr>
      <th>색상</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>🟢 초록</td>
      <td>레지스트리에 등록됨 (정상)</td>
    </tr>
    <tr>
      <td>⚫ 회색</td>
      <td>Optional — 미등록 (의도된 상태)</td>
    </tr>
    <tr>
      <td>🟠 주황 / 빨강</td>
      <td>Required — 미등록 (주의 필요)</td>
    </tr>
  </tbody>
</table>

<p>의존성 목록은 <strong>잘리지 않음</strong> — 목록이 길다는 것은 UI 노이즈가 아니라 아키텍처 신호로 취급됨.</p>

<p>런타임 <strong><code class="language-plaintext highlighter-rouge">ContextMenu</code></strong> 항목: <strong><code class="language-plaintext highlighter-rouge">Bake Dependencies</code></strong> (<code class="language-plaintext highlighter-rouge">ObjectInstaller</code>), <strong><code class="language-plaintext highlighter-rouge">Refresh Global Registry</code></strong> (<code class="language-plaintext highlighter-rouge">MasterInstaller</code>),<br />
<strong><code class="language-plaintext highlighter-rouge">Refresh Scene Registry</code></strong> (<code class="language-plaintext highlighter-rouge">SceneInstaller</code>) — inspector 워크플로와 동일한 동작.</p>

<p align="center">
  <img src="https://github.com/user-attachments/assets/84a1af2b-aa68-4722-ad2e-e9a15ed6c0de" width="32%" />
  <img src="https://github.com/user-attachments/assets/3858e28f-6332-4048-b80a-1bbcb7fa26d1" width="32%" />
  <img src="https://github.com/user-attachments/assets/a9e36b9f-7d35-416c-a5e8-145f13e5ef59" width="32%" />
</p>

<h2 id="dependency-graph-uninjectgraphwindow">Dependency Graph (<code class="language-plaintext highlighter-rouge">UNInjectGraphWindow</code>)</h2>

<p><strong>메뉴:</strong> <strong><code class="language-plaintext highlighter-rouge">Window &gt; UNInject &gt; Dependency Graph</code></strong></p>

<p><strong>클래스:</strong> <strong><code class="language-plaintext highlighter-rouge">UNInjectGraphWindow</code></strong> — <strong>현재 editor 상태</strong> (저장된 asset 아님)에 대한 <strong><code class="language-plaintext highlighter-rouge">GraphView</code></strong> (UI Toolkit)를 빌드함.</p>

<p><strong>표시 항목</strong></p>

<table>
  <thead>
    <tr>
      <th>노드 / 요소</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Installer</strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">MasterInstaller [Global]</code></strong> 및 <strong><code class="language-plaintext highlighter-rouge">SceneInstaller [Scene]</code></strong> (보라색 헤더).</td>
    </tr>
    <tr>
      <td><strong>Referral</strong></td>
      <td><strong>유효한 scene</strong>에서 <strong><code class="language-plaintext highlighter-rouge">[Referral]</code></strong> 또는 <strong><code class="language-plaintext highlighter-rouge">[SceneReferral]</code></strong> 을 가진 컴포넌트. 시안 = global, 초록 = scene. referral에 named <code class="language-plaintext highlighter-rouge">Id</code>가 있으면 레이블에 <strong><code class="language-plaintext highlighter-rouge">[id:"…"]</code></strong> 가 포함됨.</td>
    </tr>
    <tr>
      <td><strong>Inject target</strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code></strong> 또는 <strong><code class="language-plaintext highlighter-rouge">[SceneInject]</code></strong> 필드가 하나 이상 있는 타입. 모든 필수 의존성 edge가 resolve되면 <strong>금색</strong>, 하나라도 <strong><code class="language-plaintext highlighter-rouge">RegistryKey(fieldType, fieldId)</code></strong> 가 referral map에 없으면 <strong>빨간색</strong>.</td>
    </tr>
    <tr>
      <td><strong>Edges</strong></td>
      <td><strong>Installer → Referral</strong> = 등록 edge. <strong>Referral → Inject target</strong> = 의존성 edge; 해당 consumer 타입에 해당 필드에 대한 <strong>Roslyn 생성 플랜</strong>이 있으면 <strong>초록</strong> 틴트 (<strong><code class="language-plaintext highlighter-rouge">TypeDataCache.HasGeneratedGlobalPlan</code> / <code class="language-plaintext highlighter-rouge">HasGeneratedScenePlan</code></strong>), <strong>폴백</strong>을 사용할 경우 <strong>노란색</strong>.</td>
    </tr>
  </tbody>
</table>

<p><strong>툴바:</strong> <strong>Refresh</strong> 는 로드된 어셈블리와 scene 컴포넌트를 재스캔함 (registry refresh 또는 코드 변경 후 사용).</p>

<p>Inject target의 어셈블리 스캔은 다른 진단과 동일한 <strong><code class="language-plaintext highlighter-rouge">ShouldSkipAssembly</code></strong> 정책을 사용하여 프레임워크 어셈블리를 무시함.</p>

<p><img width="1232" height="623" alt="image" src="https://github.com/user-attachments/assets/a77b35c6-45e0-4277-a611-71929ff461d8" /></p>

<h2 id="bake-validator-uninjectbakevalidator">Bake Validator (<code class="language-plaintext highlighter-rouge">UNInjectBakeValidator</code>)</h2>

<p><strong>메뉴:</strong> <strong><code class="language-plaintext highlighter-rouge">Window &gt; UNInject &gt; Validate Bake</code></strong></p>

<p><strong>클래스:</strong> <strong><code class="language-plaintext highlighter-rouge">UNInjectBakeValidator</code></strong> — <strong><code class="language-plaintext highlighter-rouge">IPreprocessBuildWithReport</code></strong> (<code class="language-plaintext highlighter-rouge">callbackOrder == 0</code>) 를 구현하므로, <strong>각 player build 전에 자동으로도 실행됨</strong>.</p>

<p><strong>알고리즘 요약</strong></p>

<ol>
  <li>로드된 <strong>game</strong> 어셈블리에서 <strong>non-optional</strong> <strong><code class="language-plaintext highlighter-rouge">[GlobalInject]</code></strong> / <strong><code class="language-plaintext highlighter-rouge">[SceneInject]</code></strong> 필드를 스캔함.
    <ul>
      <li><strong>키 없는</strong> 필드는 <strong><code class="language-plaintext highlighter-rouge">field.FieldType</code></strong> 을 기여함.</li>
      <li><strong>Named</strong> 필드는 <strong><code class="language-plaintext highlighter-rouge">RegistryKey(field.FieldType, id)</code></strong> 를 기여함 (키 없는 패스와 혼용되지 않음).</li>
    </ul>
  </li>
  <li><strong><code class="language-plaintext highlighter-rouge">EditorBuildSettings</code></strong> 에 활성화된 <strong>각 scene</strong>에 대해 additively 열고 (또는 이미 열린 scene 사용),<br />
<strong><code class="language-plaintext highlighter-rouge">SerializedObject</code></strong> 를 통해 <strong><code class="language-plaintext highlighter-rouge">MasterInstaller._globalReferrals</code></strong> / <strong><code class="language-plaintext highlighter-rouge">SceneInstaller._sceneReferrals</code></strong> 를 읽어 커버되는 <strong>concrete</strong> 및 <strong>abstract/interface</strong> 키를 표시함 (런타임 <strong><code class="language-plaintext highlighter-rouge">RegisterTypeMappings</code></strong> widening을 반영).</li>
  <li>baked 리스트에 <strong>매칭 referral이 없는</strong> 필수 inject마다 <strong><code class="language-plaintext highlighter-rouge">Debug.LogError</code></strong> 를 출력함.</li>
</ol>

<p><strong>결과</strong></p>

<ul>
  <li><strong>기본:</strong> 에러가 로그되고 빌드는 <strong>계속됨</strong> (strict mode 미적용 시).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">UNINJECT_STRICT_BUILD</code></strong>: 검증 에러 발생 시 <strong><code class="language-plaintext highlighter-rouge">BuildFailedException</code></strong> 을 throw하고 <strong>빌드를 중단함</strong>.</li>
</ul>

<p>메뉴 커맨드는 <strong><code class="language-plaintext highlighter-rouge">DisplayDialog</code></strong> 요약을 표시하며; <strong><code class="language-plaintext highlighter-rouge">RunValidation()</code></strong> 은 <strong><code class="language-plaintext highlighter-rouge">public static</code></strong> 이므로 CI나 커스텀 editor 버튼에서<br />
동일 검사를 호출할 수 있음.</p>

<h2 id="play-mode-guards-editor-only">Play mode guards (editor-only)</h2>

<table>
  <thead>
    <tr>
      <th>타입</th>
      <th>트리거</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">MasterInstallerPlayModeGuard</code></strong></td>
      <td>Edit Mode → Play 전환</td>
      <td>scene에 <strong><code class="language-plaintext highlighter-rouge">MasterInstaller</code></strong> 가 존재하고 <strong><code class="language-plaintext highlighter-rouge">_globalReferrals.arraySize == 0</code></strong> 이면 <strong>empty global registry</strong> 경고를 출력함.</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">UNInjectFallbackGuard</code></strong></td>
      <td>동일 전환</td>
      <td><strong>Expression Tree / reflection</strong> 경로에 의존하는 타입 목록 출력 (missing <code class="language-plaintext highlighter-rouge">partial</code>, 생성 플랜 없는 런타임 register 후보, 생성자 폴백 사용 등).</td>
    </tr>
  </tbody>
</table>

<p>두 가드는 독립적 hook이며, 동일한 Play 진입에서 모두 발동될 수 있음.</p>

<h2 id="scripting-define-symbols-선택적-툴링">Scripting define symbols (선택적 툴링)</h2>

<table>
  <thead>
    <tr>
      <th>Symbol</th>
      <th>효과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">UNINJECT_STRICT_BUILD</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">UNInjectBakeValidator</code></strong> 가 baked 리스트에 required referral이 없을 때 <strong><code class="language-plaintext highlighter-rouge">BuildFailedException</code></strong> 을 throw함.</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">UNINJECT_PROFILING</code></strong></td>
      <td><strong><code class="language-plaintext highlighter-rouge">UNInjectProfiler</code></strong> 를 컴파일함. <strong><code class="language-plaintext highlighter-rouge">ObjectInstaller.TryInjectTarget</code></strong> 이 타입별 <strong><code class="language-plaintext highlighter-rouge">Stopwatch</code></strong> 타이밍을 기록하며; <strong><code class="language-plaintext highlighter-rouge">UNInjectProfiler.PrintReport()</code></strong>, <strong><code class="language-plaintext highlighter-rouge">GetStats()</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Reset()</code></strong> 으로 조회 가능함 (stats는 Play 시 <strong><code class="language-plaintext highlighter-rouge">SubsystemRegistration</code></strong> 에서도 reset됨). 주입 비용을 의도적으로 측정하는 경우가 아니라면 출시 빌드에서 제외할 것.</td>
    </tr>
  </tbody>
</table>

<p><strong>Profiler public API</strong> (symbol이 설정된 경우에만): <strong><code class="language-plaintext highlighter-rouge">RecordInjection(Type, double)</code></strong> (<strong><code class="language-plaintext highlighter-rouge">TryInjectTarget</code></strong> 에서 호출),<br />
<strong><code class="language-plaintext highlighter-rouge">GetStats()</code></strong>, <strong><code class="language-plaintext highlighter-rouge">PrintReport()</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Reset()</code></strong>.</p>

<hr />

<h1 id="common-editor-warnings">Common Editor Warnings</h1>

<p><strong>Empty global registry (Play)</strong><br />
<code class="language-plaintext highlighter-rouge">[MasterInstaller] Global registry is empty...</code> — <strong>Refresh Global Registry</strong> 실행 필요. (<code class="language-plaintext highlighter-rouge">MasterInstallerPlayModeGuard</code>)</p>

<p><strong>Missing <code class="language-plaintext highlighter-rouge">partial</code> (Play)</strong><br />
<code class="language-plaintext highlighter-rouge">[UNInject] ... Expression Tree fallback ...</code> — IL2CPP 전에 표시된 타입에 <code class="language-plaintext highlighter-rouge">partial</code> 추가 필요. (<code class="language-plaintext highlighter-rouge">UNInjectFallbackGuard</code>)</p>

<hr />

<h1 id="참고-및-향후-업데이트-방향성">참고 및 향후 업데이트 방향성</h1>

<h3 id="현재-지원하는-컴파일러">현재 지원하는 컴파일러</h3>

<p><strong>IL2CPP / AOT 환경을 공식 지원함.</strong></p>

<p>Roslyn Source Generator가 생성한 코드는 <code class="language-plaintext highlighter-rouge">Expression.Compile()</code>을 사용하지 않으므로<br />
<strong>모든 AOT 플랫폼(iOS, 콘솔 등)에서 안전하게 동작함</strong>.</p>

<p><code class="language-plaintext highlighter-rouge">partial</code> 선언이 없는 타입에 한해 Expression Tree 폴백이 적용되며,<br />
이 경우 <code class="language-plaintext highlighter-rouge">UNInjectFallbackGuard</code>가 Play 진입 시 명시적으로 경고를 출력함.</p>

<p>릴리즈 브랜치에서는 <strong><code class="language-plaintext highlighter-rouge">UNINJECT_STRICT_BUILD</code></strong> 를 활성화하여,<br />
<strong><code class="language-plaintext highlighter-rouge">EditorBuildSettings</code></strong> scene에 serialized registry 리스트에 없는 referral을 참조하는 consumer가 있을 때 빌드가 실패하도록 설정할 것.</p>

<hr />

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>──────────────────────────────────────────────────────────────────────
1.1.0 — IL2CPP 완전 지원 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
Roslyn Source Generator 도입. partial 클래스를 통한 IL2CPP 안전 setter 자동 생성.
UNInjectFallbackGuard 추가. Inspector Optional 3단계 표기. 의존성 전체 목록 표시.

ALL PASSED  (24 + 51 tests)
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>──────────────────────────────────────────────────────────────────────
1.1.1 — 내부 무결성 강화 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
공개 API 변경 없음. 동작 결과 변경 없음.
모든 개선은 구조적 견고성과 테스트 커버리지 확보에 집중된 내부 작업임.

InstallerRegistryHelper 도입 (신규 internal 클래스)
  RegisterTypeMappings / IsMappableAbstraction / TryAdd 를
  MasterInstaller, SceneInstaller 양쪽에서 추출하여 단일 헬퍼로 통합.
  정책 변경 시 한 곳만 수정하면 됨.

Safety Net 방어 강화 — MasterInstaller.Resolve()
  기존: 캐시 미스 시마다 _globalReferrals 전체 순회 무조건 실행.
  개선: armed/disarmed 패턴 (_safetyNetArmed) 도입.
  레지스트리 빌드 사이클당 복구 시도 1회 제한, 명시적 리빌드 시 재무장.
  무장 해제 후 미스는 Dictionary 조회 + bool 체크만으로 처리됨.

어셈블리 격리 — 3개 독립 asmdef 단위
  UNInject.Runtime / UNInject.Editor / UNInject.Tests.EditMode
  에디터 코드가 플레이어 빌드에서 구조적으로 제외됨.

EditMode 단위 테스트 43개 신규 추가
  InstallerRegistryHelper (18개): 필터 정책, 충돌/null 처리, 매핑 경로.
  TypeDataCache (25개): 필드 수집, 캐시 동일성, setter 쓰기 검증, Warmup, null 방어.

정적 캐시 오염 수정
  HasGeneratedPlan 테스트가 실행 순서에 따라 비결정론적으로 실패하던 문제 해소.
  읽기 대상과 쓰기 대상을 센티넬 타입으로 구조적으로 분리함.

ALL PASSED  (43 EditMode 단위 테스트)
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>──────────────────────────────────────────────────────────────────────
2.1.0 — Scope/Lifetime 아키텍처 완성 (릴리즈 완료)
──────────────────────────────────────────────────────────────────────
IScope 인터페이스 도입 (MasterInstaller / SceneInstaller / ObjectInstaller 공통 계약).

Pure C# service layer:
  IScope.Create&lt;T&gt;() — 생성자 주입 + 필드 주입 + Tickable 등록 통합 경로.
  [InjectConstructor] attribute 및 생성자 파라미터 [GlobalInject] / [SceneInject] 지원.
  IScopeDestroyable / ITickable / IFixedTickable / ILateTickable 인터페이스 추가.
  IPoolInjectionTarget (OnPoolGet / OnPoolRelease) 및 풀링 API 추가.

Named bindings — RegistryKey = (Type, Id) 구조로 전환.
  [Referral("id")] / [SceneReferral("id")] / [GlobalInject("id")] / [SceneInject("id")] 지원.

SceneExitPolicy (Clear / Preserve) — SceneInstaller 언로드 동작 정책 제어.

SpawnInjected 오버로드 확장 (GameObject / MonoBehaviour, 위치/부모 지정 변형).
ReleaseTargetToPool — 풀 반환 시 inject 필드 자동 null 처리.

Editor tooling 확장:
  UNInjectGraphWindow — Dependency Graph (GraphView, UI Toolkit).
  UNInjectBakeValidator — IPreprocessBuildWithReport 기반 빌드 전 자동 검증.
  UNINJECT_STRICT_BUILD / UNINJECT_PROFILING scripting define symbol 추가.
  UNInjectProfiler — 타입별 주입 비용 Stopwatch 측정 (UNINJECT_PROFILING 시).
</code></pre></div></div>

<hr />]]></content><author><name>황준혁</name></author><category term="sdks" /><category term="Unity" /><category term="OpenSource" /><category term="C#" /><summary type="html"><![CDATA[결정론적 DI 솔루션. 'Roslyn Source Generator 를 활용한 Resolve']]></summary></entry><entry><title type="html">Power Pool</title><link href="https://nightwish-0827.github.io/sdks/powerpool/" rel="alternate" type="text/html" title="Power Pool" /><published>2026-03-05T00:00:00+00:00</published><updated>2026-03-05T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/sdks/powerpool</id><content type="html" xml:base="https://nightwish-0827.github.io/sdks/powerpool/"><![CDATA[<h1 id="powerpool--high-performance-unity-object-pooling-sdk">PowerPool – High-Performance Unity Object Pooling SDK</h1>

<p><img src="https://img.shields.io/badge/unity-2021.3%2B-black" alt="" />
<img src="https://img.shields.io/badge/license-MIT-blue" alt="" /></p>

<blockquote>
  <p><strong>PowerPool</strong>은 Unity를 위한 <strong>고성능 오브젝트 풀링 프레임워크</strong>임.
<strong>zero-allocation runtime</strong>, <strong>O(1) 반환 복잡도</strong>,<br />
그리고 <strong>메모리 안전 라이프사이클 제어</strong>를 목표로 설계되었음.</p>
</blockquote>

<p><strong>스택 기반 빌더 패턴</strong>, <strong>버전 기반 안전 검증</strong>,
그리고 <strong>캐시 친화적 데이터 구조</strong>를 결합하여<br />
PowerPool은 기존 풀링 구현에서 흔히 발생하는 <strong>GC 비용</strong>이나
<strong>런타임 조회 오버헤드 없이</strong> 매우 효율적인 객체 생성을 처리.</p>

<p>단순히 오브젝트를 enable/disable 하는 기존 풀링 시스템과 달리
PowerPool은 <strong>성능</strong>, <strong>메모리 안정성</strong>, <strong>개발 편의성</strong>을 중심으로 <br />
설계된 <strong>완전한 라이프사이클 아키텍처</strong>를 제공함.</p>

<p><img width="797" height="90" alt="image" src="https://github.com/user-attachments/assets/67a707ab-46cb-4bb6-bc4a-39dfe6650b54" /></p>

<p><code class="language-plaintext highlighter-rouge">https://github.com/NightWish-0827/PowerPool.git?path=/com.nightwishlab.powerpool</code></p>

<p>UPM Add package from git URL</p>

<hr />

<h1 id="table-of-contents">Table of Contents</h1>

<ul>
  <li>
    <p><a href="#core-features">Core Features</a></p>
  </li>
  <li>
    <p><a href="#api-reference--usage">API Reference &amp; Usage</a></p>
  </li>
  <li>
    <p><a href="#lifecycle--internal-architecture">Lifecycle &amp; Internal Architecture</a></p>
  </li>
  <li>
    <p><a href="#telemetry--runtime-statistics">Telemetry &amp; Runtime Statistics</a></p>
  </li>
  <li>
    <p><a href="#performance-comparison">Performance Comparison</a></p>
  </li>
  <li>
    <p><a href="#editor-supports">Editor Supports</a></p>
  </li>
  <li>
    <p><a href="#editor-case">Editor Case</a></p>
  </li>
  <li>
    <p><a href="#마치며">마치며</a></p>
  </li>
</ul>

<hr />

<h1 id="core-features">Core Features</h1>

<h3 id="zero-allocation-fluent-builder">Zero Allocation Fluent Builder</h3>

<p>PowerPool은 <code class="language-plaintext highlighter-rouge">ref struct</code> 기반의 <strong>스택 할당 빌더 패턴</strong>을 도입했음.</p>

<p>이를 통해 <strong>위치</strong>, <strong>회전</strong>, <strong>부모 Transform</strong>, <strong>초기화 콜백</strong> 등의
스폰 설정을 <strong>fluent API</strong> 형태로 구성할 수 있음.</p>

<p>이 과정에서 <strong>힙 할당이 전혀 발생하지 않음</strong>.</p>

<hr />

<h3 id="gc-free-runtime-operation">GC-Free Runtime Operation</h3>

<p>객체 <strong>Rent</strong> 및 <strong>Return</strong> 연산은 완전히 <strong>allocation-free</strong>로 동작함.</p>

<p>반환 핸들은 경량 구조체인
<code class="language-plaintext highlighter-rouge">readonly struct PoolHandle&lt;T&gt;</code>로 구현되어 있음.</p>

<p>이를 통해 풀링 과정에서 <strong>GC 스파이크가 발생하지 않도록 보장함</strong>.</p>

<hr />

<h3 id="o1-return-complexity">O(1) Return Complexity</h3>

<p>일반적인 풀링 시스템은 객체 반환 시
어떤 풀에 속한 객체인지 판단하기 위해 <strong>Dictionary 조회</strong>가 필요함.</p>

<p>PowerPool은 이러한 오버헤드를 제거했음.</p>

<p>각 <code class="language-plaintext highlighter-rouge">PoolHandle&lt;T&gt;</code>는 내부적으로
자신을 생성한 <strong>Pool 인스턴스에 대한 직접 참조</strong>를 보유함.</p>

<p>따라서 객체 반환은 <strong>항상 O(1) 상수 시간</strong>으로 수행됨.</p>

<ul>
  <li>해시 조회 없음</li>
  <li>타입 검사 없음</li>
  <li>런타임 풀 탐색 없음</li>
</ul>

<hr />

<h3 id="anti-zombie-object-protection">Anti-Zombie Object Protection</h3>

<p>풀링 시스템에서 가장 위험한 버그 중 하나는
<strong>Zombie Reference 문제</strong>임.</p>

<p>이는 이미 풀로 반환된 객체를 코드가 계속 접근하는 상황을 의미함.</p>

<p>PowerPool은 <strong>generation 기반 검증 시스템</strong>을 통해
이 문제를 원천적으로 방지함.</p>

<p>각 풀링 객체는 <strong>PowerPoolTracker</strong>를 통해
단조 증가하는 <strong>Version 값</strong>을 관리함.</p>

<p>핸들이 발급될 때</p>

<ul>
  <li>현재 Version이 핸들에 복사됨</li>
  <li>반환 시 Tracker Version이 증가함</li>
</ul>

<p>이미 반환된 핸들이 접근을 시도하면</p>

<ul>
  <li>Version 불일치가 감지됨</li>
  <li>해당 연산이 차단됨</li>
  <li>설정에 따라 Warning 또는 Exception이 발생함</li>
</ul>

<hr />

<h3 id="cache-friendly-parallel-array-architecture">Cache-Friendly Parallel Array Architecture</h3>

<p>PowerPoolCore는 내부적으로 <strong>Parallel Array 구조</strong>를 사용함.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_items[]      → 풀링된 컴포넌트
_trackers[]   → 라이프사이클 트래커
</code></pre></div></div>

<p>두 배열은 동일한 인덱스를 공유함.</p>

<p>이를 통해 매우 빠른 조회와
캐시 친화적인 반복 처리가 가능함.</p>

<p>이 <strong>데이터 지향 구조(Data-Oriented Structure)</strong>는
일반적인 OOP 기반 풀링 구현보다 훨씬 높은 성능을 제공함.</p>

<hr />

<h3 id="zero-runtime-getcomponent-overhead">Zero Runtime GetComponent Overhead</h3>

<p><code class="language-plaintext highlighter-rouge">IPoolCleanable</code> 인터페이스를 구현한 컴포넌트는
객체 생성 시 <strong>단 한 번만 캐싱</strong>됨.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GetComponentsInChildren&lt;IPoolCleanable&gt;(true)
</code></pre></div></div>

<p>이 과정은 <strong>prewarm / instantiate 시점</strong>에만 수행됨.</p>

<p>객체가 풀로 반환될 때는</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OnReturnToPool()
</code></pre></div></div>

<p>메서드가 캐싱된 배열을 통해 <strong>직접 호출됨</strong>.</p>

<p>따라서 런타임에서 <strong>추가적인 GetComponent 탐색이 발생하지 않음</strong>.</p>

<hr />

<h3 id="independent-prefab-pool-isolation">Independent Prefab Pool Isolation</h3>

<p>PowerPool은 풀을 구분할 때
컴포넌트 타입 <code class="language-plaintext highlighter-rouge">&lt;T&gt;</code>뿐 아니라
<strong>Prefab InstanceID</strong>도 함께 사용함.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>prefab.gameObject.GetInstanceID()
</code></pre></div></div>

<p>이를 통해 동일한 컴포넌트를 사용하더라도
서로 다른 프리팹은 <strong>항상 독립된 풀로 관리됨</strong>.</p>

<p>예시</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SlimePrefab  → Pool A
OrcPrefab    → Pool B
</code></pre></div></div>

<p>두 프리팹 모두 <code class="language-plaintext highlighter-rouge">Monster</code> 스크립트를 사용하더라도
풀은 완전히 분리되어 동작함.</p>

<hr />

<h3 id="ghost-object-tracking">Ghost Object Tracking</h3>

<p>풀링 객체가 외부에서 <code class="language-plaintext highlighter-rouge">Destroy()</code> 호출로 제거되거나
씬 전환 과정에서 파괴될 경우</p>

<p>PowerPoolTracker가 이를 자동으로 감지함.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OnUnexpectedDestroyed
</code></pre></div></div>

<p>이를 통해 개발자는</p>

<ul>
  <li>라이프사이클 오류</li>
  <li>메모리 누수 가능성</li>
  <li>풀 상태 손상</li>
</ul>

<p>등의 문제를 빠르게 파악할 수 있음.</p>

<hr />

<h1 id="api-reference--usage">API Reference &amp; Usage</h1>

<p>PowerPool API는 <strong>세 가지 핵심 개념</strong>을 중심으로 설계되었음.</p>

<ul>
  <li><strong>Fluent Spawn Builder</strong></li>
  <li><strong>Handle 기반 라이프사이클 관리</strong></li>
  <li><strong>안전하고 결정적인 객체 반환</strong></li>
</ul>

<p>기존 풀링 API가 단순한 static 함수 집합을 제공하는 것과 달리
PowerPool은 <strong>구조화된 스폰 파이프라인</strong>을 제공함.</p>

<p>이를 통해 객체 생성 과정이</p>

<ul>
  <li><strong>명확하고</strong></li>
  <li><strong>안전하며</strong></li>
  <li><strong>할당 없이 동작하도록 설계되었음</strong></li>
</ul>

<hr />

<h2 id="fluent-spawn-builder">Fluent Spawn Builder</h2>

<p>객체는 <strong>fluent builder 파이프라인</strong>을 통해 생성됨.</p>

<p>빌더는 <strong><code class="language-plaintext highlighter-rouge">ref struct</code></strong>로 구현되어 있음.</p>

<p>이는 다음을 보장함.</p>

<ul>
  <li><strong>스택 전용 할당</strong></li>
  <li><strong>Zero GC Pressure</strong></li>
  <li><strong>즉시 실행</strong></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">ref struct</code>는 저장하거나 캡처할 수 없기 때문에
API 사용 패턴 역시 <strong>자연스럽게 one-shot 형태로 유도됨</strong>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PoolHandle</span><span class="p">&lt;</span><span class="n">MyComponent</span><span class="p">&gt;</span> <span class="n">handle</span> <span class="p">=</span>
    <span class="n">PowerPool</span><span class="p">.</span><span class="nf">Spawn</span><span class="p">(</span><span class="n">myPrefab</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">At</span><span class="p">(</span><span class="n">position</span><span class="p">,</span> <span class="n">rotation</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">Under</span><span class="p">(</span><span class="n">parentTransform</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">Rent</span><span class="p">();</span>
</code></pre></div></div>

<p>이 구조는 <strong>가독성을 유지하면서도
중간 힙 할당을 완전히 제거함</strong>.</p>

<hr />

<h2 id="spawn-configuration-pipeline">Spawn Configuration Pipeline</h2>

<p>최종 <code class="language-plaintext highlighter-rouge">Rent()</code> 호출 이전에
빌더를 통해 다양한 스폰 설정을 구성할 수 있음.</p>

<h3 id="transform-placement">Transform Placement</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">handle</span> <span class="p">=</span> <span class="n">PowerPool</span><span class="p">.</span><span class="nf">Spawn</span><span class="p">(</span><span class="n">prefab</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">At</span><span class="p">(</span><span class="n">spawnPosition</span><span class="p">,</span> <span class="n">spawnRotation</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Under</span><span class="p">(</span><span class="n">parentTransform</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Rent</span><span class="p">();</span>
</code></pre></div></div>

<p>지원되는 Transform 설정</p>

<ul>
  <li>spawn position</li>
  <li>spawn rotation</li>
  <li>optional parent transform</li>
</ul>

<hr />

<h3 id="initialization-callbacks">Initialization Callbacks</h3>

<p><code class="language-plaintext highlighter-rouge">With()</code> 또는 <code class="language-plaintext highlighter-rouge">WithState()</code>를 통해
스폰 시점에 추가적인 상태 값을 전달할 수 있음.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">handle</span> <span class="p">=</span> <span class="n">PowerPool</span><span class="p">.</span><span class="nf">Spawn</span><span class="p">(</span><span class="n">prefab</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">At</span><span class="p">(</span><span class="n">pos</span><span class="p">,</span> <span class="n">rot</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithState</span><span class="p">(</span>
        <span class="n">state</span><span class="p">:</span> <span class="k">new</span> <span class="nf">SpawnState</span><span class="p">(</span><span class="n">color</span><span class="p">,</span> <span class="n">impulse</span><span class="p">),</span>
        <span class="n">onInitialize</span><span class="p">:</span> <span class="k">static</span> <span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">st</span> <span class="p">=</span> <span class="p">(</span><span class="n">SpawnState</span><span class="p">)</span><span class="n">s</span><span class="p">;</span>
            <span class="n">item</span><span class="p">.</span><span class="nf">Init</span><span class="p">(</span><span class="n">st</span><span class="p">.</span><span class="n">Color</span><span class="p">,</span> <span class="n">st</span><span class="p">.</span><span class="n">Impulse</span><span class="p">);</span>
        <span class="p">})</span>
    <span class="p">.</span><span class="nf">Rent</span><span class="p">();</span>
</code></pre></div></div>

<p>이 패턴을 통해</p>

<ul>
  <li>추가 컴포넌트 탐색 없이</li>
  <li>글로벌 상태에 의존하지 않고</li>
</ul>

<p><strong>객체 초기화 데이터를 직접 주입할 수 있음</strong>.</p>

<p>콜백을 <code class="language-plaintext highlighter-rouge">static</code>으로 선언할 경우
<strong>클로저 할당 또한 발생하지 않음</strong>.</p>

<hr />

<h2 id="handle-based-lifecycle">Handle-Based Lifecycle</h2>

<p>PowerPool은 객체를 직접 반환하지 않고
<strong><code class="language-plaintext highlighter-rouge">PoolHandle&lt;T&gt;</code></strong>를 반환함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PoolHandle</span><span class="p">&lt;</span><span class="n">MyComponent</span><span class="p">&gt;</span> <span class="n">handle</span> <span class="p">=</span> <span class="n">PowerPool</span><span class="p">.</span><span class="nf">Spawn</span><span class="p">(</span><span class="n">prefab</span><span class="p">).</span><span class="nf">Rent</span><span class="p">();</span>
</code></pre></div></div>

<p>핸들에는 다음 정보가 포함됨.</p>

<ul>
  <li><strong>source pool 참조</strong></li>
  <li>객체의 <strong>tracker</strong></li>
  <li>발급 시점의 <strong>version</strong></li>
</ul>

<p>이를 통해 시스템은 다음과 같은 잘못된 사용을 감지할 수 있음.</p>

<ul>
  <li>double return</li>
  <li>zombie reference</li>
  <li>stale handle</li>
</ul>

<hr />

<h2 id="returning-objects">Returning Objects</h2>

<p>객체 반환은 여러 방식으로 수행할 수 있음.</p>

<hr />

<h3 id="explicit-return">Explicit Return</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PowerPool</span><span class="p">.</span><span class="nf">Return</span><span class="p">(</span><span class="k">ref</span> <span class="n">handle</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ref</code> 반환 패턴을 사용하여
반환 즉시 핸들이 <strong>무효화되도록 보장함</strong>.</p>

<hr />

<h3 id="instance-method-return">Instance Method Return</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">handle</span><span class="p">.</span><span class="nf">Return</span><span class="p">();</span>
</code></pre></div></div>

<p>핸들이 지역 변수로 존재할 때
가장 일반적으로 사용되는 방식임.</p>

<hr />

<h3 id="scoped-lifetime-using">Scoped Lifetime (using)</h3>

<p>핸들은 <code class="language-plaintext highlighter-rouge">IDisposable</code>을 구현하고 있음.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">fx</span> <span class="p">=</span> <span class="n">PowerPool</span><span class="p">.</span><span class="nf">Spawn</span><span class="p">(</span><span class="n">effectPrefab</span><span class="p">).</span><span class="nf">Rent</span><span class="p">())</span>
<span class="p">{</span>
    <span class="c1">// temporary effect</span>
<span class="p">}</span>
</code></pre></div></div>

<p>스코프가 종료되면
객체는 <strong>자동으로 풀로 반환됨</strong>.</p>

<hr />

<h2 id="resetting-objects-with-ipoolcleanable">Resetting Objects with IPoolCleanable</h2>

<p>객체는 <code class="language-plaintext highlighter-rouge">IPoolCleanable</code> 인터페이스를 구현하여
자신의 초기화 로직을 정의할 수 있음.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">MyEntity</span> <span class="p">:</span> <span class="n">MonoBehaviour</span><span class="p">,</span> <span class="n">IPoolCleanable</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">OnReturnToPool</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// Reset state</span>
        <span class="c1">// Clear buffs</span>
        <span class="c1">// Restore HP</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>PowerPool은 반환 과정에서
<code class="language-plaintext highlighter-rouge">OnReturnToPool()</code>을 자동 호출함.</p>

<p>이 과정에서 런타임 비용을 줄이기 위해
필요한 컴포넌트는 <strong>객체 생성 시점에 한 번만 캐싱됨</strong>.</p>

<hr />

<h2 id="prewarming-and-pool-registration">Prewarming and Pool Registration</h2>

<p>풀은 최초 Spawn 시 <strong>Lazy Initialization</strong>으로 생성될 수 있음.</p>

<p>하지만 일반적으로는 초기화 단계에서
<strong>Prewarm을 수행하는 것이 권장됨</strong>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PowerPool</span><span class="p">.</span><span class="nf">Register</span><span class="p">(</span><span class="n">prefab</span><span class="p">,</span> <span class="n">initialCapacity</span><span class="p">,</span> <span class="n">poolRoot</span><span class="p">);</span>
</code></pre></div></div>

<p>선택 파라미터를 통해 다음을 설정할 수 있음.</p>

<ul>
  <li>initial capacity</li>
  <li>pool root transform</li>
  <li>maximum pool size</li>
  <li>overflow policy</li>
</ul>

<p>예시</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PowerPool</span><span class="p">.</span><span class="nf">Register</span><span class="p">(</span>
    <span class="n">prefab</span><span class="p">,</span>
    <span class="n">initialCapacity</span><span class="p">:</span> <span class="m">32</span><span class="p">,</span>
    <span class="n">poolRoot</span><span class="p">:</span> <span class="n">poolTransform</span><span class="p">,</span>
    <span class="n">maxPoolSize</span><span class="p">:</span> <span class="m">256</span><span class="p">,</span>
    <span class="n">overflowPolicy</span><span class="p">:</span> <span class="n">PowerPoolOverflowPolicy</span><span class="p">.</span><span class="n">Expand</span><span class="p">);</span>
</code></pre></div></div>

<p>Prewarm을 통해
런타임 Instantiate로 인한 <strong>프레임 스파이크를 방지할 수 있음</strong>.</p>

<hr />

<h2 id="runtime-statistics">Runtime Statistics</h2>

<p>PowerPool은 <code class="language-plaintext highlighter-rouge">PowerPoolStats</code>를 통해
상세한 런타임 통계를 제공함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">PowerPool</span><span class="p">.</span><span class="nf">TryGetStats</span><span class="p">(</span><span class="n">prefab</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">stats</span><span class="p">))</span>
<span class="p">{</span>
    <span class="n">Debug</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span>
        <span class="s">$"Created=</span><span class="p">{</span><span class="n">stats</span><span class="p">.</span><span class="n">TotalCreated</span><span class="p">}</span><span class="s">, "</span> <span class="p">+</span>
        <span class="s">$"InPool=</span><span class="p">{</span><span class="n">stats</span><span class="p">.</span><span class="n">InPool</span><span class="p">}</span><span class="s">, "</span> <span class="p">+</span>
        <span class="s">$"Active=</span><span class="p">{</span><span class="n">stats</span><span class="p">.</span><span class="n">ActiveEstimated</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>제공되는 통계</p>

<ul>
  <li>생성된 총 객체 수</li>
  <li>활성 객체 수</li>
  <li>풀 내부 객체 수</li>
  <li>rent / return 호출 횟수</li>
  <li>pool capacity</li>
  <li>expansion 이벤트</li>
  <li>overflow 정책</li>
</ul>

<p>이를 통해 개발자는
<strong>런타임 풀 동작과 메모리 사용량을 분석할 수 있음</strong>.</p>

<hr />

<h2 id="asynchronous--delayed-return-patterns">Asynchronous / Delayed Return Patterns</h2>

<p>코루틴, 타이머, async 로직 등
지연 반환이 필요한 경우 핸들을 안전하게 저장할 수 있음.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">handle</span> <span class="p">=</span> <span class="n">PowerPool</span><span class="p">.</span><span class="nf">Spawn</span><span class="p">(</span><span class="n">prefab</span><span class="p">).</span><span class="nf">Rent</span><span class="p">();</span>

<span class="nf">StartCoroutine</span><span class="p">(</span><span class="nf">ReturnLater</span><span class="p">(</span><span class="n">handle</span><span class="p">,</span> <span class="m">2f</span><span class="p">));</span>
</code></pre></div></div>

<p>코루틴 내부</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PowerPool</span><span class="p">.</span><span class="nf">Return</span><span class="p">(</span><span class="k">ref</span> <span class="n">handle</span><span class="p">);</span>
</code></pre></div></div>

<p>핸들은 내부적으로 <strong>version 및 ownership 정보를 추적</strong>하기 때문에
잘못된 반환이나 중복 반환 상황에서도 시스템 안전성이<br />
유지됨.</p>

<hr />

<h3 id="in-practice">In Practice</h3>

<p>PowerPool은 의도적으로 <strong>유연한 설계</strong>를 채택했음.</p>

<p>다음과 같은 다양한 상황에서 활용 가능함.</p>

<ul>
  <li>수동 라이프사이클 관리</li>
  <li>scoped temporary objects</li>
  <li>coroutine 기반 이펙트</li>
  <li>고빈도 projectile 스폰</li>
  <li>대규모 엔티티 시스템</li>
</ul>

<p>Fluent Builder와 Handle 시스템은
<strong>성능</strong>, <strong>안정성</strong>, <strong>API 가독성</strong>을 동시에 유지하도록 설계되었음.</p>

<hr />

<h1 id="lifecycle--internal-architecture">Lifecycle &amp; Internal Architecture</h1>

<p>PowerPool은 구조화된 런타임 파이프라인을 통해
객체 라이프사이클을 관리함.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pool Initialization
-------------------
PowerPool.Spawn(prefab)
→ Locate or create pool instance
→ Lazy initialization if necessary

Prewarm Phase
-------------
EnsureCapacity / PrewarmAdditional
→ Allocate pool memory ahead of time
→ Prevent runtime allocation spikes

Rent Phase
----------
PoolBuilder&lt;T&gt;.Rent()
→ Retrieve inactive object
→ Assign tracker version
→ Activate object

Return Phase
------------
PoolHandle.Return()
→ Version validation
→ Call IPoolCleanable reset hooks
→ Disable and return to pool

Overflow Handling
-----------------
When pool reaches MaxPoolSize:

Expand
→ dynamically allocate additional objects

DestroyOnReturn
→ destroy objects when returned if capacity exceeded
</code></pre></div></div>

<hr />

<h1 id="telemetry--runtime-statistics">Telemetry &amp; Runtime Statistics</h1>

<p>PowerPool은 <code class="language-plaintext highlighter-rouge">PowerPoolStats</code> 구조체를 통해
런타임 텔레메트리를 제공함.</p>

<p>개발자는 다음 정보를 모니터링할 수 있음.</p>

<ul>
  <li>생성된 총 객체 수</li>
  <li>현재 풀 내부 객체 수</li>
  <li>씬에서 활성 상태인 객체 수</li>
  <li>예상치 못한 파괴 이벤트</li>
  <li>런타임 풀 확장 횟수</li>
</ul>

<p>이를 통해 팀은 <strong>메모리 사용량과 풀 동작을 실시간으로 분석할 수 있음</strong>.</p>

<hr />

<h1 id="performance-comparison">Performance Comparison</h1>

<h3 id="가비지-컬렉션-할당-비용-제거">가비지 컬렉션 할당 비용 제거</h3>

<p><strong>PowerPool</strong>은 <code class="language-plaintext highlighter-rouge">ref struct</code> 기반의 스택 할당 빌더 패턴을 도입하여<br />
객체 스폰 설정 시 힙 할당이 전혀 발생하지 않음.</p>

<p>객체의 Rent 및 Return 연산 또한 완전히 <strong>allocation-free</strong>로 동작하며,<br />
이를 통해 풀링 과정에서 발생하는 GC 스파이크를 원천 방지함.</p>

<h3 id="o1-반환-복잡도">O(1) 반환 복잡도</h3>

<p><strong>일반적인 풀링 시스템</strong>은 객체 반환 시 어떤 풀에 속한 객체인지 찾기 위해<br />
<strong>Dictionary 조회</strong>  등의 추가적인 런타임 탐색이 필요.</p>

<p>반면 <strong>PowerPool</strong>은 각 <code class="language-plaintext highlighter-rouge">PoolHandle&lt;T&gt;</code> 이 자신을 생성한 Pool 인스턴스에 대한 직접 참조를 보유하고 있어<br />
해시 조회, 타입 검사, 런타임 풀 탐색이 일절 필요 없는 O(1) 상수 시간으로 반환이 수행됨.</p>

<h3 id="런타임-resolve-getter-제거">런타임 Resolve (Getter) 제거</h3>

<p><code class="language-plaintext highlighter-rouge">IPoolCleanable</code> 인터페이스로 구현한 컴포넌트는 객체 생성 시점에서<br />
<strong>단 한 번만 캐싱</strong></p>

<p>객체가 풀로 반환될 때 캐싱된 배열을 통해 직접 <code class="language-plaintext highlighter-rouge">OnReturnToPool()</code>
메서드가 호출되므로, 런타임 중에 추가적인 <code class="language-plaintext highlighter-rouge">GetComponent</code> 가 요구되지 않음.</p>

<p><img width="1600" height="1200" alt="unnamed (2)" src="https://github.com/user-attachments/assets/01236128-ef27-499a-a2d7-a310ddaba46c" /></p>

<hr />

<h1 id="editor-supports">Editor Supports</h1>

<blockquote>
  <p>에디터 레벨에서 <strong>Tracker Window</strong>를 제공함.
이를 통해 객체 상태를 추적할 수 있음.</p>
</blockquote>

<p><img width="447" height="276" alt="image" src="https://github.com/user-attachments/assets/579b2a3c-4eba-4977-8eb7-5bcac7c38541" /></p>

<p><img width="750" height="397" alt="image" src="https://github.com/user-attachments/assets/27999d2b-dd44-4c21-bc89-5e2b43fc4df6" /></p>

<hr />

<h1 id="editor-case">Editor Case</h1>

<blockquote>
  <p><strong>Editor</strong>에서 <strong>PlayMode 테스트</strong>를 수행할 경우 다음과 같은 오류 또는 경고가 발생할 수 있음.</p>
</blockquote>

<dl>
  <dt><code class="language-plaintext highlighter-rouge">[Powerful] An object that failed to pass through the pool was forcibly destroyed.</code></dt>
  <dd><strong>LogWarning</strong></dd>
  <dt><code class="language-plaintext highlighter-rouge">[Powerful] Handle is invalid or has already been returned. (Tracer not found (default handle or external manipulation)</code></dt>
  <dd><strong>LogError</strong></dd>
</dl>

<p>이는 <strong>Editor Stop 호출 시 발생하는 노이즈 로그이며 무시해도 안전함</strong>.</p>

<p>다만 Dev Build 또는 정상 종료 상황에서 해당 메시지가 발생한다면<br />
설계된 Pool Manager 또는 객체 반환 사이클 설계를 잘못했음을 의미함</p>

<hr />

<h1 id="마치며">마치며</h1>

<p>굉장히 오랫동안 사용하던 패턴을 SDK화 해서 제공하게 되었는데…. 
‘LeanPool’ 이라는 Pool SDK 신뢰도가 굉장히<br />
높다길래 제작해봄.</p>

<p>WEBGL, 기타 프로젝트에서 TC 는 정상적으로 진행했으며,<br />
내부에 예제도 충분히 구현되어있음.</p>

<p>당장 Prefab에 묵직한 걸 넣고, Profiler 켜서 커브를 확인해보자</p>

<p>결과적으로, 이 SDK는 Unity 오브젝트 풀링을<br />
<strong>안전하고</strong>, <strong>결정적이며</strong>, <strong>극도로 빠르게 만들기 위해 설계된 런타임 시스템임</strong>.</p>

<p><strong>Zero-allocation 패턴</strong>,
<strong>Generation 기반 안전성 검증</strong>,
<strong>캐시 최적화 데이터 구조</strong>를 결합하여</p>

<p>해당 SDK는 <strong>고부하 런타임 환경에서도 안정적으로 동작하는 풀링 아키텍처</strong>를 제공함.</p>

<hr />]]></content><author><name>황준혁</name></author><category term="sdks" /><category term="Unity" /><category term="OpenSource" /><category term="C#" /><summary type="html"><![CDATA[Pure Unity에서 가능한 '최고 성능 Pooling 시스템']]></summary></entry><entry><title type="html">UNFinder</title><link href="https://nightwish-0827.github.io/sdks/unfinder/" rel="alternate" type="text/html" title="UNFinder" /><published>2026-03-05T00:00:00+00:00</published><updated>2026-03-05T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/sdks/unfinder</id><content type="html" xml:base="https://nightwish-0827.github.io/sdks/unfinder/"><![CDATA[<h1 id="un-unity-native-fast-object-finder-sdk">UN (Unity-Native) Fast Object Finder SDK</h1>
<h2 id="current-version--200">Current Version : 2.0.0</h2>

<p><img src="https://img.shields.io/badge/unity-2021.3%2B-black" alt="" />
<img src="https://img.shields.io/badge/license-MIT-blue" alt="" />
<img src="https://img.shields.io/badge/IL2CPP-supported-brightgreen" alt="" />
<img src="https://img.shields.io/badge/[A.O.T]-supported-brightgreen" alt="" /></p>

<blockquote>
  <p><strong>UNFinder</strong>는 Unity를 위한 고성능 조회 및 쿼리 프레임워크.<br />
이름 기반 조회(<code class="language-plaintext highlighter-rouge">GameObject.Find</code> 스타일), 컴포넌트 접근,<br />
타입/태그 기반 필터링을 결정적 런타임 동작으로 가속함.</p>
</blockquote>

<p>UNFinder는 <strong>FNV-1a 해싱</strong>, <strong>순수 C# 버킷</strong>, <strong>풀링 기반 쿼리 파이프라인</strong>을 결합해<br />
네이티브 브리지 비용과 런타임 할당을 줄임.<br />
또한 이름/태그/컴포넌트 변경 시 캐시 무결성을 유지하도록 라이프사이클 안전 API를 제공함.</p>

<p>빌드 과정에서 SDK는 숨겨진 트래커와 씬 레지스트리를 자동 삽입함.<br />
런타임에서는 오브젝트를 이름/타입/태그 버킷으로 인덱싱하여 전체 씬 스캔 없이 조회함.</p>

<p><img width="798" height="85" alt="image" src="https://github.com/user-attachments/assets/5f04341e-a445-4614-aae7-43d25f632744" /></p>

<p><code class="language-plaintext highlighter-rouge">https://github.com/NightWish-0827/UNFinder.git?path=/com.nightwishlab.unfinder</code><br />
UPM Add package from git URL</p>

<hr />

<h1 id="table-of-contents">Table of Contents</h1>

<ul>
  <li><a href="#core-features">Core Features</a></li>
  <li><a href="#api-reference--usage">API Reference \&amp; Usage</a></li>
  <li><a href="#lifecycle--callback-mechanism">Lifecycle \&amp; Callback Mechanism</a></li>
  <li><a href="#operational-notes">Operational Notes</a></li>
  <li><a href="#performance-comparison">Performance Comparison</a></li>
</ul>

<hr />

<h1 id="core-features">Core Features</h1>

<h3 id="o1-이름-조회-경로">O(1) 이름 조회 경로</h3>

<p><code class="language-plaintext highlighter-rouge">UN.Find(name)</code>은 해시된 이름 버킷을 먼저 조회하고,<br />
필요할 때만 네이티브 조회를 1회 지연 수행하여 캐시에 기록함.<br />
캐시 히트 경로는 순수 C# 메모리에서 동작함.</p>

<hr />

<h3 id="쿼리-기반-타입태그-필터링">쿼리 기반 타입/태그 필터링</h3>

<p><code class="language-plaintext highlighter-rouge">UN.Query()</code>는 <strong>With</strong>, <strong>Without</strong>, <strong>Tag</strong>, <strong>Scene</strong> 필터를 지원함.<br />
엔진은 후보 버킷 중 가장 작은 집합을 기준으로 순회해 탐색 비용을 줄임.</p>

<hr />

<h3 id="저할당-런타임">저할당 런타임</h3>

<p>UNFinder는 쿼리 빌더와 쿼리 결과를 풀링함.<br />
프레임 반복 호출이 많은 핫패스에서 불필요한 임시 할당을 줄임.</p>

<hr />

<h3 id="라이프사이클-안전-캐시-무결성">라이프사이클 안전 캐시 무결성</h3>

<p>전용 API(<code class="language-plaintext highlighter-rouge">Rename</code>, <code class="language-plaintext highlighter-rouge">SetTag</code>, <code class="language-plaintext highlighter-rouge">AddComponent</code>, <code class="language-plaintext highlighter-rouge">NotifyComponentChanged</code>, <code class="language-plaintext highlighter-rouge">Destroy</code>)를 통해<br />
실제 Unity 오브젝트 상태와 인덱스를 동기화함.</p>

<hr />

<h3 id="빌드-타임-오토-베이킹">빌드 타임 오토 베이킹</h3>

<p>빌드 처리 시 <code class="language-plaintext highlighter-rouge">[UNBake]</code>가 지정된 컴포넌트를 하나 이상 가진 오브젝트에<br />
숨김 트래커를 자동 부착함.</p>

<hr />

<h1 id="api-reference--usage">API Reference &amp; Usage</h1>

<p>API는 Unity에서 자주 사용하는 조회 패턴을 그대로 확장할 수 있도록 설계되었음.</p>

<hr />

<h2 id="object--component-lookup">Object &amp; Component Lookup</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GameObject.Find("Player") 대체</span>
<span class="n">GameObject</span> <span class="n">player</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">Find</span><span class="p">(</span><span class="s">"Player"</span><span class="p">);</span>

<span class="c1">// 컴포넌트 조회 + 캐싱</span>
<span class="n">PlayerController</span> <span class="n">controller</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="n">FindComponent</span><span class="p">&lt;</span><span class="n">PlayerController</span><span class="p">&gt;(</span><span class="s">"Player"</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="scene-aware-lookup">Scene-Aware Lookup</h2>

<p>Additive 씬 환경처럼 이름/타입 충돌이 가능한 구조에서 유용함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine.SceneManagement</span><span class="p">;</span>

<span class="n">Scene</span> <span class="n">combatScene</span> <span class="p">=</span> <span class="n">SceneManager</span><span class="p">.</span><span class="nf">GetSceneByName</span><span class="p">(</span><span class="s">"Combat"</span><span class="p">);</span>
<span class="n">GameObject</span> <span class="n">boss</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">FindInScene</span><span class="p">(</span><span class="n">combatScene</span><span class="p">,</span> <span class="s">"Boss"</span><span class="p">);</span>
<span class="n">BossController</span> <span class="n">bossCtrl</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="n">FindComponentInScene</span><span class="p">&lt;</span><span class="n">BossController</span><span class="p">&gt;(</span><span class="n">combatScene</span><span class="p">,</span> <span class="s">"Boss"</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="dynamic-instantiation--destroy">Dynamic Instantiation &amp; Destroy</h2>

<p>생성된 인스턴스는 즉시 등록되며 기본 <code class="language-plaintext highlighter-rouge">"(Clone)"</code> 접미사는 제거됨.<br />
컴포넌트 오버로드도 지원함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">GameObject</span> <span class="n">enemyInstance</span> <span class="p">=</span>
    <span class="n">UN</span><span class="p">.</span><span class="nf">Instantiate</span><span class="p">(</span><span class="n">enemyPrefab</span><span class="p">,</span> <span class="n">spawnPos</span><span class="p">,</span> <span class="n">Quaternion</span><span class="p">.</span><span class="n">identity</span><span class="p">);</span>

<span class="n">EnemyAI</span> <span class="n">ai</span> <span class="p">=</span>
    <span class="n">UN</span><span class="p">.</span><span class="n">Instantiate</span><span class="p">&lt;</span><span class="n">EnemyAI</span><span class="p">&gt;(</span><span class="n">enemyAIPrefab</span><span class="p">,</span> <span class="n">spawnPos</span><span class="p">,</span> <span class="n">Quaternion</span><span class="p">.</span><span class="n">identity</span><span class="p">);</span>

<span class="c1">// Destroy 래퍼로 라이프사이클 동작을 명시적으로 유지</span>
<span class="n">UN</span><span class="p">.</span><span class="nf">Destroy</span><span class="p">(</span><span class="n">enemyInstance</span><span class="p">);</span>
<span class="n">UN</span><span class="p">.</span><span class="nf">Destroy</span><span class="p">(</span><span class="n">ai</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="rename-binding-and-state-sync">Rename, Binding, and State Sync</h2>

<p>인덱스에 영향을 주는 가변 상태는 UN API를 통해 변경하는 것을 권장함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 수동 등록</span>
<span class="n">UN</span><span class="p">.</span><span class="nf">Bind</span><span class="p">(</span><span class="n">dynamicObject</span><span class="p">,</span> <span class="s">"DynamicBoss"</span><span class="p">);</span>

<span class="c1">// 이름 캐시 무결성을 유지하며 이름 변경</span>
<span class="n">UN</span><span class="p">.</span><span class="nf">Rename</span><span class="p">(</span><span class="n">dynamicObject</span><span class="p">,</span> <span class="s">"DynamicBoss_Phase2"</span><span class="p">);</span>

<span class="c1">// 태그 인덱스를 동기화하며 태그 변경</span>
<span class="n">UN</span><span class="p">.</span><span class="nf">SetTag</span><span class="p">(</span><span class="n">dynamicObject</span><span class="p">,</span> <span class="s">"Respawn"</span><span class="p">);</span>

<span class="c1">// 타입 인덱스 재빌드를 포함한 안전한 컴포넌트 추가</span>
<span class="n">UN</span><span class="p">.</span><span class="n">AddComponent</span><span class="p">&lt;</span><span class="n">FrozenState</span><span class="p">&gt;(</span><span class="n">dynamicObject</span><span class="p">);</span>

<span class="c1">// Unity 기본 API로 컴포넌트를 바꿨다면 UNFinder에 변경 통지</span>
<span class="n">UN</span><span class="p">.</span><span class="nf">NotifyComponentChanged</span><span class="p">(</span><span class="n">dynamicObject</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="fluent-query-api">Fluent Query API</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">var</span> <span class="n">result</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">Query</span><span class="p">()</span>
    <span class="p">.</span><span class="n">WithComponent</span><span class="p">&lt;</span><span class="n">IDamageable</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="n">WithoutComponent</span><span class="p">&lt;</span><span class="n">IFrozen</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">WithTag</span><span class="p">(</span><span class="s">"Enemy"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Execute</span><span class="p">();</span>

<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">go</span> <span class="k">in</span> <span class="n">result</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// 결과 오브젝트 사용</span>
<span class="p">}</span>
</code></pre></div></div>

<p>추가 종료 연산:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 씬 필터</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">sceneOnly</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">Query</span><span class="p">()</span>
    <span class="p">.</span><span class="n">WithComponent</span><span class="p">&lt;</span><span class="n">IEnemy</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">WithScene</span><span class="p">(</span><span class="n">combatScene</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Execute</span><span class="p">();</span>

<span class="c1">// 콜백 순회</span>
<span class="n">UN</span><span class="p">.</span><span class="nf">Query</span><span class="p">()</span>
  <span class="p">.</span><span class="n">WithComponent</span><span class="p">&lt;</span><span class="n">ITickable</span><span class="p">&gt;()</span>
  <span class="p">.</span><span class="nf">ForEach</span><span class="p">(</span><span class="n">go</span> <span class="p">=&gt;</span> <span class="n">go</span><span class="p">.</span><span class="n">GetComponent</span><span class="p">&lt;</span><span class="n">ITickable</span><span class="p">&gt;().</span><span class="nf">Tick</span><span class="p">(</span><span class="n">Time</span><span class="p">.</span><span class="n">deltaTime</span><span class="p">));</span>

<span class="c1">// 조기 종료</span>
<span class="kt">bool</span> <span class="n">completed</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">Query</span><span class="p">()</span>
    <span class="p">.</span><span class="n">WithComponent</span><span class="p">&lt;</span><span class="n">IEnemy</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">TryForEach</span><span class="p">(</span><span class="n">go</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">go</span><span class="p">.</span><span class="n">activeInHierarchy</span><span class="p">)</span> <span class="k">return</span> <span class="k">true</span><span class="p">;</span> <span class="c1">// continue</span>
        <span class="k">return</span> <span class="k">false</span><span class="p">;</span>                            <span class="c1">// break</span>
    <span class="p">});</span>

<span class="c1">// 첫 번째 매치만 빠르게 조회</span>
<span class="n">GameObject</span> <span class="n">firstEnemy</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">Query</span><span class="p">().</span><span class="n">WithComponent</span><span class="p">&lt;</span><span class="n">IEnemy</span><span class="p">&gt;().</span><span class="nf">First</span><span class="p">();</span>
</code></pre></div></div>

<hr />

<h2 id="cached-query-mode">Cached Query Mode</h2>

<p>캐시 모드는 동일 프레임에서 동일 지문(fingerprint)의 쿼리 결과를 재사용함.<br />
프레임 경계 또는 오브젝트 그래프 변경 시 자동 무효화됨.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">var</span> <span class="n">result</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">Query</span><span class="p">()</span>
    <span class="p">.</span><span class="n">WithComponent</span><span class="p">&lt;</span><span class="n">IDamageable</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">WithScene</span><span class="p">(</span><span class="n">combatScene</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Cached</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">Execute</span><span class="p">();</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Cached()</code>는 의도적으로 <code class="language-plaintext highlighter-rouge">Execute()</code>만 노출하는 <code class="language-plaintext highlighter-rouge">UNCachedQuery</code>를 반환함.</p>

<hr />

<h1 id="lifecycle--callback-mechanism">Lifecycle &amp; Callback Mechanism</h1>

<p>UNFinder는 Unity의 빌드/런타임 콜백과 트래커 라이프사이클 훅을 통해 자동 동기화됨.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Build Phase
-----------
IProcessSceneWithReport.OnProcessScene
→ 씬 루트 스캔
→ [UNBake] 대상에 숨김 UNTracker 부착
→ 베이크된 트래커 참조를 가진 ~UN_AutoRegistry 생성

Bootstrapping Phase
-------------------
DefaultExecutionOrder(-32000)
→ UNSceneRegistry.Awake()
→ 베이크된 트래커 초기화
→ 이름/타입/태그 인덱스 등록

Runtime Mutation
----------------
UN.Bind / UN.Rename / UN.SetTag / UN.AddComponent / UN.Destroy
→ 캐시와 인덱스 동기화 유지

Destroy Phase
-------------
UNTracker.OnDestroy()
→ 이름/타입/태그 버킷에서 제거
→ Stale 참조 방지
</code></pre></div></div>

<hr />

<h1 id="operational-notes">Operational Notes</h1>

<h3 id="메인-스레드-전용">메인 스레드 전용</h3>

<p>UNFinder API는 Unity 메인 스레드 사용을 전제로 설계되었음.<br />
Worker Thread 또는 Job에서 호출하지 말 것.</p>

<hr />

<h3 id="가변-인덱스-상태는-un-api-우선">가변 인덱스 상태는 UN API 우선</h3>

<p>트래킹된 오브젝트에서 <code class="language-plaintext highlighter-rouge">go.name</code>, <code class="language-plaintext highlighter-rouge">go.tag</code>를 직접 수정하지 않는 것을 권장함.<br />
<code class="language-plaintext highlighter-rouge">UN.Rename</code>, <code class="language-plaintext highlighter-rouge">UN.SetTag</code>를 사용하면 쿼리/인덱스 정합성이 유지됨.</p>

<hr />

<h3 id="query-결과는-using-사용-권장">Query 결과는 <code class="language-plaintext highlighter-rouge">using</code> 사용 권장</h3>

<p><code class="language-plaintext highlighter-rouge">UNQueryResult</code>와 <code class="language-plaintext highlighter-rouge">UNCachedQuery</code>는 풀링/해제 가능한 타입.<br />
즉시 반환되도록 <code class="language-plaintext highlighter-rouge">using</code>을 사용하는 것을 권장함.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">var</span> <span class="n">result</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">Query</span><span class="p">().</span><span class="n">WithComponent</span><span class="p">&lt;</span><span class="n">IEnemy</span><span class="p">&gt;().</span><span class="nf">Execute</span><span class="p">();</span>
</code></pre></div></div>

<hr />

<h3 id="메모리-트리밍-선택">메모리 트리밍 (선택)</h3>

<p>대량 파괴 웨이브나 씬 전환 후 버킷 메모리를 회수할 수 있음.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">trimmedBuckets</span> <span class="p">=</span> <span class="n">UN</span><span class="p">.</span><span class="nf">TrimTypeBuckets</span><span class="p">();</span>
</code></pre></div></div>

<hr />

<h1 id="performance-comparison">Performance Comparison</h1>

<p>씬 규모가 커질수록 네이티브 조회 API는 계층 순회 비용이 커지는 경향이 있음.<br />
UNFinder는 인덱스 기반 조회와 버킷 기반 후보 축소로 전체 씬 스캔을 피함.</p>

<p><img width="1000" height="600" alt="Code_Generated_Image (2)" src="https://github.com/user-attachments/assets/cfe59351-e4f6-46c2-88c7-26978186bf6e" /></p>

<h3 id="default-unity-api">Default Unity API</h3>

<ul>
  <li>네이티브 C++ 브리지 및 계층 순회 비용</li>
  <li>반복 호출이 누적될수록 총 순회 오버헤드 증가</li>
  <li>필터 중심 로직에서 씬 전체 재탐색이 반복됨</li>
</ul>

<h3 id="unfinder-sdk">UNFinder SDK</h3>

<ul>
  <li>해시 기반 C# 이름 버킷 조회</li>
  <li>인덱스 기반 타입/태그 쿼리</li>
  <li>동일 프레임 재요청에 대한 쿼리 캐시 모드</li>
  <li>풀 기반 쿼리/결과 객체로 예측 가능한 런타임 동작</li>
</ul>

<hr />

<h1 id="마치며">마치며</h1>

<p>UNFinder는 조회 중심 게임플레이 코드를 인덱스 기반 런타임 워크플로우로 전환함.<br />
기존 사용 패턴을 유지하면서도 더 결정적이고 확장 가능한 접근 경로를 제공함.</p>

<hr />]]></content><author><name>황준혁</name></author><category term="sdks" /><category term="Unity" /><category term="OpenSource" /><category term="C#" /><summary type="html"><![CDATA[Unity Native Symbol 'Find'를 부담 없이 쓰기 위한 고성능 SDK]]></summary></entry><entry><title type="html">Forge Master Cloning</title><link href="https://nightwish-0827.github.io/games/forgemaster/" rel="alternate" type="text/html" title="Forge Master Cloning" /><published>2026-02-03T00:00:00+00:00</published><updated>2026-02-03T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/games/forgemaster</id><content type="html" xml:base="https://nightwish-0827.github.io/games/forgemaster/"><![CDATA[<h2 id="제작한-sdk를-검증하기-위한-비-상업용-프로젝트-입니다">제작한 SDK를 검증하기 위한 비 상업용 프로젝트 입니다.</h2>

<p>화제의 D2G 장르. 독일 개발사 <strong>Lessmore</strong>의 <strong>Forge Master</strong> 게임을 클로닝 했습니다.</p>

<p>리소스는 <strong><a href="https://play.google.com/store/apps/details?id=com.hariwn.legendofcivilizations&amp;hl=en">Forge Master</a></strong> 원작을 Dotpeek 활용하여 분해 후 사용했습니다. <br /></p>

<p><span style="color:red"><code class="language-plaintext highlighter-rouge">이를 상업적으로 활용한 사례가 일체 없음을 밝힙니다.</code></span></p>

<div style="width: 100%; max-width: 700px;">
<div class="notion-link-card-wrapper">
  <a href="https://play.google.com/store/apps/details?id=com.hariwn.legendofcivilizations&amp;hl=en" class="notion-link-card" target="_blank" rel="noopener noreferrer">
    <div class="notion-link-card-content">
      <div class="notion-link-card-title">Forge Master</div>
      <div class="notion-link-card-desc">Are you ready for this adventure?.</div>
      <div class="notion-link-card-url">
        <img src="https://www.google.com/s2/favicons?domain=https://play.google.com/store/apps/details?id=com.hariwn.legendofcivilizations&amp;hl=en&amp;sz=32" alt="favicon" class="notion-link-card-favicon" />
        <span>https://play.google.com/store/apps/details?id=com.hariwn.legendofcivilizations&amp;hl=en</span>
      </div>
    </div>
    
    <div class="notion-link-card-image" style="background-image: url('https://play-lh.googleusercontent.com/emNDoWnEo13CRoLpnoUfhygnTXQ7zQVZ9SPpcLWZrSEOZ8Jbk6dhx0bZ2zgY0BFg4g=w240-h480-rw');"></div>
    
  </a>
</div>
</div>
<hr />

<h2 id="screen-shots--videos">Screen Shots &amp; Videos</h2>

<p><img src="/assets/images/tycoon-teaser.png" width="300" /></p>

<video controls="" width="300">
  <source src="/assets/videos/fg.mp4" type="video/mp4" />
</video>

<hr />

<h2 id="my-package-list">My Package List</h2>

<table>
  <thead>
    <tr>
      <th>SDK</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><a href="https://github.com/NightWish-0827/UNFinder">UNFinder</a></strong></td>
      <td>Find 심벌을 Editor 에서 최적화 하여 사용</td>
    </tr>
    <tr>
      <td><strong><a href="https://github.com/NightWish-0827/PowerPool">PowerPool</a></strong></td>
      <td>초고성능 Object Pooling 시스템</td>
    </tr>
    <tr>
      <td><strong>[UNInject]</strong></td>
      <td>Unity 직렬화 Bake Up을 활용한 Lapid Dependecy Injection SDK (UPM 지원 예정)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="open-source-sdk-list">Open Source SDK List</h2>

<table>
  <thead>
    <tr>
      <th>SDK</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><a href="https://github.com/Cysharp/UniTask">Unitask</a></strong></td>
      <td>스레드 기반 비동기 메서드 지원</td>
    </tr>
    <tr>
      <td><strong><a href="https://github.com/Demigiant/dotween">DOTween</a></strong></td>
      <td>Unity 스크립트 애니메이션 시퀀스</td>
    </tr>
    <tr>
      <td><strong><a href="https://github.com/applejag/Newtonsoft.Json-for-Unity">Json For Unity</a></strong></td>
      <td>Json 읽기 쓰기 호환</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="test-device">Test Device</h2>

<table>
  <thead>
    <tr>
      <th>테스트 기기</th>
      <th>컴파일</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>LG VELVET (LM-G900N)</strong></td>
      <td>IL2CPP</td>
    </tr>
    <tr>
      <td><strong>Iphone 15 (MTP43KH/A)</strong></td>
      <td>Dev Build</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="간단한-후기">간단한 후기</h2>

<p>잉여 시간에 SDK TC 진행겸 제작하게 된 게임입니다.<br /></p>

<p>원작의 리소스가 단순함을 넘어, <strong>특이점이 온 수준의 표현 억제</strong>가 특징입니다. <br /> 
하지만 의외로 게임 자체가 중독성이 매우 강하여, 개발 하면서도 제법 즐겁게 진행했습니다.</p>

<hr />]]></content><author><name>황준혁</name></author><category term="games" /><category term="SDK TC" /><summary type="html"><![CDATA[제작한 SDK의 TC, 개발 보조 능력을 측정하기 위해 제작되었습니다.]]></summary></entry><entry><title type="html">타워 슬래셔</title><link href="https://nightwish-0827.github.io/games/towerslasher/" rel="alternate" type="text/html" title="타워 슬래셔" /><published>2025-11-01T00:00:00+00:00</published><updated>2025-11-01T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/games/towerslasher</id><content type="html" xml:base="https://nightwish-0827.github.io/games/towerslasher/"><![CDATA[<h2 id="회사-보안으로-정보-공개가-불가능한-프로젝트-입니다">회사 보안으로, 정보 공개가 불가능한 프로젝트 입니다.</h2>

<p><strong>주식회사 111%</strong> 재직 중 개발을 진행했습니다. <br /></p>

<p>단순 영상만을 포트폴리오 및 이력서에 게시하며, 코드 레벨 및 런타임의 구조 공개가 이뤄지지 않습니다. <br /></p>

<p><span style="color:red"><code class="language-plaintext highlighter-rouge">이를 상업적으로 활용한 사례가 일체 없음을 밝힙니다.</code></span></p>

<hr />

<h2 id="screen-shots--videos">Screen Shots &amp; Videos</h2>

<figure class="half">
<img src="/assets/images/cap 1.png" width="500" />
<img src="/assets/images/cap 2.png" width="500" />
</figure>

<figure class="half">
<img src="/assets/images/cap 3.png" width="500" />
<img src="/assets/images/cap 4.png" width="500" />
</figure>

<figure class="half">
<img src="/assets/images/cap 5.png" width="500" />
<img src="/assets/images/cap 6.png" width="500" />
</figure>

<video controls="" width="300">
  <source src="/assets/videos/ts.mp4" type="video/mp4" />
</video>

<hr />

<h2 id="업무">업무</h2>

<p><strong>해당 프로젝트에서 단독 클라이언트 프로그래머를 담당했습니다.</strong></p>

<ul>
  <li>아트 리소스를 제외한 모든 업무를 수행했습니다.</li>
</ul>

<hr />

<h2 id="간단한-후기">간단한 후기</h2>

<p>정말 간만에 쉐이더를 극한으로 다뤄가며 제작한 프로젝트입니다. <br /></p>

<p>개인적으로 이러한 아트 스타일을 굉장히 선호하기 때문에, <span style="color:yellow"><strong>최대한 아름답게</strong></span>  제작하고 싶었습니다. <br /></p>

<p>본디 모바일에서는 투명 쉐이더나 굴절은 거의 회피하며 제작을 진행하지만, <br /> 
최적화를 극한의 극한까지 하여.. 10장 가까이 겹쳐 사용했습니다. <br /></p>

<p>성능은 로우 ~ 미드 티어 기기에서 <strong>안정적으로 60프레임</strong>이 보장되었습니다. <br /></p>

<p>짧은 기간이었지만, 수려한 아트 리소스 작업 해주신 팀원분께 무한한 감사를 드립니다. 🥹</p>

<hr />]]></content><author><name>황준혁</name></author><category term="games" /><category term="사진, 영상 외 정보 공개 불가" /><summary type="html"><![CDATA[111% 재직 당시, 단기 제작을 마친 게임입니다.]]></summary></entry><entry><title type="html">아르르! 야르르!</title><link href="https://nightwish-0827.github.io/games/arryarr/" rel="alternate" type="text/html" title="아르르! 야르르!" /><published>2024-09-05T00:00:00+00:00</published><updated>2024-09-05T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/games/arryarr</id><content type="html" xml:base="https://nightwish-0827.github.io/games/arryarr/"><![CDATA[<h2 id="스토브-출시-및-네오위즈-사옥-1층-전시-작품-입니다">스토브 출시 및 네오위즈 사옥 1층 전시 작품 입니다.</h2>

<p><strong>청강 크로니클 전시회</strong>에 출품하기 위해 개발이 진행되었습니다. <br /></p>

<p>3개의 주무기, 각종 스킬과 증강 및 아이템을 활용하여 전투하는 2D 멀티 액션 대전 게임입니다. <br />
각 무기 및 스테이지 마다 고유한 기믹과 스킬이 존재하며, 20개가 넘는 증강 시스템을 통해 색다른 게임성을 자랑합니다.</p>

<p><strong>꼬마 해적</strong> 이라는 핵심 컨셉을 통해 귀여운 캐릭터들과, 개성있는 여러 스테이지를 통해 캐주얼한 세계관으로 표현되었습니다.</p>

<p>독특한 특징으로는 <strong><span style="color:yellow">‘라운드 패자에게 증강의 기회가 주어진다.’</span></strong> 라는 게임의 룰을 통해 플레이 방식이 정형화 되지 않고,<br />
매 라운드마다 다른 플레이 경험을 즐길 수 있습니다.</p>

<p>레거시 Fusion 2.0 의 라이브 서버가 다소 불안정한 탓에 원활하진 않으나, 해당 링크에서 다운로드 및 플레이가 가능합니다. <br /></p>

<div style="width: 100%; max-width: 700px;">
<div class="notion-link-card-wrapper">
  <a href="https://store.onstove.com/games/4518" class="notion-link-card" target="_blank" rel="noopener noreferrer">
    <div class="notion-link-card-content">
      <div class="notion-link-card-title">아르르! 야르르! | STOVE 스토어</div>
      <div class="notion-link-card-desc">바다에서, 동굴에서 펼쳐지는 쌍둥이 해적의  2D 하이퍼 캐주얼 멀티 슈팅 대전 게임 입니다.</div>
      <div class="notion-link-card-url">
        <img src="https://www.google.com/s2/favicons?domain=https://store.onstove.com/games/4518&amp;sz=32" alt="favicon" class="notion-link-card-favicon" />
        <span>https://store.onstove.com/games/4518</span>
      </div>
    </div>
    
    <div class="notion-link-card-image" style="background-image: url('https://d2x8kymwjom7h7.cloudfront.net/live/application_no/119001/images/4518_ARRRYARRR_IND_DEMO_01_IND_0_1732074036444.png');"></div>
    
  </a>
</div>
</div>

<hr />

<h2 id="screen-shots--videos">Screen Shots &amp; Videos</h2>

<p><img src="/assets/images/arryarr-teaser.png" width="700" /></p>

<div style="width: 100%; max-width: 700px;">
  <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
    <iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/nm_4CSCKa8A?si=j_pEDkyX4xjHidP5" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="">
    </iframe>
  </div>
</div>

<hr />

<h2 id="업무">업무</h2>

<p><strong>해당 프로젝트에서 PD를 담당했습니다.</strong></p>

<ul>
  <li>프로젝트 코어 설계</li>
  <li>스테이지 싱커 구현</li>
  <li>각종 스테이지 기믹 구현</li>
  <li>각종 쉐이더 및 스테이지 이펙트, 파티클 구현</li>
  <li>PC 전반 구현</li>
  <li>서버 동기화 및 네트워크 인자 안정화 &amp; 최적화</li>
  <li>아이템 및 각종 강화 요소 구현</li>
  <li>
    <p>사운드 디자인 및 제작</p>
  </li>
  <li>작업 지시, 작업 보고 등의 PD 대외 업무</li>
</ul>

<hr />

<h2 id="간단한-후기">간단한 후기</h2>

<p>14명 모두가 3개월이라는 짧은 개발 기간 동안,
최대한의 퍼포먼스를 보여줬다고 생각되는 <strong><span style="color:yellow">이상적인 게임 개발</span></strong>이었습니다. <br /></p>

<p>즐거웠습니다. 감사합니다.<br />
<br /> Farewell <strong>언덕 위의 개들</strong></p>

<hr />]]></content><author><name>황준혁</name></author><category term="games" /><category term="Multi Game" /><category term="Photon Fusion 2.0" /><summary type="html"><![CDATA[Stove / 네오위즈 사옥 전시 작품 입니다.]]></summary></entry><entry><title type="html">창해유주</title><link href="https://nightwish-0827.github.io/games/projectgs/" rel="alternate" type="text/html" title="창해유주" /><published>2024-03-01T00:00:00+00:00</published><updated>2024-03-01T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/games/projectgs</id><content type="html" xml:base="https://nightwish-0827.github.io/games/projectgs/"><![CDATA[<h2 id="청강문화산업대학교-졸업작품-입니다">청강문화산업대학교 졸업작품 입니다.</h2>

<p>바다 속을 배경으로 하는 <strong>동양풍 3D 액션 어드벤쳐</strong> 게임입니다. <br /></p>

<p>기본 3 마리의 소환수 친구들과 본인 고유의 능력을 활용해 적들을 해치우고 나아가며, <br />
추가 소환수 친구들을 만나거나 필요한 속성의 소환수를 엔트리에서 조합하여, 최종 보스까지 클리어하는 스토리 게임입니다.<br /></p>

<p><strong>용궁 선녀</strong> 라는 동양풍 컨셉과, <strong>심해 바다</strong>를 표현하기 위한 섬세한 TA 요소들이 잘 어우러져 감도 높은 환경이 구축되었습니다.</p>

<hr />

<h2 id="screen-shots--videos">Screen Shots &amp; Videos</h2>

<p><img src="/assets/images/gs_teaser.png" width="700" /></p>

<video controls="" style="width: 100%; max-width: 700px; height: auto;">
  <source src="/assets/videos/gs.mp4" type="video/mp4" />
</video>

<hr />

<h2 id="업무">업무</h2>

<p><strong>클라이언트 프로그래머, 사운드 디자이너, (인계) PD를 담당했습니다.</strong></p>

<ul>
  <li>프로젝트 메인 코어 설계 및 구현</li>
  <li>PC 3D 물리 모듈 설계 및 구현</li>
  <li>인 게임 FSM</li>
  <li>보스 패턴 에디터 구현</li>
  <li>사운드 디자인 및 제작</li>
  <li>FMOD 에디터 개발</li>
  <li>
    <p>중간 보스 전반 개발</p>
  </li>
  <li>PD 인수인계 이후 기타 대외 업무</li>
</ul>

<hr />

<h2 id="간단한-후기">간단한 후기</h2>

<p>각 게임 요소 최적화와 사운드 디자인에 매우 많은 시간을 들인 게임입니다. <br /></p>

<p>특히, 시놉시스를 살리기 위한 사운드를 제작하기 매우 어려웠던 기억이 납니다. <br />
<del>미들웨어를 폭력적으로 다뤄야 할 만큼 난이도 있는 오퍼가 매 주차마다 추가되었던…</del><br /></p>

<p><img src="/assets/images/gscap.png" width="700" /></p>

<p>이제는 그 일련의 과정마저 추억으로 느껴질 만큼 숱한 경험을 했지만.. 이 게임이 가장 강력한 적이었던 것 같습니다… <br /></p>

<p>즐거웠습니다. 감사합니다.<br />
<br /> Farewell <strong>바다의 보배</strong></p>

<hr />]]></content><author><name>황준혁</name></author><category term="games" /><category term="Highend Audio Design" /><category term="FMOD" /><summary type="html"><![CDATA[청강문화산업대학교 졸업 작품 입니다.]]></summary></entry><entry><title type="html">Loving My Devil</title><link href="https://nightwish-0827.github.io/games/lovingmydevil/" rel="alternate" type="text/html" title="Loving My Devil" /><published>2023-08-01T00:00:00+00:00</published><updated>2023-08-01T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/games/lovingmydevil</id><content type="html" xml:base="https://nightwish-0827.github.io/games/lovingmydevil/"><![CDATA[<h2 id="xr-캡스톤-프로젝트-입니다">XR 캡스톤 프로젝트 입니다.</h2>

<p>국가 지원 사업인 <strong>XR 캡스톤 프로젝트</strong>에 출품하기 위해 개발이 진행되었습니다. <br /></p>

<p>그래피티를 통해 스테이지를 클리어 하는 굉장히 독특한 <strong>2D 퍼즐 플랫포머</strong> 게임입니다. <br /></p>

<p><strong><span style="color:red">지옥</span></strong>이라는 환경에서 엄숙하게 행동하지 못하고 그래피티를 마구 그리는 주인공. 공주 <strong>‘루시’</strong>와, <br />
이를 막으려는 하인, 하녀 그리고 <strong>아버지와의 한 판 승부</strong>라는 귀엽고 흥미로운 시놉시스를 표현하였습니다. <br /></p>

<p><strong>왕과 공주의 승부</strong>라는 컨셉 덕분에 해당 게임에는 독특하게도, <strong>공격</strong> 이라는 개념이 존재하지 않습니다.<br />
넉백과 색칠만으로 서로를 골려먹는 유쾌한 퍼즐 게임입니다.</p>

<hr />

<h2 id="screen-shots--videos">Screen Shots &amp; Videos</h2>

<p><img src="/assets/images/lvd_teaser.png" width="700" /></p>

<video controls="" style="width: 100%; max-width: 700px; height: auto;">
  <source src="/assets/videos/lvd.mp4" type="video/mp4" />
</video>

<hr />

<h2 id="업무">업무</h2>

<p><strong>클라이언트 프로그래머, 사운드 디자이너를 담당했습니다.</strong></p>

<ul>
  <li>프로젝트 코어 설계</li>
  <li>각종 인 게임 요소 제작</li>
  <li>인 게임 몬스터 구현</li>
  <li>사운드 디자인 및 제작</li>
  <li>사운드 에디터 개발</li>
</ul>

<hr />

<h2 id="간단한-후기">간단한 후기</h2>

<p>색칠을 통해 플랫폼을 만들고, <br />
그래피티를 통해 스테이지의 진행도를 높여 나가는 퍼즐 플랫포머라는 아이디어 자체는 매우 흥미로운 시도였습니다. <br /></p>

<p>다만 스토리 기반 게임을 단기간에 완성해야 했던 프로젝트 특성상,<br />
해당 시스템을 충분히 확장하거나 더 다양한 방식으로 활용해 보지 못한 점은 다소 아쉬움으로 남습니다. <br /></p>

<p>그럼에도 불구하고 프로젝트는 큰 문제 없이 마무리되었으며, 각자의 위치에서 최선을 다한 결과라고 생각합니다.</p>

<p>즐거웠습니다. 감사합니다.<br />
<br /> Farewell <strong>어둠의 자식들</strong></p>

<hr />]]></content><author><name>황준혁</name></author><category term="games" /><category term="Highend Audio Design" /><category term="FMOD" /><summary type="html"><![CDATA[국가 지원 사업. XR 캡스톤 프로젝트 입니다.]]></summary></entry><entry><title type="html">Error Log</title><link href="https://nightwish-0827.github.io/games/errorlog/" rel="alternate" type="text/html" title="Error Log" /><published>2019-09-20T00:00:00+00:00</published><updated>2019-09-20T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/games/errorlog</id><content type="html" xml:base="https://nightwish-0827.github.io/games/errorlog/"><![CDATA[<h2 id="리듬-탄막-게임">리듬 탄막 게임</h2>

<p>리듬에 맞춰서 탄막이 발생하고, 이를 회피하는 <strong>2D 리듬 탄막</strong> 게임입니다. <br /></p>

<p><strong>Window XP</strong> 라는 빈티지한 컨셉에 맞춰 게임의 핵심 요소들이 윈도우 XP의 테마를 따라 제작되었습니다. <br /></p>

<p>버튼이 아닌 커맨드를 통해 설정 및 각종 스테이지를 로드하거나, 각 스테이지를 클리어 할 때 마다 오류가 하나씩 픽스된다는 <br />
흥미로운 컨셉을 통해 게임을 표현하였습니다. <br /></p>

<p><strong>리듬에 맞춰 탄막의 패턴 및 탄환이 상태 변화</strong>하는 <strong><span style="color:red">하드코어</span></strong> Bullet Hell 시스템을 갖고 있습니다.</p>

<hr />

<h2 id="screen-shots--videos">Screen Shots &amp; Videos</h2>

<p><img src="/assets/images/g1.png" width="700" /></p>

<p><img src="/assets/images/g2.png" width="700" /></p>

<p><img src="/assets/images/g3.png" width="700" /></p>

<p><img src="/assets/images/g4.png" width="700" /></p>

<div style="display: flex; gap: 5px; width: 100%;">
  <img src="/assets/images/g5.png" style="flex: 1; min-width: 0; width: 100%; height: auto;" />
  <img src="/assets/images/g6.png" style="flex: 1; min-width: 0; width: 100%; height: auto;" />
</div>
<p><br /></p>

<div style="display: flex; gap: 5px; width: 100%;">
  <img src="/assets/images/g7.png" style="flex: 1; min-width: 0; width: 100%; height: auto;" />
  <img src="/assets/images/g8.png" style="flex: 1; min-width: 0; width: 100%; height: auto;" />
</div>
<p><br /></p>

<div style="display: flex; gap: 5px; width: 100%;">
  <img src="/assets/images/g9.png" style="flex: 1; min-width: 0; width: 100%; height: auto;" />
  <img src="/assets/images/g10.png" style="flex: 1; min-width: 0; width: 100%; height: auto;" />
</div>
<p><br /></p>

<p><img src="/assets/images/g11.jpg" width="700" /></p>

<hr />

<h2 id="간단한-후기">간단한 후기</h2>

<p><strong>Matf 심벌</strong>에 각종 연산을 추가시켜, 패턴의 구현을 최소한의 라인으로 제작하기 위한 SDK를 제작하며 개발을 병행한 게임입니다. <br /></p>

<p>LinQ 구문과 유사하게 패턴 선언이 가능하도록 해뒀지만, <br />
당시에는 코드레벨 이해도가 다소 부족하여 확장성을 고려하지 않은 SDK로 남게 되었습니다. <br /></p>

<p><strong>FMOD</strong>를 사용하여 오디오 이벤트를 구축했는데, <br /> 
FMOD의 네이티브 언어인 C++ 와 유니티 런타임과의 <strong>[A.O.T]</strong>를 처음 시도한 프로젝트입니다. <br />
난이도는 있었으나, 구현을 성공하니 효과가 상당했으며. 이를 통해 <strong>FMOD</strong>를 적극 사용하기 시작했습니다. <br /></p>

<p>2019 당시에는 지금처럼 개발 커뮤니티가 크지도 않고, <br /> 이런 SDK들이 제공되지 않았기에 이런 기능을 손수 제작하는 것이 당연했던 시기입니다.</p>

<p>현재는 갖가지 다양한 SDK들과 AI가 발전해서 예전 만큼의 개발 재미도가 높진 않기에, 향수를 부르는 그런 게임입니다..</p>

<hr />]]></content><author><name>황준혁</name></author><category term="games" /><category term="Highend Audio Design" /><category term="FMOD" /><summary type="html"><![CDATA[[Mathf Symbols] SDK의 TC 프로젝트 입니다.]]></summary></entry><entry><title type="html">VVVVVV - Gravity Jump</title><link href="https://nightwish-0827.github.io/games/v6/" rel="alternate" type="text/html" title="VVVVVV - Gravity Jump" /><published>2018-11-01T00:00:00+00:00</published><updated>2018-11-01T00:00:00+00:00</updated><id>https://nightwish-0827.github.io/games/v6</id><content type="html" xml:base="https://nightwish-0827.github.io/games/v6/"><![CDATA[<h2 id="원작-게임의-추가-스테이지-및-기믹을-개발한-프로젝트입니다">원작 게임의 추가 스테이지 및 기믹을 개발한 프로젝트입니다.</h2>

<p>인디 게임 <strong>VVVVVV</strong> 의 팬 게임 프로젝트로, <br /> 
원작의 부족한 UX 개선과 다양한 스테이지 패턴 변경 및 연장 등에 초점을 맞춰 제작하였습니다. <br /></p>

<p>추가적으로, 2018년도 당시에 유행했던 아트 스타일 [<strong>네온 빈티지</strong>]와, [<strong>Hide UI</strong>] 등을 사용하여 차별성을 주었습니다.</p>

<p><span style="color:red"><code class="language-plaintext highlighter-rouge">이를 상업적으로 활용한 사례가 일체 없음을 밝힙니다.</code></span></p>

<div style="width: 100%; max-width: 700px;">
<div class="notion-link-card-wrapper">
  <a href="https://play.google.com/store/apps/details?id=air.com.distractionware.vvvvvvmobile&amp;hl=en" class="notion-link-card" target="_blank" rel="noopener noreferrer">
    <div class="notion-link-card-content">
      <div class="notion-link-card-title">VVVVVV</div>
      <div class="notion-link-card-desc"></div>
      <div class="notion-link-card-url">
        <img src="https://www.google.com/s2/favicons?domain=https://play.google.com/store/apps/details?id=air.com.distractionware.vvvvvvmobile&amp;hl=en&amp;sz=32" alt="favicon" class="notion-link-card-favicon" />
        <span>https://play.google.com/store/apps/details?id=air.com.distractionware.vvvvvvmobile&amp;hl=en</span>
      </div>
    </div>
    
    <div class="notion-link-card-image" style="background-image: url('https://play-lh.googleusercontent.com/JNfM0eGzWXi4PTf65vjvAPjKeASNZx30FpJHg_Xhvdp4ZgKsC6Lv3sV-9tsmObDIgXY=w526-h296-rw');"></div>
    
  </a>
</div>
</div>

<hr />

<h2 id="screen-shots--videos">Screen Shots &amp; Videos</h2>

<p><img src="/assets/images/gj_teaser.png" width="700" /></p>

<p><img src="/assets/images/gjori.png" width="700" /></p>

<hr />

<h2 id="간단한-후기">간단한 후기</h2>

<p>첫 협업 작품입니다. <br /></p>

<p>언어가 안 통하던 영어권 분들 밖에 없던 공식 커뮤니티에서, Git 사용법과 원작 엔진에 대한 많은 가르침을 받았습니다. <br /></p>

<p>원작 엔진은 Direct X 기반의 엔진이었는데, 모바일 포팅이 다소 어려웠던 기억이 납니다.</p>

<p>GUI가 아예 없는 환경에서 개발을 했기 때문에, 코드 자체와 친해질 수 있었던 그런 프로젝트였습니다.</p>

<hr />]]></content><author><name>황준혁</name></author><category term="games" /><summary type="html"><![CDATA[VVVVVV DevSup Forum 합동 작품입니다.]]></summary></entry></feed>