logo MSJO.kr

Free the memory used by a collection

2025-01-24
MsJ
 

C# .NET은 Managed Code이다. .NET의 구성요소인 CLR(Common Language Runtime)에서 실행되는 코드 유형이다. 상대적으로 Unmanaged Code는 CLR의 개입 없이 운영체제나 하드웨어에서 직접 실행되는 코드 유형이다. C#보다 머신 수준에 더 가까운 C, C++ 와 같은 언어로 작성된다.1 Managed Code인 C#은 런타임 환경에서 자동으로 메모리 관리(GC, Garbage Collection)를 관리한다.

IDisposable을 구현하는 개체를 사용할 때 using StreamReader와 같이 사용하면 함수의 블록이 끝날 때 자동으로 메모리에서 개체를 메모리에서 해제한다.2 간혹 개발자 중에 직접 메모리를 해제하고자 할 때 아래와 같이 코드를 작성하곤 하는 데 이러한 방법은 GC를 오해한 까닭에서 나온 결과이다.3

개선 전 코드
public static void ClearMemory<T>(this List<T> list)
{
    int id = GC.GetGeneration(list);
    list.Clear();
    GC.Collect(id, GCCollectionMode.Forced);
}

GC.Collect()를 호출하면 가비지 컬렉션이 실행될 가능성을 높이지만 Unmanaged 언어와 같이 바로 메모리가 반환되는 것을 보장하지 않는다. 즉, 힌트를 준 정도의 역할만 할 뿐이고 특히 GC를 자주 호출하면 CPU 시간을 소모하는 작업이기 때문에 오히려 성능에 악영향을 미칠 수 있다.

아래의 예제는 권장하는 코드와 함께 공부 차원에서 직접 작성한 몇 가지 클래스이다.

권장하는 코드
public static void ClearAll<T>(this List<T> list)  
{  
    list.Clear();  
    list.Capacity = 0;  
}

위에서처럼 Clear()를 사용하여 리스트가 차지하고 있던 메모리 공간을 가비지 컬렉션의 대상이 되게 한다. 그리고 Capacity = 0을 적용하여 리스트가 내부적으로 가지고 있는 배열의 메모리도 가비지 컬렉션의 대상이 되게 한다. 닷넷에서는 이렇게 하여 자동으로 GC가 일어나도록 힌트를 줄 수 있다. 나머지는 .NET이 알아서 자동 처리한다.

확장 함수 구현

Disposable한 컬렉션은 DisposeAll()을 사용하고 그렇지 않은 개체는 ClearAll()을 사용한다.

public static class DisposableExtensions  
{  
    public static void DisposeAll<T>(this List<T> list) where T : IDisposable  
    {  
        list.ForIn(x => x.Dispose());  
    }  
  
    public static void DisposeAll<T>(this T[] array) where T : IDisposable  
    {  
        array.ForIn(x => x.Dispose());  
    }  
  
    public static void DisposeAll<T>(this IEnumerable<T> collection) where T : IDisposable  
    {  
        collection.ForIn(x => x.Dispose());  
    }  
  
    public static void ClearAll<T>(this List<T> list)  
    {  
        list.Clear();  
        list.Capacity = 0;  
    }  
  
    private static void ForIn<T>(this IEnumerable<T>? seq, Action<T> act) where T : IDisposable  
    {  
        if (seq == null)  
        {  
            return;  
        }  
  
        List<Exception> exceptions = [];  
  
        foreach (T item in seq)  
        {  
            try  
            {  
                act(item);  
            }  
            catch (Exception ex)  
            {  
                exceptions.Add(ex);  
            }  
        }  
  
        if (exceptions.Count > 0)  
        {  
            throw new AggregateException($"Disposing Item ERROR : {exceptions}");  
        }  
    }  
}
IDisposable 패턴 적용
public sealed class DisposeList<T> : List<T>, IDisposable  
{  
    private bool mDisposedValue;  
  
    private void Dispose(bool disposing)  
    {  
        if (mDisposedValue)  
        {  
            return;  
        }  
  
        if (disposing)  
        {  
            foreach (IDisposable item in this.OfType<IDisposable>())  
            {  
                try  
                {  
                    item.Dispose();  
                }  
                catch (Exception ex)  
                {  
                    LogHelper.Logger.Error($"Disposing Item ERROR : {ex.Message}");  
                }  
            }  
  
            Clear();  
            Capacity = 0;  
        }  
  
        mDisposedValue = true;  
    }  
  
