ITEM 11 :: EFFECTIVE C#
안녕하세요, 11번째 시간입니다. 휴가를 다녀와서 그런지 오랜만에 찾아온 기분이 듭니다.
이번 챕터는 .Net 리소스 관리에 관한 이해라는 주제로 찾아왔습니다. 개발자들이라면 확실하게 알아야할 부분이기도 하고, 2장 시작부터 꽤 중요한 부분에 대해서 하게 되었네요.
내용이 조금 길어질 수 있으니 바로 들어가도록 하겠습니다!
설명
.NET 환경에서는 특히 메모리 관리와 GC의 동작 방식을 정확하게 이해하는 것이 무엇보다 중요하다.
GC(가비지 컬렉터)는 관리되는 메모리를 관장하며 네이티브 환경과는 다르게 메모리 누수(memory leak), 댕글링 포인터(이미 해제된 메모리 영역을 가리키고 있는 포인터), 초기화 되지 않은 포인터와 같은 여타 메모리 관리 문제를 개발자들이 직접 다루지 않도록 자동화 해주는 역할을 한다!
또한, 개발자가 메모리 관리에 대해서 올바르게 작업을 수행하면 더욱 효과적으로 GC가 돌아가게 된다.
하지만, 비관리 리소스는 여전히 개발자가 직접 관리해야 한다.(객체 : DB Connect, GDI+,COM, System) 여기에 더해 C#의 경우 이벤트 핸들러나 델리게이트를 잘못 사용하면 이들이 참조하고 있는 객체들이 불필요하게 오랫동안 메모리에 남게된다.
여기서 GC의 역할을 간략하게 설명하자면, 사용되지 않는 메모리들을 정리하고 나머지 공간들을 콤팩트라고 불리우는,( 콤팩트는 쉽게 말해서 빈 공간이 없도록 메모리를 조정하고, 나머지 공간을 더욱 넓게 쓸 수 있도록 하는 GC의 작업중 하나다. 좀 더 심도 깊게 말하면, 관리 힙에 대하여 사용 중인(혹은 도달가능한) 객체들을 한쪽으로 차곡차곡 옮기고 나머지 조각난 가용 메모리를 단일의 큰 메모리 공간으로 만드는 과정이라고 한다. ) 작업을 통해 정리하는 역할을 한다.
이러한 역할을 어떻게 하는지 알기 위해서는 GC의 내부원리를 알아야하는데, GC의 내부원리는, 응용 프로그램 내의 최상위 객체로부터 참조 트리를 구성하여 도달 가능한 객체를 살아 있는 객체로 판단하고 도달 불가능한 객체를 가비지로 간주한다. 집 주소에 찾아가보고, 사람이 있는지 없는지 판단하는 로직과 얼추 비슷하다.
여기서, 비관리 리소스의 생명 주기를 관리하고 싶을 때 개발자가 쉽게 사용할 수 있도록, Finalizer와 Idisposable 인터페이스 라는 두 가지 메커니즘을 .NET에서는 준비해놓았다. 하지만, Finalizer는 단점이 많기 때문에 Idisposable 인터페이스를 통해서 적시에 비관리 리소스가 빠르게 해제될 수 있도록 구현하는 것이 좋다.
Idisposable 인터페이스는 직접 찾아보면 많은 자료가 있으므로 여기서 깊게 설명하진 않겠다. 간단하게 말하면 Dispose를 적시에 호출할 수 있게 작성한 인터페이스를 상속받는 것이다. 그리고, 소멸자에서 Dispose를 호출하는 것이 아니라 원하는 시기에 호출하는 식으로 구현한다. 구현 예는 아래와 같다.
public class DisposableClass : IDisposable
{
public DisposableSample()
{
}
~DisposableSample()
{
this.Dispose(false);
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
Protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
if (disposing)
{
//IDisposable 인터페이스를 구현하는 멤버들을 여기에서 정리합니다. (메모리 정리 부분)
}
// .Net Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다. (System, COM, DB Connect, GDI+ ...etc)
this.disposed = true;
}
}
다음으로 Finalizer에 대한 설명을 하겠다. Finalizer는 GC가 호출한 이후에 실행되어 메모리를 해제시키는 방식인데, 문제는 메모리가 해제되는 때를 정확하게 알 수 없다는 것이다. 언젠가 객체가 정리는 되겠지만, 그 정리 될 때를 모르니 개발하는 데에 있어서는 치명적일 수 밖에 없다.
결국 명시적으로 원하는시기에 메모리를 해제하기 위해서는 Idisposable 인터페이스가 필수조건이라는 것이다. 물론, 이런상황에서도 .NET의 GC는 진화하여 세대라는 기능을 사용해 가비지가 될 가능성이 높은 객체를 더 빠르게 찾아낼 수 있게 되었다.
세대란, 1회 GC가 호출되었음에도 불구하고 살아남은 객체에 한해서 1세대로, 그 이후에 GC를 실행한 이후에도 살아남는다면 2세대와 같은 식으로 되며, 0세대부터 시작한다.
또한, 세대별로, 낮은 세대라면 가비지가 될 가능성이 높은 객체이므로 더욱 정밀하게 관리하고 세대가 높은 객체는 조금 제한적으로 GC를 실행하여 내부적인 기능 향상을 볼 수 있게 만들어져 있다.
다만 세대기능이 생기면서 Finalizer는 더욱 단점이 부각되었는데, 그 단점이란, 세대가 적용되는 버전에서 Finalizer 를 사용하게 되면 Finalizer를 들고 있기 때문에 바로 해제되지 않아, 최소 1세대로 인식하게 되고. 따라서 많은 횟수의 GC 검사가 필요하며, 이로 인해 불필요한 검사횟수가 늘어나 내부적으로 성능 하락을 만들어낸다는 것이다. (2세대까지 넘어가면 100번이나 검사를 더 해야 메모리를 정리할 수 있다.)
정리
따라서, 내부 리소스 관리를 할때에는, Finalizer를 사용하는 것보다, Idisposable 인터페이스를 구현하여 가비지 수집과정이 지연되는 것을 방지하고 적시에 명시적으로 호출하여 응용프로그램 개발의 관리 환경을 더 효율적으로 관리하는 것이 좋다!