---
title: "Zenject 의 조회는 가까운 컨테이너와 조건 있는 바인딩을 고른다"
description: "같은 계약이 부모와 자식에, 또는 한 컨테이너에 둘 있으면 Zenject 는 무엇을 줄까요. 단일 조회는 가까운 컨테이너, 조건 있는 쪽 순으로 하나를 고르고 같은 등급이 둘이면 예외, 목록 조회는 전부 모읍니다."
author: "유니얼"
pubDate: 2026-09-27
category: "game-dev/unity/libraries"
tags: ["Unity", "라이브러리", "DI"]
canonical: https://blog.unialgames.com/posts/unity-zenject-lookup/
---
```csharp
// ProjectContext 설치 파일
Container.Bind<IWeapon>().To<Bow>().AsSingle();
// 씬 설치 파일
Container.Bind<IWeapon>().To<Sword>().AsSingle();

public class Archer { [Inject] IWeapon _weapon; }   // 무엇이 들어올까
```

같은 계약이 두 곳에 있어도 설치는 조용히 지나갑니다. 그러면 `Archer` 는 `Sword` 를 받을까, `Bow` 를 받을까, 예외를 받을까요.

Zenject 의 단일 조회는 **가까운 컨테이너가 이기고, 같은 거리에서는 조건 있는 바인딩이 이기고, 같은 등급이 둘이면 처음 조회할 때 예외**입니다. 여러 개를 달라고 하면 이 규칙을 쓰지 않고 전부 모읍니다.

## 가까운 컨테이너가 이기고, 같은 거리에서는 조건 있는 쪽이 이긴다

조회 키는 (계약 타입, `WithId` 값) 쌍인 `BindingId` 입니다. `WithId("main")` 으로 바인딩한 것은 `[Inject(Id = "main")]` 으로만 찾습니다. 키로 찾은 후보는 이렇게 고릅니다.

- 자기 컨테이너가 거리 0, 부모가 1, 조부모가 2 입니다. 가까운 데서 고르면 먼 컨테이너는 보지 않습니다.
- 같은 거리에서는 `When` 조건을 통과한 후보가 조건 없는 후보를 이깁니다.
- 같은 거리에 같은 등급이 둘이면 모호하다며 예외를 던집니다.

