728x90

.NET Framework에서 새로운 클래스를 만들 때 여러가지 메모리 관리 디자인 패턴과 기법을 적용할 수 있지만, 한시적으로 사용해야 할 필요가 있는 자원들을 묶어서 관리할 때에는 IDisposable 패턴을 적극적으로 활용하는 것이 매우 유용합니다.

하지만 생각보다 IDisposable 패턴을 제대로 구현해서 사용하는 것은 쉽지 않으며 잘못 구현하기 쉽습니다.

이 아티클에서는 IDisposable 패턴을 구현하는 몇 가지 일반적인 전략들을 소개합니다. 잘못 설명된 부분이 있거나 보충이 필요한 부분은 댓글로 피드백을 자세히 남겨주시면 적극 반영하겠습니다.

IDisposable 인터페이스에 대한 이해

IDisposable 인터페이스가 제공하는 Dispose 메서드는 명시적이고 코드 작성자가 직접 호출할 수 있는 finalizer로, 이 메서드가 불리면 가비지 컬렉터에 의하여 나중에 호출되는 finalizer의 역할을 대체하도록 되어있습니다. 물론, 메모리 상에 할당된 메모리 블록의 해제까지 건너뛴다는 의미는 아닙니다.

그리고 무엇을 Dispose 메서드에서 제거해야 하는지 기준을 세운다면 객체에 대한 소유 권한을 정의하는 것이 필요합니다. 적어도 다음의 경우에는 확실히 Dispose 메서드 내에서 정리가 되어야 합니다.

  • 해당 객체를 Dispose 하게 되면 외부에서 더 이상 사용하는 것이 의미가 없는 객체 (예를 들어 클래스 내부에서 사용하던 파일 입출력 관련 객체, 비동기 작업을 위하여 만들어 놓은 스레드 관련 객체)
  • 외부에서 전달받은 객체이지만 객체의 생명 주기를 위탁하여 관리하도록 지정한 객체 (예를 들어 StreamReader나 StreamWriter가 객체 생성 시 인자로 Stream을 받는 사례)

가장 기본이 되는 IDisposable 구현 패턴

.NET 은 finalizer에 해당되는 멤버를 재정의할 수 있습니다. 하지만 finalizer가 언제 호출이 될 것인지 기약할 수 없으므로 이 finalizer를 대신하여 좀 더 이른 시기에 명시적으로 소멸자와 동등한 효과를 낼 수 있도록 만든 것이 바로 IDisposable.Dispose 메서드가 되겠습니다.

IDisposable 패턴을 처음 구현할 때에는 다음의 사항들이 핵심이 됩니다.

  • protected virtual void Dispose(bool disposing) 메서드를 추가합니다. sealed 클래스에 대해서는 private void Dispose(bool disposing)으로 바꾸어 정의합니다.
  • 객체가 dispose 처리가 이루어진 상태인지를 관리할 수 있는 boolean 필드를 하나 추가하고 이 필드는 기본값을 false로 설정합니다.
  • Dispose(bool disposing) 호출 시 다음의 로직을 구현합니다.
  • 만약 객체가 dispose 상태인 경우에는 함수를 종료합니다.
  • 객체가 dispose 상태임을 필드에 지정합니다. (true로 설정)
  • disposing 매개 변수의 상태와 관계없이 P/Invoke 등을 활용하여 메모리 할당을 받은 나머지 모든 리소스들에 대해 할당을 해제하는 코드를 추가합니다.
  • disposing 매개 변수가 true로 전달되는 경우는 명시적으로 Dispose 메서드를 호출한 경우이며, 이 때에 다른 모든 IDisposable을 구현하는 객체들을 Dispose 처리합니다.
  • IDisposable.Dispose 메서드 구현 시 Dispose(true)를 호출하고 finalizer 호출을 건너뛰기 위하여 GC.SuppressFinalize(this)를 호출합니다.
  • 소멸자에서는 Dispose(false)를 호출합니다.

다음은 코드 예시입니다.

public class DisposableSample : IDisposable
{
public DisposableSample()
{ }
    ~DisposableSample()
{
this.Dispose(false);
}

private bool disposed;
    public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
    protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
if (disposing)
{
// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
}
// .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다.
this.disposed = true;
}
}

IDisposable 객체의 컬렉션에 대한 소거

