리소스 해제를 위한 .NET 개발환경에서 제공하는 Dispose 패턴을 파악하기 위해 테스트용으로 적용할 클래스 정의는 다음과 같으며 총 세가지의 경우로 시험을 해보았다.
ref class T : public IDisposable { public: T() { Console::WriteLine(L"T() invoked"); } ~T() { Console::WriteLine(L"~T() invoked"); } !T() { Console::WriteLine(L"!T() invoked"); } };
첫번째 시험 코드는 마치 지역변수처럼 할당하는 경우이다. 하지만 절대 지역 변수가 아니라는 점.. CLR Heap에 할당된다.
int main(array ^args) { T a; return 0; }
실행 결과는 다음과 같다. 지역변수처럼 변수의 유효 Scope를 벗어나는 순간 소멸자가 호출되었다. 실제 IL에 의해 구현된 내부 흐름은 소멸자 호출이 아니라 IDisposable::Dispose 매서드의 호출이다. 즉, 소멸자가 IDisposable::Dispose의 재구현이다. 또한 내부적으로 GC에 의해 호출되어져야했을 Finalize 매서드는 호출되지 않게 조치된다. C#과는 다르게 Finalize가 호출되지 않도록 자동화되었다는 점이 매우 특이하다.
T() invoked ~T() invoked
두번째 시험 코드는 C++에서는 포인터 개념으로 생각되는, 즉 C++/CLI는 Handle 개념으로 CLR의 Heap에 객체를 생성하였고 delete를 호출하지 않은 경우이다.
int main(array ^args) { T ^a = gcnew T(); return 0; }
실행 결과는 다음과 같다. Finalize에 해당하는 !T()가 호출되었다는 점에 유의하자.
T() invoked !T() invoked
세번째 시험 코드는 두번째와 다르게 delete 연산자를 적용해 주었다.
int main(array ^args) { T ^a = gcnew T(); delete a; return 0; }
실행 결과는 다음과 같다. Finalize가 아닌 Dispose가 호출되었다.
T() invoked ~T() invoked
여기서 얻을 수 있는 가장 중요한 한가지 결론은 ~T()에 해당하는 Dispose()와 !T()에 해당하는 Finalize()의 코드는 절대로 같이 호출되지 않는다는 점이다. C++/CLI의 사용자가 .NET의 참조형 변수를 지역변수처럼 사용하든지, gcnew에 의해 할당하여 사용하든지.. 또한 사용한 후 delete를 했든지, 하지 않았든지 간에 ~T()와 !T() 둘중에 하나는 반드시 실행되다는 점이다. ~T()는 기존의 C++ 개념으로써 호출되며 !T()는 .NET의 GC에 의해 호출된다. 즉, 리소스 해제를 위한 코드는 ~T()와 !T()에 똑 같이 중복적으로 와야한다고 생각한다.
실험을 통한 🙂 결과 감사합니다. 틈틈히 내부를 분석하시다니 정말 놀랍네요 🙂
arload님, 댓글 감사합니다~ 블로그에 잠시 방문해 봤습니다. 아키텍쳐가 주토픽인듯 싶습니다. 생각날때 종종 방문하겠습니다~ ^^
좋은 글 잘 읽고 갑니다.
소멸 부분에서 혼동 되던 부분에 대한 글을 읽게 돼서 좋네요. 🙂
네, 도움이 되셨다니 좋습니다!
C++/CLI에서
1. 스택되감기에 의한 해제는 Dispose() 가 호출 = ~T()
2. delete 에 의한 해제는 Dispose() 가 호출 = ~T()
3. GC에 의한 해제는 Finalize()가 호출 = !T()
잘 보고 갑니다.
그러면, !T()가 없으면, ~T()가 호출 되는건가요?
백견이 불여일행! 직접 해보시길! ^^
잘 읽었습니다… 좀 헷갈려서 저도 테스트를 해 봤습니다. 그리고 추가적인 실험결과를 공유합니다.
Native C++ 에서는 상속관계에 있어서 부모의 소멸자를 호출하기 위해서는 “~” 소멸자에 virtual 키워드를 붙여야 합니다. 하지만 C++/CLI 의 ref class 는 “~” 든 “!” 든 virtual 키워드를 붙이지 않아도 되더군요. 상속구조에 맞게 알아서 소멸자를 불러 줍니다( “!” 는 아예 virtual 을 지정할 수도 없습니다 ).