    ~DisposeList() => Dispose(false);  
  
    public void Dispose()  
    {  
        Dispose(disposing: true);  
        GC.SuppressFinalize(this);  
    }  
}
public sealed class DisposeArray<T>(uint initialLength = 0) : IDisposable  
{  
    private T[]? mArray = initialLength > 0 ? new T[initialLength] : null;  
    private bool mDisposedValue;  
  
    public T this[int index]  
    {  
        get  
        {  
            ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray<T>));  
  
            if (mArray == null || index < 0 || index >= Count)  
            {  
                throw new IndexOutOfRangeException();  
            }  
  
            return mArray[index];  
        }  
        set  
        {  
            ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray<T>));  
  
            if (index < 0)  
            {  
                throw new IndexOutOfRangeException();  
            }  
  
            if (mArray == null)  
            {  
                mArray = new T[Math.Max(index + 1, 4)];  
            }  
            else if (index >= mArray.Length)  
            {  
                Array.Resize(ref mArray, Math.Max(index + 1, mArray.Length * 2));  
            }  
  
            mArray[index] = value;  
            Count = Math.Max(Count, index + 1);  
        }  
    }  
  
    public int Count { get; private set; }  
  
    public int Capacity  
    {  
        get => mArray?.Length ?? 0;  
    }  
  
    public T[] ToArray()  
    {  
        ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray<T>));  
  
        if (mArray == null || Count == 0)  
        {  
            return [];  
        }  
  
        T[] newArray = new T[Count];  
        Array.Copy(mArray, newArray, Count);  
        return newArray;  
    }  
  
    public void Clear()  
    {  
        ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray<T>));  
  
        if (mArray != null)  
        {  
            Array.Clear(mArray, 0, mArray.Length);  
        }  
  
        Count = 0;  
    }  
  
    private void Dispose(bool disposing)  
    {  
        if (mDisposedValue) return;  
  
        if (disposing)  
        {  
            if (typeof(IDisposable).IsAssignableFrom(typeof(T)) && mArray != null)  
            {  
                List<Exception> exceptions = [];  
  
                foreach (IDisposable item in mArray.OfType<IDisposable>())  
                {  
                    try  
                    {  
                        item.Dispose();  
                    }  
                    catch (Exception ex)  
                    {  
                        exceptions.Add(ex);  
                    }  
                }  
  
                if (exceptions.Count != 0)  
                {  
                    throw new AggregateException($"Disposing Item ERROR : {exceptions}");  
                }  
            }  
            else  
            {  
                mArray = null;  
            }  
        }  
  
        mDisposedValue = true;  
    }  
  
    ~DisposeArray() => Dispose(false);  
  
    public void Dispose()  
    {  
        Dispose(true);  
        GC.SuppressFinalize(this);  
    }  
}
// 사용 예제
using DisposeList<string> comList = [];  
using DisposeArray<string> comArray = new(10); //완벽하지 않지만 동적 배열 가능

위의 클래스는 ‘확장 함수 구현’에서 보여준 것을 IDisposable 패턴을 적용하여 자동으로 처리하도록 구현하였다. 굳이 이렇게 사용할 필요는 없고 Clear(), Capacity = 0으로 충분하며, 당연하지만 StreamReader, Network의 Connection과 같이 IDisposable이 적용된 개체는 using을 사용해야 한다.

  • 참고로 위에서 직접 작성한 소스를 deepseek에 질문했더니 아래와 같은 답변을 해주었다. 비교해서 학습하는 데 도움이 되길 바라면서 소스코드를 아래에 옮겨본다. 옮기는 과정에서 약간의 소스 추가(IEnumerable<T>,GetEnumerator 관련)및 정리는 하였다.
// 사용 예제
// List
using DisposeList<string> comList = new();
using DisposeList<string> comList = [];  
// Array
using DisposeArray<string> comList = new(10);  
using DisposeArray<string> comList = new();
public sealed class DisposeList<T> : IDisposable, IEnumerable<T>  
{  
    private readonly List<T> mList = [];  
    private bool mDisposedValue;  
  
