본문 바로가기

IT 살이/04. 기술 - 프로그래밍

null, Finalize 메소드, Dispose 메소드, Dispose 패턴


메모리 관리를 설명하는 부분을 보면 항상 나오는 말들인데도, 처음 개발에 들어선 개발자들에겐 돌아서면 헛갈리는 부분이다. 이 포스트에서는 이것들의 차이점을 정리해 본다. 

■ null !

이것의 의미는 분명하다. 메모리상에 존재하는 객체에 대한 모든 참조의 끈을 끊는다는 의미이다. 만약 메모리상의 객체를 참조하는 변수가 모두 null로 되면 메모리상의 객체는 가비지 컬렉트 (Garbage Collect)후보가 된다. 이때 가비지 컬렉팅이 일어나면 해제된다.

null의 원래 의미는 이렇지만, .NET 프레임워크에서는 디버깅모드와 릴리스 모드( + 최적화모드)에 따라서 다르게 해석할 수 있다. 다음 코드를 보자.

[STAThread]

static void Main()

{

    Timer t = new Timer();



    // 필요한 작업을 한다.


    Console.Read();


    t = null;

}

필요한 작업을 하고 코드는 사용자로부터 키 입력을 기다릴것이다. Main 메소드가 호출될때 이 메소드는 컴파일(JIT 컴파일)되고 Timer 객체 t가 메모리에 생성될 것이다. 그러나 Timer 객체를 참조하는 t에 null을 설정함으로써 이제 이 객체를 가리키는 참조가 하나도 없게 된다. 만약 최적화 옵션(그런 것이 있단다-_-;;)이 활성화된 상태의 JIT 컴파일러가 컴파일을 하게 되면 이 경우 Timer객체를 참조하는 변수가 하나도 없다고 판단한다.

그래서 사용자로부터 키 입력을 기다리고 있는 동안 가비지 컬렉팅이 수행되면  이 Timer 객체는 가비지 컬렉션의 후보가 될 것이다.  메소드가 아직 실행중이라는 것은 객체의 가비지 컬렉팅을 막아주지 못한다는 사실을 염두에 둘 필요가 있다.

그러나 디버깅 모드에서는 메소드(여기서는 Main)도 하나의 가상 참조(참조 그래프상의 "Root"노드가 되는 것이다)로 여긴다. 무슨 말인가 하면 Main 메소드라는 참조가 Timer 객체를 참조하고 있다고 해석하는 것이다. 결과적으로 디버깅모드에서는 가비지 컬렉팅이 일어나도 Timer객체는 살아남게 되는 것이다.

그러나 어플리케이션이 디버깅 모드에서는 잘 돌아가다가 릴리스 모드에서는 에러가 발생한다면 난처한 일이 아닐 수 없다. 이것을 해결하는 방법으로 객체를 해제할때 null대신에 Dispose 메소드를 사용하면 된다.

■ Finalize 메소드

.NET에서 Finalize 메소드는 일반 메소드와 달리 클래스명 앞에 틸드(~)가 붙어서 "~클래스명() "형태를 갖는 메소드를 말한다. 어떤 타입에 대해서 Finalize 메소드를 정의해놓게 되면 가비지 컬렉팅 작업에 의해서 객체가 수거되려고 할때 GC는 그 타입의 Finalize 메소드를 호출해서 정의된 메모리 해제 작업을 수행한다.

■ Dispose 메소드

Finalize 메소드는 GC가 호출하는 메소드인 반면에 Dispose 메소드는 객체의 클라이언트가 직접 호출할 수 있는 메소드이다. null은 객체의 메모리 해제 시기를 결정할 수 없다. GC(Garbage Collector)가 컬렉팅을 수행할때까지 메모리에 남게 된다. 해서 객체를 사용하는 클라이언트측에서 직접 객체가 차지하는 자원을 해제할 수 있는 방법을 제공해줄 수 있는 방법이 Dispose 메소드이다. 앞의 코드를 Dispose()를 이용해서 변경하면 다음과 같다.

[STAThread]

static void Main()

{

    Timer t = new Timer();



    // 필요한 작업을 한다.


    Console.Read();

    t.Dispose(); // t = null;

}

이런 식으로 코딩을 하면 디버깅 모드이든, 최적화된 릴리스 모드이간에 Timer 객체에 대한 참조가 여전히 유효하게 되어 Timer 객체는 가비지 대상이 되지 않는다.

■ Dispose 패턴

IDisposable 인터페이스를 구현하는 .NET 프레임워크 제공의 모든 객체들은 Dispose() 메소드를 호출해서 클라이언트가 직접 원하는 시기에 해제할 수 있도록 하고 있다. 만약 Dispose()를 지원하는 타입을 사용자가 직접 정의한다면 Dispose()에서 자원을 해제하는 코드를 원하는 대로 넣으면 된다.

Dispose()를 직접 정의할때 주의할 점은 클라이언트가 직접 Dispose()를 호출해서 자원을 해제했다면 그 자원 해제 사실을 GC가 알 수 있도록 표시를 해 줘야 한다. 그래야  이미 해제 했던 자원을 GC가 또 해제하지 않을 수 있게 된다. 만약 또 해제하려고 한다면 에러가 발생한다. 따라서 Dispose 패턴 이라는 것이 나오게 된다. 이 패턴은 객체의 클라이언트와 GC가 자원 해제를 중복되게 시도하지 않고 안전하게 자원을 해제할 수 있는 코딩 패턴을 말해준다. 실제 Dispose 코딩 패턴에 대해서는 "dipose pattern"이라는 검색어로 구글링해보면 자세히 알아 볼 수 있을 것이다.