(

목차


CLR(공용 언어 런타임)이 관리 코드 개발자의 생산성 향상에 가장 크게 기여하는 부분 중 하나는 바로 관리되는 힙에 할당된 메모리가 더 이상 사용되지 않을 경우 GC(가비지 수집기)를 통해 정리된다는 점입니다. 따라서 개발자는 메모리 누수, 해제된 메모리 사용 시도, 이중 메모리 회수 등으로 인해 발생하는 여러 가지 어려운 문제를 해결하기 위해 시간을 허비하지 않아도 됩니다. 그러나 가비지 수집기는 메모리 누수를 방지하는 데 효과적인 도구이지만 회수가 필요한 다른 유형의 리소스에 대해서는 아무 기능도 제공하지 않습니다. 예를 들어 가비지 수집기는 파일 핸들을 닫거나 CoAllocTaskMem과 같은 API를 사용하여 관리되는 힙 외부에 할당된 메모리를 해제할 수 없습니다.

이러한 유형의 리소스를 관리하는 개체는 리소스가 더 이상 필요하지 않을 때 해제할 방법이 필요합니다. 이를 위해서는 System.Object의 Finalize 메서드를 다시 정의하여 이 개체에 관련된 리소스를 가비지 수집기에서 수집하도록 설정해야 합니다. 참고로 C#의 경우 메서드를 직접 다시 정의하는 대신 C++의 소멸자 구문인 ~MyObject를 사용할 수 있습니다. 클래스에 종료자가 있으면 이러한 유형의 개체가 수집되기 전에 가비지 수집기에서 개체의 종료자를 호출하여 해당 개체가 점유하고 있는 리소스를 모두 정리할 수 있습니다.

하지만 이 시스템에서는 가비지 수집기가 능동적으로 실행 시점을 결정하지 않으므로, 개체에 대한 마지막 참조가 사라진 후에도 개체가 장시간 종료되지 않을 수 있다는 문제점이 있습니다. 따라서 개체가 데이터베이스 연결과 같이 중요하거나 민감한 리소스를 점유하고 있는 경우에는 이러한 시스템이 적합하지 않습니다. 예를 들어 사용 가능한 연결이 10개뿐이고 개체가 그 중 하나를 점유하고 있다면 가비지 수집기에서 종료자 메서드를 호출할 때까지 기다리지 말고 가능한 한 빨리 연결을 해제해야 합니다.


삭제 가능한 개체

가비지 수집기가 작동할 때까지 막연히 기다리지 않으려면 리소스를 점유한 개체 유형에서 IDisposable 인터페이스를 구현하여 이러한 유형의 소비자가 리소스를 제때에 해제할 수 있도록 해야 합니다. IDisposable의 구현은 소비자에게 개체의 사용이 끝난 시점에서 최대한 빨리 Dispose를 호출하여 더 이상 필요하지 않은 리소스를 해제해야 함을 알리는 일종의 힌트입니다. C# 및 Visual Basic에서는 이 프로세스를 비교적 간단하게 구현할 수 있도록 "using" 키워드를 제공합니다. C#의 경우 IDisposable을 구현하는 MyClass라는 클래스가 있다고 할 때 다음과 같은 코드를 실행하면 컴파일러에서 그림1과 유사한 코드를 생성합니다.

using (MyClass myClass = GetMyClass()){    myClass.DoSomething();}

try/finally 블록은 using 블록의 코드에서 예외가 발생한 경우에도 MyClass의 Dispose 메서드가 개체를 정리할 수 있도록 합니다. 컴파일러에서 생성된 이 코드는 Dispose 메서드를 호출하기 전에 두 가지 흥미로운 작업을 수행합니다. 첫 번째 작업은 삭제 가능한 개체가 null이 아닌지 확인하는 것으로, 이 작업은 변수에 할당된 식이 null로 계산될 경우 using 블록에 의해 응용 프로그램에서 NullReferenceException이 발생하지 않도록 합니다. 두 번째 작업은 IDisposable 참조를 통해 Dispose를 호출하는 것으로, 이 작업은 IDisposable을 명시적으로 구현하는 개체 유형이 제대로 정리될 수 있도록 합니다.

IDisposable이 명시적으로 구현된 클래스에서도 using 블록을 사용할 수 있지만 이러한 클래스에서 이와 같은 방식으로 인터페이스를 구현하는 것은 바람직하지 않습니다. IDisposable을 명시적으로 구현할 경우 개발자가 Visual Studio에서 IntelliSense를 사용하여 개체 모델을 살펴볼 때 개체에 Dispose 메서드가 있다는 사실을 파악하지 못하여 빠른 정리 기능을 활용하지 못할 수 있습니다. 이렇게 되면 가비지 수집기가 종료자를 실행할 때까지 개체에서 리소스를 점유하게 됩니다.

가비지 수집기의 관점에서 보면 IDisposable은 인터페이스 중 하나일 뿐이라는 점을 유의해야 합니다. 가비지 수집기는 삭제 가능한 개체를 IDisposable을 구현하지 않는 개체와 다르게 취급하지 않습니다. 다시 말해, 가비지 수집기에서 개발자를 위해 Dispose 메서드를 호출해주지 않습니다. 가비지 수집기에서 호출하는 정리 메서드는 개체의 종료자뿐입니다. 따라서 Dispose 메서드를 명시적으로 호출하도록 코드를 작성하지 않으면 이 메서드가 호출되지 않습니다.

Back to top

삭제 가능한 패턴

C# 및 Visual Basic에서 "using" 구문을 이용하면 삭제 가능한 개체를 손쉽게 정리할 수 있다는 사실을 살펴보았습니다. 이제 반대로 삭제 가능한 유형을 만들어야 하는 경우를 생각해 보아야 합니다. IDisposable 인터페이스는 단일 메서드만 정의하므로 삭제 가능한 유형의 인터페이스를 빠르고 쉽게 구현할 수 있다고 생각할 것입니다. 그러나 Joe Duffy의 IDisposable 지침에 설명되어 있는 것과 같이 완전한 삭제 가능 유형을 구현하는 데는 여러 가지 미묘하고 세부적인 문제가 있습니다.

Dispose 메서드에 대한 중요한 사실 하나는 이 메서드의 정리 작업이 개체의 종료자가 실행하는 정리 작업과 매우 유사하다는 것입니다. 따라서 코드 중복을 방지하기 위해 이 정리 코드를 한 위치에서 복사해서 다른 위치에 붙여 넣는 것보다, 두 위치에서 같은 정리 코드를 공유하는 편이 낫습니다. 뿐만 아니라 이렇게 하면 해당 유형의 모든 정리 코드를 한곳에서 관리할 수 있다는 이점도 있습니다. 이 코드를 공유하는 확실한 방법은 다음과 같이 모든 정리 코드를 Dispose 메서드에 포함하고 개체의 종료자에서 Dispose를 호출하도록 하는 것입니다.

public class MyClass : IDisposable{    ~MyClass()    {        Dispose();    }    public void Dispose()    {        // Cleanup    }}

이러한 방식으로 종료자를 구현하면 정리 코드를 한곳에서 관리할 수는 있지만 한 가지 문제가 있습니다. 일부 정리 코드는 Dispose와 종료자 간에 공유될 수 있지만, Dispose 메서드에서는 해제하되 종료 과정에서는 해제하지 않아야 할 리소스도 있기 때문입니다.

Back to top

관리되는 리소스와 네이티브 리소스

종료자는 시기를 판단하여 실행되지 않으므로 종료자를 실행할 때 개체에서 참조하는 다른 개체가 아직 종료되지 않았을 수도 있습니다. 예를 들어 다음과 같은 클래스가 있다고 가정해 보겠습니다.

public class Database : IDisposable{    private NetworkConnection connection;    private IntPtr fileHandle;    // ...}public class NetworkConnection : IDisposable{    private IntPtr connectionHandle;    // ...}

Database 클래스에는 두 가지 리소스, 즉 NetworkConnection 유형의 리소스 하나와 파일 핸들을 나타내는 IntPtr이 있습니다. Database 유형의 개체에 대한 활성화된 참조가 있을 때는 가비지 수집기가 연결 멤버 변수에서 참조하는 NetworkConnection 개체를 수집하지 않습니다. 그러나 가비지 수집기가 Database의 종료자를 호출하면 개체에 대한 라이브 참조가 더 이상 없기 때문에 개체가 수집 대상이 된다는 것을 알 수 있습니다.

NetworkConnection 개체에 대한 다른 라이브 참조가 없으면 데이터베이스 종료자를 실행하기 전에 해당 개체가 이미 종료되어 있을 수도 있습니다. 이미 종료된 개체는 사용 가능한 상태가 아닐 가능성이 크므로 사용하지 않는 것이 바람직합니다. 즉, 종료 순서가 명확하지 않기 때문에 Database 종료자가 연결 필드를 참조해서는 안 됩니다.

그러나 Dispose 메서드를 호출하여 개체를 정리하는 경우에는 개체에 대한 라이브 참조가 있어야만 Dispose가 호출되므로 개체가 종료되지 않음을 알 수 있습니다. 다시 말해, Database 개체가 여전히 유지되고 연결 필드에서 참조하는 NetworkConnection의 GC 루트 역할을 하므로 NetworkConnection 개체가 종료되지 않습니다. 따라서 Dispose 메서드에서 종료 가능한 멤버 변수에 액세스해도 문제가 없습니다.

그러나 fileHandle 필드의 경우에는 사정이 다릅니다. 이 핸들 필드는 가비지 수집기에서 종료되는 항목이 아니라 단순한 IntPtr에 불과합니다. 따라서 종료자와 Dispose 메서드에서 fileHandle 필드에 액세스해도 아무 문제가 없습니다.

이 원칙을 일반화해 보면, Dispose 메서드에서는 관리되는 개체든, 네이티브 리소스든 관계없이 개체가 점유하고 있는 모든 리소스를 정리해도 상관이 없지만, 종료자에서는 종료할 수 없는 개체만 정리하는 것이 안전하며 일반적으로 종료자는 네이티브 리소스만 해제합니다.

이 예에서 연결 필드에 네이티브 연결 핸들이 분명히 포함되어 있지만 연결 필드는 네이티브 리소스로 간주되지 않는다는 사실을 알 수 있습니다. NetworkConnection 클래스의 내부 구현에는 네이티브 리소스가 포함되어 있지만, NetworkConnection 유형은 자체 리소스의 수명 주기 관리를 담당하므로 Database 클래스의 관점에서 NetworkConnection은 관리되는 리소스입니다. 그러나 NetworkConnection 클래스 내에서 connectionHandle은 명시적으로 정리하도록 코드를 작성하지 않는 한 정리되지 않으므로 네이티브 리소스라고 할 수 있습니다.

삭제 시에는 리소스를 모두 정리하고 종료 시에는 리소스 중 일부를 정리해야 하는 경우, 정리 코드를 메서드로 만들어 종료자와 Dispose 메서드에서 호출하는 방법을 사용할 수 있습니다. 이 새 메서드를 사용하면 삭제 시에는 모든 리소스를 정리하고 종료 시에는 네이티브 리소스만 정리할 수 있습니다. 이렇게 하려면 그림2의 패턴에 따라 삭제 가능한 구현을 만들어야 합니다.

이제 모든 정리 코드가 Dispose 메서드의 재정의 코드에 포함되므로 코드 중복을 피하고 정리 코드를 한곳에서 관리한다는 원래 목표를 여전히 만족하는 셈입니다. 이 기사의 나머지 부분에서는 다른 Dispose 메서드와 구분하기 위해 이 메서드를 정리 메서드라고 지칭하도록 하겠습니다. 정리 메서드는 삭제 코드 경로를 실행하고 있는지, 종료 코드 경로를 실행하고 있는지를 나타내는 부울 매개 변수를 취합니다. 두 가지 코드 경로 모두 클래스에서 점유하고 있는 네이티브 리소스를 정리해야 하며 Dispose 코드 경로에서는 관리되는 리소스도 정리해야 합니다.

클래스 소비자가 직접 정리 메서드를 호출할 필요는 없으므로 정리 코드는 공용으로 만들지 않아도 됩니다. 대신 클래스 소비자는 IDisposable 인터페이스를 통해 정리 논리에 액세스할 수 있습니다. 클래스 소비자가 정리 메서드를 직접 호출할 필요는 없지만, 하위 클래스에서 호출해야 할 수도 있으므로 정리 메서드는 비공개가 아니라 보호된 것으로 설정됩니다. 삭제 가능한 유형의 하위 클래스에 대해서는 나중에 다시 설명합니다.

Dispose는 개체가 점유하고 있는 모든 리소스를 해제하므로 가비지 수집기에서 개체를 종료할 필요가 없습니다. 가비지 수집기에 종료자가 있더라도 개체를 종료할 필요가 없음을 알리기 위해 Dispose 메서드에서는 정리 코드가 실행된 후에 GC.SuppressFinalize를 호출합니다.

사실 네이티브 리소스를 소유하지 않는 개체 유형의 경우 종료자에서 실행하는 코드가 아무런 효과도 없습니다. 이 경우 클래스에서 종료자 메서드를 정의해서는 안 됩니다.

Database 예의 새 정리 메서드는 그림3과 같습니다. Database 개체를 삭제하는 경우 관리되는 연결 리소스와 fileHandle 네이티브 리소스가 모두 정리됩니다. 그러나 개체를 종료하는 경우에는 fileHandle 리소스만 정리됩니다. 정리 코드는 여러 번 호출할 수 있습니다. Dispose 메서드를 통해 이루어지는 첫 번째 호출에서 Database 개체에서 점유하고 있는 모든 리소스를 해제하면, Dispose에 대한 이후 호출은 리소스가 이미 해제되었다는 사실을 확인하고 해당 리소스의 회수를 시도하지 않은 채로 종료됩니다. 불필요하게 Dispose를 여러 번 호출해도 성공적으로 실행되도록 Dispose 코드를 구현해야 합니다. 개체에 대한 다른 메서드의 경우 개체가 삭제된 후에 호출되면 ObjectDisposedException이 발생할 수 있습니다.

정리 코드에서는 예외가 발생하지 않는다는 사실도 알 수 있습니다. 일반적으로 개발자는 예외를 발생시키지 않고 개체를 정리하기를 원합니다. 정리 코드 경로를 실행하는 동안에는 예외 상태로부터 복구할 수 있는 마땅한 방법이 없기 때문입니다. 정리 과정에서 개체 오류가 발생하면 해당 개체의 소유자는 다시 시도한 후에 두 번째 시도가 성공하기를 바라거나 정리를 포기하고 일부 리소스를 정상적으로 해제되지 않은 채로 남겨 둘 수밖에 없습니다.

정리 코드에서 예외가 발생하지 않도록 하려면 정리 메서드에서 액세스하는 개체 참조를 검사하여 null이 아닌지, 그리고 오류가 발생하지 않는 메서드만 호출하는지 확인해야 합니다. 개체를 종료하는 경우에는 이러한 과정이 특히 중요합니다. 2.0 버전의 CLR에서 Dispose를 호출하는 동안 예외가 발생하면 리소스의 효율성이 떨어질 뿐이지만, 종료자 스레드의 처리되지 않은 예외는 기본적으로 전체 프로세스를 중단시킵니다.

종료자 스레드를 실행하지 않더라도 정리 도중에 예외가 발생하면 다른 리소스가 정상적으로 정리되지 않게 됩니다. 예를 들어 그림4를 살펴보겠습니다. 여기서 DisposableClass에는 Foo 유형과 Bar 유형의 리소스가 포함되어 있습니다. Foo.Dispose를 실행하는 동안 예외가 발생하면 이 DisposableClass에서 Bar 개체를 삭제할 수 없습니다.

Back to top

관리되는 리소스 정리

개체를 종료할 때 관리되는 리소스가 정리되지 않는다면 해당 리소스의 누수는 어떻게 방지할까요? 이러한 개체에 정리해야 할 리소스가 포함되어 있다면 IDisposable 패턴을 따라야 합니다. 개체의 종료자는 관리되는 리소스가 종료되었는지 알 수 없으므로 직접 관리되는 리소스를 정리할 수 없습니다. 그러나 이러한 유형의 개체 중 해제해야 할 리소스가 있는 개체에는 가비지 수집기에서 실행할 수 있는 종료자가 필요하며, 관리되는 리소스만 있는 개체에는 종료자가 필요하지 않습니다.

이에 대한 예를 살펴보겠습니다. 관리되는 컨트롤에서 브라우저 세션 간에 상태를 유지하는 데 사용되는 Cookie 클래스를 생각해 보십시오. 이러한 Cookie 클래스는 IsolatedStorage를 사용하여 구현할 수 있습니다. 그리고 IsolatedStorage 자체는 FileStream을 사용하여 구현되며, Win32 파일 핸들을 포함할 수 있습니다. 따라서 최종 클래스는 그림5의 코드와 같습니다.

IsolatedStorageFileStream은 관리되는 리소스이므로 컨트롤에서 Cookie 개체를 삭제하지 않으면 Cookie에 이 스트림을 정리하는 종료자가 없게 되지만 가비지 수집기가 스트림 개체를 수집하므로 이는 문제가 되지 않습니다. 또한 IsolatedStorageFileStream의 경우에도 관리되는 리소스만 포함하므로 종료자가 필요 없습니다. FileStream 개체에 자체적인 네이티브 리소스가 있어 종료자가 포함되므로 이것도 문제가 되지 않습니다. 가비지 수집기는 FileStream을 종료할 때 핸들을 닫습니다. 이 개체 그래프에서 루트 개체는 처리되지 않지만 모든 관련 클래스가 삭제 가능한 패턴을 따르므로 리소스 누수가 발생하지 않습니다.

Back to top

삭제 가능한 유형에서 파생

삭제 가능한 유형에서 새 유형이 파생될 때 파생한 유형에 새로운 리소스가 포함되지 않으면 특별히 조치를 취할 필요가 없습니다. 기본 유형의 IDisposable 구현에서 자동으로 리소스가 정리되므로 하위 클래스에서는 세부 사항을 신경 쓸 필요가 없기 때문입니다. 그러나 일반적으로는 정리해야 할 새 리소스가 포함된 하위 클래스가 있기 마련입니다. 이 경우 클래스에서 리소스를 해제하는 동시에, 기본 유형의 리소스도 해제되도록 해야 합니다. 이를 위해서는 그림6과 같이 정리 메서드를 다시 정의하고 리소스를 해제한 다음 기본 유형을 호출하여 리소스를 정리하도록 합니다.

필요한 작업은 상속된 버전의 공용 Dispose 메서드와 종료자가 모두 수행하므로 파생된 유형은 정리 메서드만 다시 정의하면 됩니다. 단, 기본 유형에는 네이티브 리소스가 없고 파생된 유형에는 네이티브 리소스가 포함되므로, 기본 유형에 종료자가 없는 경우에는 이 규칙이 적용되지 않습니다. 이 예외 상황에서는 파생된 유형에 종료자를 추가해야 합니다.

파생된 유형의 리소스는 기본 유형의 정리 메서드를 호출하기 전에 정리해야 합니다. 이렇게 하면 개체가 생성된 순서의 역순으로 삭제됩니다. 기본 유형의 정리 메서드를 먼저 호출하면 기본 유형이 정리 메서드를 완료하는 데 필요한 개체의 일부를 제거하게 될 수 있습니다.

Back to top

Dispose 및 보안

정리 코드를 작성할 때는 정리 코드가 실행되는 보안 컨텍스트가 미묘한 문제가 됩니다. 모든 정리 코드는 실행 스레드가 가장하는 ID에 관계없이 항상 실행할 수 있어야 합니다. 가비지 수집기는 전용 스레드에서 종료자를 실행하므로 삭제 가능한 개체를 인스턴스화한 스레드에서 활성화된 모든 가장은 종료자가 실행될 때 존재하지 않게 됩니다. 또한 개체를 생성한 후 Dispose를 호출할 때까지 해당 개체 유형을 사용하는 코드에서 스레드의 가장을 되돌리거나 완전히 다른 ID를 가장하지 않도록 할 방법이 없습니다.

가장을 잘못 처리하는 클래스의 대표적인 예로 Microsoft .NET Framework 2.0의 RSACryptoServiceProvider 클래스를 들 수 있습니다. RSACryptoServiceProvider 개체에서 작업에 사용할 임시 키를 만들면 개체의 종료자가 키를 삭제하려 시도합니다. 이 키는 키를 만든 스레드가 실행 시에 가장하는 사용자의 프로필에 저장되어 있습니다. 이때 스레드가 사용자를 가장하는 동안, 개체가 삭제되기 전에 RSACryptoServiceProvider 개체가 만들어지면 문제가 발생합니다. 이 경우 종료자는 개체 생성자가 아닌 다른 사용자의 컨텍스트에서 실행됩니다. 대개 종료자 스레드가 실행되는 동안 가장하는 사용자에게는 키가 저장된 사용자 프로필에 대한 액세스 권한이 없으므로 종료자가 키를 삭제하려 하면 예외가 발생합니다.

종료자는 별도의 스레드에서 실행되므로 Dispose 메서드에 포함된 호출 스택의 권한을 검사하는 것만으로는 이 문제를 해결할 수 없습니다. 개체의 종료자에서 정리 메서드를 호출할 때 호출 스택의 위쪽에는 다른 코드가 없으므로 코드 액세스 보안 검사는 의미가 없습니다. 뿐만 아니라 정리 코드에서는 예외가 발생할 수 있으므로 일반적으로 보안 요구 사항을 구현하지 않아야 합니다.

Back to top

SafeHandles

.NET Framework 2.0에는 리소스 관리를 지원하기 위해 SafeHandle 클래스가 새로 추가되었습니다. SafeHandle은 네이티브 리소스를 래핑하며, IntPtr을 사용하여 리소스를 점유함으로써 몇 가지 이점을 제공합니다. 이 기사에서는 SafeHandle에 대해 자세히 다루지 않지만 Brian Grunkemeyer의 BCL(기본 클래스 라이브러리) 팀 블로그에서 자세한 내용을 확인할 수 있습니다.

SafeHandle이 새로 추가되면서 SafeHandle에서 파생되지 않은 유형에 더 이상 원시 네이티브 리소스를 포함할 이유가 없다는 주장이 제기될 수 있습니다. 대신 개체에서 사용하는 각 네이티브 리소스에 사용되는 SafeHandle의 하위 클래스를 비롯한 관리되는 리소스만 개체에 포함하면 되기 때문입니다.

이 패턴을 따르면 몇 가지 이점을 얻을 수 있습니다. 우선 SafeHandle만 네이티브 리소스를 소유하므로 SafeHandle 이외의 클래스에 종료자를 사용할 필요가 없습니다. 다음으로, 각 SafeHandle이 네이티브 리소스를 하나씩만 소유하는 간단한 모델이 구성되므로, 관리되는 개체가 필요에 따라 다양한 SafeHandle을 만들 수 있습니다.

Back to top

결론

CLR의 가비지 수집기는 관리되는 힙에 할당된 메모리의 관리 부담을 덜어 주지만, 다른 유형의 리소스는 여전히 정리해 주어야 합니다. 관리되는 클래스는 IDisposable 인터페이스를 사용하여 가비지 수집기가 개체를 종료하기 전에 소비자가 중요한 리소스를 해제할 수 있도록 합니다. 삭제 가능한 패턴을 따르고 발생할 수 있는 문제에 주의를 기울인다면, 클래스에서 모든 리소스가 제대로 정리되도록 하고, 정리 코드를 Dispose 호출을 통해 직접 실행하거나 종료자 스레드를 통해 실행할 때 문제가 발생하지 않도록 할 수 있습니다.

http://msdn.microsoft.com/msdnmag/issues/07/07/CLRInsideOut/default.aspx?loc=ko

'알고리즘' 카테고리의 다른 글

Advanced C# 2. 인터페이스  (0) 2008.01.08
Advanced C# 1. UML  (0) 2008.01.08
C#에서 C API사용  (0) 2006.09.12
신재호의 SW 개발이야기]-아키텍처 중심의 개발(1)  (0) 2006.03.29
모듈러 디자인(Modular Design)-2부  (0) 2006.03.27
Posted by 퓨전마법사
,