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을 사용해야 한다.
Reference
- c-sharpcorner.com, “Managed vs. Unmanaged Code in .NET”
- C++에서 smart pointer를 사용하고, Rust 언어에서는 언어 수준 자체의 기능이다.
- stackoverflow.com, “How free memory used by a large list in C#?, Answer:Oswaldo Junior”