<x-flow>
<script type="application/json">
{
  "label": "TryGetUniqueProvider 가 후보를 고르는 순서 (줄인 코드)",
  "lines": [
    "foreach (var c in lookups) c.FlushBindings();",
    "foreach (var c in lookups)            // 자기, 부모, 조부모 순",
    "    int d = GetContainerHeirarchyDistance(c);",
    "    if (d > selectedDistance) continue;",
    "    foreach (var p in c.GetLocalProviders(bindingId))",
    "        if (p.Condition != null && !p.Condition(context)) continue;",
    "        조건 유무를 비교해 ambiguous 를 정한다;",
    "        if (!ambiguous) { selected = p; selectedDistance = d; }",
    "if (ambiguous) throw \"Found multiple matches ...\";",
    "return selected;"
  ],
  "cases": [
    {
      "label": "자식이 다시 바인딩했다",
      "state": "씬 = { IWeapon: [Sword] } · 프로젝트(부모) = { IWeapon: [Bow] }",
      "path": [
        { "line": 0, "why": "씬 컨테이너와 프로젝트 컨테이너의 큐를 모두 비웁니다." },
        { "line": 1, "state": "c = 씬 · selected = 없음" },
        { "line": 2, "state": "c = 씬 · d = 0" },
        { "line": 3, "verdict": "false" },
        { "line": 4, "state": "p = Sword" },
        { "line": 5, "why": "조건이 없어 통과합니다." },
        { "line": 6, "why": "처음 고르는 것이라 모호하지 않습니다." },
        { "line": 7, "state": "selected = Sword · selectedDistance = 0" },
        { "line": 1, "state": "c = 프로젝트 · selected = Sword" },
        { "line": 2, "state": "c = 프로젝트 · d = 1" },
        { "line": 3, "verdict": "true", "why": "이미 거리 0 에서 골랐으니 부모의 딕셔너리는 보지 않습니다." },
        { "line": 8, "verdict": "false" },
        { "line": 9, "out": "Sword" }
      ]
    },
    {
      "label": "한 컨테이너에 조건 없이 둘",
      "state": "씬 = { IWeapon: [Sword, Bow] }",
      "path": [
        { "line": 0 },
        { "line": 1, "state": "c = 씬" },
        { "line": 2, "state": "c = 씬 · d = 0" },
        { "line": 3, "verdict": "false" },
        { "line": 4, "state": "p = Sword" },
        { "line": 5 },
        { "line": 6 },
        { "line": 7, "state": "selected = Sword · d = 0" },
        { "line": 4, "state": "p = Bow" },
        { "line": 5 },
        { "line": 6, "why": "둘 다 조건이 없고 거리도 같습니다. ambiguous = true", "state": "selected = Sword · ambiguous = true" },
        { "line": 8, "verdict": "true", "out": "Found multiple matches when only one was expected for type 'IWeapon'" }
      ],
      "skip": [9]
    },
    {
      "label": "한쪽에 When 을 걸었다",
      "state": "씬 = { IWeapon: [Sword, Bow + WhenInjectedInto<Archer>] } · 요청한 쪽 = Archer",
      "path": [
        { "line": 0 },
        { "line": 1, "state": "c = 씬" },
        { "line": 2, "state": "c = 씬 · d = 0" },
        { "line": 3, "verdict": "false" },
        { "line": 4, "state": "p = Sword" },
        { "line": 5 },
        { "line": 6 },
        { "line": 7, "state": "selected = Sword (조건 없음)" },
        { "line": 4, "state": "p = Bow" },
        { "line": 5, "why": "context.ObjectType 이 Archer 라 조건을 통과합니다.", "verdict": "통과" },
        { "line": 6, "why": "조건 있는 쪽이 조건 없는 쪽을 이깁니다. ambiguous = false" },
        { "line": 7, "state": "selected = Bow (조건 있음)" },
        { "line": 8, "verdict": "false" },
        { "line": 9, "out": "Bow" }
      ]
    }
  ],
  "note": "조건 없는 둘은 등록할 때가 아니라 이 줄에서, 처음 조회할 때 터집니다."
}
</script>
</x-flow>

맨 위 예제의 `Archer` 는 `Sword` 를 받습니다. 부모의 `Bow` 는 예외도 경고도 없이 가려집니다. 모호성 예외는 등록할 때가 아니라 처음 조회할 때 납니다. 등록은 목록에 하나를 더 붙이는 일이라 막을 이유가 없고, 몇 개가 경쟁하는지는 조회해 봐야 압니다.

찾을 범위는 `[Inject(Source = ...)]` 로 바꿉니다. 기본값 `Any` 는 자기와 모든 조상, `Local` 은 자기만, `Parent` 는 직속 부모만, `AnyParent` 는 조상만입니다.

## 여러 개를 달라고 하면 거리도 모호성도 따지지 않는다

`ResolveAll` 은 범위 안 **모든 컨테이너**의 후보를 이어 붙이고, 조건만 거릅니다. 배열, `List<T>`, `IEnumerable<T>` 같은 목록 요청은 그 타입의 바인딩이 없으면 원소 타입의 `ResolveAll` 로 넘어가는데, 이때 요청을 선택(Optional)으로 바꿉니다.

<x-compare>
<script type="application/json">
{
  "label": "씬에 Sword, 부모인 프로젝트 컨테이너에 Bow 가 IWeapon 으로 있을 때",
  "tabs": [
    {
      "label": "Resolve<IWeapon>()",
      "code": "var w = Container.Resolve<IWeapon>();",
      "text": "거리 0 인 씬 컨테이너에서 Sword 를 고르고, 부모는 들여다보지 않습니다.",
      "stats": [["결과", "Sword"], ["부모의 Bow", "가려진다"], ["둘이 같은 거리면", "Found multiple matches 예외"]]
    },
    {
      "label": "ResolveAll<IWeapon>()",
      "code": "var all = Container.ResolveAll<IWeapon>();",
      "text": "씬, 프로젝트 순으로 모든 컨테이너의 후보를 이어 붙입니다. 거리도 모호성도 따지지 않습니다.",
      "stats": [["결과", "[Sword, Bow]"], ["하나도 없으면", "선택이 아니면 Could not find required dependency 예외"]]
    },
    {
      "label": "[Inject] List<IWeapon>",
      "code": "[Inject] List<IWeapon> _weapons;",
      "text": "List<IWeapon> 바인딩이 없으니 원소 타입의 ResolveAll 로 넘어가고, 이때 요청이 선택으로 바뀝니다.",
      "stats": [["결과", "[Sword, Bow]"], ["하나도 없으면", "예외 없이 빈 목록"]],
      "tone": "bad"
    }
  ]
}
</script>
</x-compare>

