VContainer 는 자기가 처음 만든 것만 해제한다
Esc

    VContainer 는 자기가 처음 만든 것만 해제한다

    2026년 9월 30일 수정–UnityDI라이브러리
    public class LogWriter : ILogWriter, IDisposable
    {
        StreamWriter writer = new StreamWriter("log.txt");
        public void Dispose() => writer?.Dispose();
    }
    
    builder.Register<ILogWriter, LogWriter>(Lifetime.Transient);

    이 코드는 요청할 때마다 파일 핸들을 하나씩 흘립니다. Dispose() 는 한 번도 안 불립니다.

    VContainer 는 이번에 처음 만들었고, IDisposable 이고, 컨테이너가 만든 것만 해제 목록에 올립니다. 목록은 스택이라 스코프가 닫힐 때 생성 역순으로 풀립니다.

    해제는 생성의 역순이다

    해제 목록은 Stack<IDisposable> 입니다. 만들 때 넣고, 해제할 때 위에서부터 꺼냅니다. Presenter 가 Session 을, Session 이 Connection 을 참조하는 스코프를 닫아 봅니다.

    만든 순서대로 닫으면 Connection 이 먼저 죽어 Session.Dispose() 가 죽은 연결을 건드립니다. 역순이라 의존하는 쪽이 먼저 닫히고 의존받는 쪽이 끝까지 남습니다. 컨테이너 사이에서도 같은 원칙이라, 최상위 컨테이너는 품고 있는 rootScope 를 먼저 닫고 자기 싱글턴을 닫습니다. Dispose() 를 락 밖에서 부르므로 해제 도중에 새로 Add 가 일어나도 데드락이 없고, do-while 이라 그 항목도 처리됩니다.

    해제 목록에 오르려면 조건 셋을 다 넘어야 한다

    Singleton 과 Scoped 는 인스턴스를 처음 만드는 자리에서 목록에 넣을지를 정합니다. IsValueCreated 를 Value 앞에서 읽어 이번에 처음 만든 것인지 보고, IDisposable 인지, 밖에서 받은 객체(ExistingInstanceProvider)가 아닌지를 봅니다. 등록 방식별로 걸어 봅니다.

    그래서 이렇게 갈립니다.

    등록 방식 해제되나 왜
    Register + Singleton 또는 Scoped 됩니다 컨테이너가 만들었으니 소유합니다
    Register + Transient 안 됩니다 추적 자체를 안 합니다
    RegisterInstance 안 됩니다 밖에서 만든 것은 밖의 책임입니다
    RegisterFactory(func) 안 됩니다 내부적으로 같은 취급입니다
    팩토리가 실행 중에 만든 객체 안 됩니다 컨테이너가 생성 시점을 모릅니다
    컴포넌트 등록 Dispose() 는 불립니다 제외 조건에 안 걸립니다

    컴포넌트는 Dispose 만 당하고 살아남는다

    씬에 이미 있는 컴포넌트를 넘겨 등록하는 것은 RegisterInstance 처럼 밖에서 만든 것을 맡기는 일입니다. 그런데 결과가 다릅니다. IDisposable 을 구현한 객체를 두 방식으로 등록하고 스코프를 닫으면 이렇게 됩니다.

    등록 스코프가 닫힐 때 Dispose() 게임오브젝트
    RegisterInstance(obj) 안 불림 (해당 없음)
    RegisterComponent(component) 불림 씬에 그대로 남음
    RegisterComponentInHierarchy, RegisterComponentInNewPrefab 불림 씬에 그대로 남음

    차이는 “밖에서 만든 것은 해제하지 않는다” 는 검사가 RegisterInstance 로 들어온 것만 알아보기 때문입니다. 컨테이너는 등록마다 인스턴스를 꺼내 주는 프로바이더를 두는데, 검사는 그 프로바이더가 ExistingInstanceProvider 인지 하나만 봅니다.

    // Container.cs, 해제 목록에 넣을지 정하는 줄
    if (!created && instance is IDisposable disposable && !(registration.Provider is ExistingInstanceProvider))
        disposables.Add(disposable);

    컴포넌트 등록은 ExistingComponentProvider, FindComponentProvider, PrefabComponentProvider 같은 다른 프로바이더를 씁니다. 이름이 비슷해도 별개 클래스라 이 검사에 걸리지 않고, 해제 목록에 들어갑니다. 그리고 컨테이너가 하는 일은 Dispose() 를 부르는 것까지라 Destroy(gameObject) 는 하지 않습니다. 그래서 오브젝트는 씬에 살아 있는데 Dispose() 는 이미 불린 상태가 됩니다.

    주의할 점

    1. IDisposable 을 Transient 로 등록하지 않는다

    안 지키면 생기는 문제. Transient 는 조건을 보러 오지도 않고 만들어 돌려주고 끝납니다. 맨 위의 LogWriter 처럼 요청할 때마다 닫히지 않는 객체가 하나씩 쌓입니다.

    이렇게 쓴다. Scoped 로 바꿔 스코프가 닫게 합니다. 매번 새로 만들어야 하면 팩토리로 받고 호출부에서 using 으로 감쌉니다.

    2. RegisterInstance 로 넣은 것은 직접 닫는다

    안 지키면 생기는 문제. 밖에서 만든 객체는 밖의 책임이라 컨테이너가 닫지 않습니다. 연결 객체를 이렇게 넣으면 스코프가 죽어도 연결은 열려 있습니다.

    이렇게 쓴다. 컨테이너가 만들게 넘기거나, 해제 콜백을 직접 답니다.

    builder.Register<IConnection>(_ => new TcpConnection(host, port), Lifetime.Singleton);
    // 또는
    builder.RegisterDisposeCallback(_ => connection.Dispose());

    3. 컴포넌트에 IDisposable 을 구현하면 살아남는다고 계산한다

    안 지키면 생기는 문제. 스코프가 닫힐 때 Dispose() 가 불리지만 게임오브젝트는 파괴되지 않습니다. DontDestroyOnLoad 로 살려 둔 오브젝트라면 해제된 채로 계속 돕니다.

    이렇게 쓴다. Dispose() 에서 정리한 뒤에는 그 컴포넌트가 다시 쓰이지 않게 합니다. 오브젝트의 수명은 씬이나 직접 부르는 Destroy 가 정한다고 보고 짭니다.

    정리

    VContainer 는 자기가 처음 만든 IDisposable 만 해제하고, 생성 역순으로 닫습니다.

    • 해제 목록이 스택이라 의존하는 쪽이 먼저 닫히고 의존받는 쪽이 끝까지 남습니다.
    • Transient, RegisterInstance, 팩토리가 만든 객체는 해제 목록에 오르지 않습니다.
    • 컴포넌트 등록은 Dispose() 가 불리지만 게임오브젝트는 그대로 남습니다.