IDisposable 인터페이스
https://learn.microsoft.com/ko-kr/dotnet/api/system.idisposable?view=net-7.0
namespace System
{
public interface IDisposable
{
void Dispose();
}
}
- C#의 using문은 IDisposable을 구현하는 개체에 대해
Dispose 메서드를 try/finally 블록을 이용해서 호출하는 코드를 단축 표기
// 컴파일러는 이 코드를
using (FileStream fs = new FileStream("myfile.txt", FileMode.Open))
{
// ...
}
// 이렇게 바꾸어서 컴파일 한다
FileStream fs = new FileStream("myfile.txt", FileMode.Open);
try
{
}
finally
{
if(fs != null)
{
((IDisposable)fs).Dispose();
}
}
- 예외가 발생하거나 기타 이유로 try 블록을 벗어날 때에도 반드시 Dispose 메서드가 호출됨
Dispose
- 특별한 표준안이나 명세는 없으나 사실상의 표준이 존재
1. 처분된 객체는 다시 살릴 수 없음
* 처분된 객체의 메서드(Dispose 제외), 속성을 호출하면 ObjectDisposedExcetion 예외 발생
2. 한 객체에 대해 Dispose를 여러번 호출 가능
3. IDispose X가 IDispose Y를 소유한 경우, X의 Dispose는 Y의 Dispose를 자동으로 호출
Close
- 역시 특별한 표준안은 없으나 아래 둘 중 하나의 기능을 수행함
1. Dispose와 정확히 동일한 기능
2. Dispose 기능의 일부
Stop
- Dispose와 비슷하지만 Start로 작동을 다시 시작시킬 수 있음
Dispose를 호출해야 할 때
- 거의 모든 경우에서 의심스럽다면 사용해서 처분하는 것이 안전함
- 가끔 처분하지 말아야 할 상황
1. 현재 코드가 객체를 소유하지 않을 때. 정적 필드나 속성을 통해서 공유 객체를 얻은 경우
ex) Brushes.Blue, Font.FromHdc 등을 통해 얻은 객체들
2. 객체의 Dispose가 상황에 맞지 않는 작업을 수행 할 경우
ex) MemoryStream: 나중에 스트림을 읽거나 써야 할 경우
StreamReader, StreamWriter: 스트림을 계속 열어두고 싶을 때(Flush를 호출하여 비워줘야 함)
IDbConnection: 나중에 Open으로 연결을 다시 열고 싶을 때(Close를 호출하여 닫아줘야 함)
DataContext: 게으르게 평가되는 질의가 문맥에 연결되어 있을 가능성이 있을 경우
3. 객체의 설계 차원에서 Dispose가 꼭 필요한 것이 아닌 경우, 객체를 처분하려면 프로그램이 쓸대없이 복잡해질 때
* 기반 클래스가 처분 가능이라서 처분 가능 형식이 된 것일 뿐, 본질적인 마무리 작업을 하지 않음
ex) WebClient, StringReader, StringWriter, BackgroundWorker 등
명시적 선택 기반 처분
- 비본질적인 작업은 옵션으로 본질적인 작업은 항상 진행 가능하도록 설정
public sealed class HouseManager : IDisposable
{
public readonly bool checkMailOnDispose;
public HouseManager(bool checkMailOnDispose)
{
this.checkMailOnDispose = checkMailOnDispose;
}
public void Dispose()
{
if (checkMailOnDispose)
{
CheckTheMail();
}
LockTheHouse();
}
void CheckTheMail()
{
// ...
}
void LockTheHouse()
{
// ...
}
}
- 이렇게하면 소비자는 고민 없이 항상 Dispose를 호출 할 수 있음
ex) DeflateStream - public DeflateStream(Stream stream, CompressionLevel compressionLevel, bool leaveOpen);
- 이 패턴을 따르지 않은 예(4.5 이전)
* StreamReader, StreamWriter - Flush 사용
* CryptoStream - FlushFinalBlock 사용
처분 시 필드 비우기
- 일반적으로 Dispose 메서드에서 객체의 필드들을 비울 필요는 없음
- 객체가 암호화 키 같은 고가의 비밀을 담는다면 처분 도중에 필드 내용을 비우는 것이 좋음
- 내부적으로 등록한 이벤트들의 구독을 해제하는 것은 좋은 습관
- 객체가 처분되었음을 뜻하는 필드를 두고 Dispose에서 그 필드를 설정하는 것이 좋음
(이렇게 하면 처분된 객체에 대해 멤버 함수를 호출하려 할 때 ObjectDisposedException을 던질 수 있음)
- 누구나 읽을 수 있는 자동 속성을 두는 것도 좋음 (ex) IsDisposed)
- 객체 자신의 이벤트 처리부들을 Dispose 메서드에서 비우는것도 좋음
종료자 (Finalizer; 종결 함수)
- 객체 종료자가 있다면 객체의 메모리가 해제되기 전에 종료자가 호출 됨
class Test
{
~Test()
{
// 종료자 코드
}
}
- 진행 순서
1. 종료자가 없는 객체는 즉시 삭제, 종료자가 있다면 특별한 대기열에 넣음
2. 대기열에 넣는 작업이 끝난다면 한 번의 쓰레기 수거 주기가 끝나는 것이고,
프로그램의 실행과 동시에 종료자 스레드를 만듬
3. 종료자 스레드는 프로그램과 병렬로 대기열에서 객체를 뽑아 종료자 메서드를 실행
* 종료자 메서드가 실행되지 않은 객체는 여전히 살아 있는 것으로 간주됨
4. 종료자 메서드가 실행되면 객체는 버림 받은 상태가 되며, 다음번 쓰래기 수거에서 삭제됨
- 종료자 사용에서 염두에 둬야 할 점
1. 종료자 때문에 메모리 할당과 쓰레기 수거가 느려짐 (종료자 존재 여부 및 실행 여부 추적 비용)
2. 종료자는 객체와 그 객체가 참조하는 모든 객체의 수명을 필요 이상으로 늘림 (실제 삭제는 다음번 GC 때이므로)
3. 여러 객체들에 대해 종료자가 호출 될 순서를 예측 할 수 없음
4. 종료자가 실행되는 시점을 프로그래머가 거의 제어 할 수 없음
5. 종료자 안에서 코드 실행이 차단되면 나머지 종료자들이 호출되지 못함
6. 응용 프로그램이 정상적으로 종료되지 않으면 종료자들이 호출되지 않을 수 있음
7. 객체를 생성하는 도중에 예외가 발생해도 그 객체의 종료자가 호출 될 수 있음
(객체의 필드들이 정확히 초기화 되지 않았다고 가정하는 것이 좋음)
- 종료자 구현시 따를 만한 지침
1. 종료자의 실행은 빠르게 끝나도록
2. 종료자 않에서 코드 실행이 차단되지 않도록 함
3. 종료자 안에서 다른 종료 가능 객체를 참조하지 않음
4. 예외 던지지 말기
종료자에서 Dispose 호출
- 흔히 쓰이는 패턴: 종료자 안에서 Dispose를 호출
* 자원 해제와 메모리 해제의 결합도가 올라감
1. 객체의 정리 작업이 그리 급하지 않을 때
2. Dispose를 호출 해서 정리 작업을 촉진하는 것이 꼭 필요해서보다는 일종의 최적화를 위한 것을 때 적합
3. 소비자가 Dispose 호출을 까먹는 경우의 대비책
* 단, 프로그래머가 나중에라도 버그를 고칠 수 있도록 Dispose 호출 누락 사실을 로그에 기록하는 것이 좋음
class Test : IDisposable
{
public void Dispose()
{
// 사용자가 직접 Dispose한 경우 true로 넘겨줌
Dispose(true);
// 추후 GC가 이 객체를 거둬 갈 때 종료자의 실행을 방지
// 꼭 필요하진 않지만 이렇게 넣어주면 성능이 향상 됨
GC.SuppressFinalize(this);
}
// 실제로 처분을 진행하는 메소드
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// 이 인스턴스가 소유한 객체들에 대해 Dispose를 호출
// 다른 종료 가능 객체들을 참조해도 괜찮음
}
// 이 객체가(객체만) 소유하고 있는 비관리 자원들을 해제
// 1. OS 자원들(ex: P/Invoke로 Win32 API를 호출해서 얻은)에 대한 모든 직접 참조 해제
// 2. 생성 시 만든 임시 파일을 삭제
//
// try/catch 블록을 감싸서 예외 처리를 철저히 해야하고,
// 가능하다면 간단하고 안정적인 방법으로 로그에 기록
}
~Test()
{
// 종료자에 의해서 Dispose가 호출되는 경우 false를 돌려 줌
Dispose(false);
}
}
'C#' 카테고리의 다른 글
[C#] Contract, Code Contracts(코드 계약) (0) | 2023.10.20 |
---|---|
[C#] Debug, Trace (0) | 2023.10.18 |
[C#] WeakReference (0) | 2023.10.14 |
[C#] GC, GCSettings, 쓰레기 수거(Garbege Collection) (0) | 2023.10.12 |
[C#] IStructuralEquatable, IStructuralComparable (0) | 2023.10.08 |
[C#] StringComparer (0) | 2023.10.06 |
[C#] IComparer<T>, IComparer, Comparer<T> (0) | 2023.10.04 |
[C#] IEqualityComparer<T>, IEqualityComparer, EqualityComparer (0) | 2023.10.02 |