그래서 같은 계약을 단일로 받는 쪽은 하나를, 목록으로 받는 쪽은 둘을 봅니다. 단일 요청이 아무것도 못 찾으면 선택 요청은 기본값을 받고, 나머지는 `Unable to resolve` 예외입니다. 목록만 예외 없이 빈 목록으로 통과합니다.

Zenject 도 이 범위를 적어 씁니다. 진입점을 부르는 `InitializableManager` 는 `[Inject(Optional = true, Source = InjectSources.Local)] List<IInitializable>` 로 받아, 부모 컨테이너의 진입점을 섞지 않습니다.

## 주의할 점

### 1. 같은 계약을 조건 없이 두 번 바인딩하지 않는다

**안 지키면 생기는 문제.** 같은 컨테이너에 `IWeapon` 을 두 번 바인딩해도 설치는 조용히 지나가고, 예외는 누가 처음 `IWeapon` 하나를 요구할 때 `Found multiple matches when only one was expected` 로 납니다. 드문 경로의 조회라면 한참 뒤에 드러나고, 두 설치 파일이 모르고 같은 계약을 넣은 경우라면 어느 쪽인지 바로 보이지 않습니다.

**이렇게 쓴다.** 여러 구현이 필요하면 쓰는 쪽을 목록으로 받거나, `WithId` 로 이름을 가르거나, `When` 계열 조건으로 누가 무엇을 받는지 정합니다. 실행 전 검증을 돌리면 이 예외도 플레이 전에 나옵니다.

### 2. 자식 컨테이너의 바인딩은 부모 것을 가린다

**안 지키면 생기는 문제.** 프로젝트 컨테이너의 계약을 씬 설치 파일이 다시 바인딩하면 그 씬의 단일 조회는 모두 씬 것을 받고, 경고는 없습니다. 반면 `ResolveAll` 과 목록 주입은 부모 것까지 받아, 한쪽은 하나, 한쪽은 둘을 봅니다.

**이렇게 쓴다.** 전역 서비스는 프로젝트 컨테이너 한 곳에만 바인딩합니다. 씬마다 바꾸는 것이 의도라면 그 계약은 프로젝트 쪽에 두지 않습니다. 목록 범위를 좁혀야 하면 `[Inject(Source = InjectSources.Local)]` 을 적습니다.

### 3. 목록 주입은 비어 있어도 통과한다

**안 지키면 생기는 문제.** 목록 요청은 선택으로 바뀌므로 원소 바인딩이 하나도 없어도 빈 목록을 받고 검증도 통과합니다. 설치 파일에서 한 줄을 지운 실수가 "아무 일도 안 일어나는 기능" 으로만 드러납니다.

**이렇게 쓴다.** 적어도 하나는 있어야 하는 목록이면 생성자에서 개수를 검사해 바로 예외를 던집니다. 단일로 받을 수 있는 자리라면 단일로 받아, 빠졌을 때 `Unable to resolve` 가 나게 합니다.

## 정리

Zenject 의 단일 조회는 **가까운 컨테이너, 조건 있는 바인딩 순으로 하나를 고르고, 같은 등급이 둘이면 처음 조회할 때 예외**입니다.

- 조회 키는 (계약 타입, Id) 쌍이고, 범위는 `Source` 로 `Any`, `Local`, `Parent`, `AnyParent` 중에서 고릅니다.
- 자식의 바인딩은 부모 것을 경고 없이 가리고, 모호성은 등록할 때 막지 않습니다.
- `ResolveAll` 과 목록 주입은 모든 컨테이너의 후보를 모으고, 목록은 비어 있어도 예외 없이 통과합니다.