VContainer 는 자기가 처음 만든 것만 해제한다
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()가 불리지만 게임오브젝트는 그대로 남습니다.