소켓, 스레드 풀, 혹은 커넥션 풀 같은 컬렉션을 객체 내부에 보관해야 하는 경우도 있습니다. 이러한 경우에도 컬렉션 내의 모든 IDisposable 객체에 대해서 정리를 하는 것이 필요합니다.

  • 컬 렉션 내의 모든 요소가 IDisposable 인터페이스를 구현하고 있다면 Dispose(bool disposing) 메서드에서 disposing이 true일 때 정리를 하면 됩니다. 그렇지 않은 경우, disposing 매개 변수의 상태에 무관하게 정리합니다.
  • Dispose 작업 도중 새로운 항목이 추가되는 것을 방지하기 위하여 disposed 필드를 확인하도록 하는 코드를 추가해야 할 수 있습니다.
  • 컬렉션 내의 모든 요소를 배열에 복사하여 Immutable Collection으로 변환한 다음 하나씩 방문하여 Dispose를 진행하고, 최종적으로 컬렉션의 요소들을 모두 제거합니다.

다음은 코드 예시입니다.

public class DisposableSample : IDisposable
{
public DisposableSample()
{
this.items = new List<IDisposable>();
}
    ~DisposableSample()
{
this.Dispose(false);
}

private bool disposed;
private List<IDipsosable> items;
    public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
    protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
if (disposing)
{
// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
IDisposable[] targetList = new IDisposable[this.items.Count];
this.items.CopyTo(targetList);
foreach (IDisposable eachItem in targetList)
{
eachItem.Dispose();
}
this.items.Clear();
}
// .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다.
this.disposed = true;
}
}

Dispose 메서드 내의 예외 처리

만약 Dispose 메서드를 실행하는 도중에 예외가 발생한다면, CA1065의 지침에 따라 명시적인 Dispose 호출이었든 아니었든 예외를 전파하지 않도록 처리하는 것이 필요합니다. 다만 명시적으로 Dispose를 호출하면서 예외가 발생했다면 예외를 전파하지 않는 대신 적절한 예외 처리는 필요합니다.

이 부분에 대한 자세한 내용은 https://msdn.microsoft.com/ko-kr/library/bb386039.aspx 페이지의 내용을 참고하시면 도움이 될 것입니다.

컬렉션 내의 모든 요소들을 Dispose 하는 코드를 조금 더 보강하면 다음과 같이 고쳐쓸 수 있겠습니다.

public class DisposableSample : IDisposable
{
public DisposableSample()
{
this.items = new List<IDisposable>();
}
    ~DisposableSample()
{
this.Dispose(false);
}
    private bool disposed;
private List<IDisposable> items;
    public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
    protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
try
{
if (disposing)
{
// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
IDisposable[] targetList = new IDisposable[this.items.Count];
this.items.CopyTo(targetList);
foreach (IDisposable eachItem in targetList)
{
try { eachItem.Dispose(); }
catch (Exception ex) { /* 예외 처리를 수행합니다. */ }
finally { /* 정리 작업을 수행합니다. */ }
}
this.items.Clear();
}
try { /* .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다. */ }
catch { /* 예외 처리를 수행합니다. */ }
finally
{
/* 정리 작업을 수행합니다. */
this.disposed = true;
}
}
finally { /* 정리 작업을 수행합니다. */ }
}
}

소유하고 있는 객체들에 대한 Dispose 또는 정리 작업들을 각각 try, catch, finally 블록안에 두어 예외가 발생하면 적절한 예외 처리를 할 수 있게 하고, Dispose 메서드 전체에 대해서 try, finally 블록 안에 두어 예외가 전파되지 않도록 하였습니다.

결론

.NET Framework 기반의 응용프로그램이 안정적으로 장시간 실행될 수 있게 만들어야 할 때 고려해야 할 요소들 가운데에서 가장 비중있게 다루어야 할 부분이 바로 메모리 관리입니다. IDisposable 인터페이스를 통한 명시적인 finalizer 호출은 적절하게 활용하면 응용프로그램의 메모리 관리를 단순하게 만드는데 큰 도움을 줍니다.


출처 : https://medium.com/rkttu/idisposable-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%98%AC%EB%B0%94%EB%A5%B8-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-4fa0fcf0e67a

728x90

'Basic Programming > C#' 카테고리의 다른 글

C# - 작업 스케줄러에 등록하기  (0) 2021.12.10
C# - Steam Game 실행하기  (0) 2020.07.21
C# - WeakReference  (0) 2018.10.11
C# - 공부하기 좋은 사이트  (0) 2018.09.05
C# - Attibute  (0) 2018.07.09

+ Recent posts