    public T this[int index]  
    {  
        get => mList[index];  
        set => mList[index] = value;  
    }  
  
    public int Count  
    {  
        get => mList.Count;  
    }  
  
    public int Capacity  
    {  
        get => mList.Capacity;  
    }  
  
    public void Add(T item) => mList.Add(item);  
  
    public bool Remove(T item) => mList.Remove(item);  
  
    public void Clear() => mList.Clear();

    public IEnumerator<T> GetEnumerator() => mList.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
  
    private void Dispose(bool disposing)  
    {  
        if (mDisposedValue) return;  
  
        if (disposing)  
        {  
            List<Exception> exceptions = [];  
  
            foreach (T item in mList)  
            {  
                if (item is not IDisposable disposable)  
                {  
                    continue;  
                }  
  
                try  
                {  
                    disposable.Dispose();  
                }  
                catch (Exception ex)  
                {  
                    exceptions.Add(ex);  
                }  
            }  
  
            if (exceptions.Count > 0)  
            {  
                throw new AggregateException("DisposeList disposal failed", exceptions);  
            }  
  
            mList.Clear();  
        }  
  
        mDisposedValue = true;  
    }  
  
    ~DisposeList() => Dispose(false);  
  
    public void Dispose()  
    {  
        Dispose(true);  
        GC.SuppressFinalize(this);  
    }  
}
public sealed class DisposeArray<T> : IDisposable  
{  
    private T[]? mArray;  
    private bool mDisposedValue;  
  
    public DisposeArray(uint initialLength = 0)  
    {  
        if (initialLength > 0) mArray = new T[initialLength];  
    }  
  
    public T this[int index]  
    {  
        get  
        {  
            ObjectDisposedException.ThrowIf(mDisposedValue, this);  
  
            if (mArray == null || index < 0 || index >= Count)  
            {  
                throw new IndexOutOfRangeException();  
            }  
  
            return mArray[index];  
        }  
        set  
        {  
            ObjectDisposedException.ThrowIf(mDisposedValue, this);  
  
            if (index < 0)  
            {  
                throw new IndexOutOfRangeException();  
            }  
  
            if (mArray == null)  
            {  
                mArray = new T[Math.Max(index + 1, 4)];  
            }  
            else if (index >= mArray.Length)  
            {  
                Array.Resize(ref mArray, Math.Max(index + 1, mArray.Length * 2));  
            }  
  
            if (mArray[index] is IDisposable disposable)  
            {  
                disposable.Dispose();  
            }  
  
            mArray[index] = value;  
            Count = Math.Max(Count, index + 1);  
        }  
    }  
  
    public int Count { get; private set; }  
  
    public int Capacity  
    {  
        get => mArray?.Length ?? 0;  
    }  
  
    public T[] ToArray()  
    {  
        ObjectDisposedException.ThrowIf(mDisposedValue, this);  
        return mArray == null || Count == 0 ? [] : mArray[..Count];  
    }  
  
    private void Dispose(bool disposing)  
    {  
        if (mDisposedValue) return;  
  
        if (disposing && mArray != null)  
        {  
            List<Exception> exceptions = [];  
  
            foreach (T item in mArray)  
            {  
                if (item is not IDisposable disposable)  
                {  
                    continue;  
                }  
  
                try  
                {  
                    disposable.Dispose();  
                }  
                catch (Exception ex)  
                {  
                    exceptions.Add(ex);  
                }  
            }  
  
            if (exceptions.Count > 0)  
            {  
                throw new AggregateException(exceptions);  
            }  
  
            mArray = null;  
        }  
  
        mDisposedValue = true;  
    }  
  
    ~DisposeArray() => Dispose(false);  
  
    public void Dispose()  
    {  
        Dispose(true);  
        GC.SuppressFinalize(this);  
    }  
}
Reference
  1. c-sharpcorner.com, “Managed vs. Unmanaged Code in .NET”
  2. C++에서 smart pointer를 사용하고, Rust 언어에서는 언어 수준 자체의 기능이다.
  3. stackoverflow.com, “How free memory used by a large list in C#?, Answer:Oswaldo Junior”

Prεv(Θld)  
Content
Search     RSS Feed     BY-NC-ND