비고
이 콘텐츠는 프레임워크 디자인 지침: 재사용 가능한 .NET 라이브러리에 대한 규칙, 관용구 및 패턴, 2판에서 Pearson Education, Inc.의 권한으로 다시 인쇄됩니다. 이 버전은 2008년에 출판되었으며, 이후 세 번째 에디션에서 완전히 수정되었습니다. 이 페이지의 일부 정보는 오래된 것일 수 있습니다.
모든 프로그램은 실행 과정에서 메모리, 시스템 핸들 또는 데이터베이스 연결과 같은 하나 이상의 시스템 리소스를 획득합니다. 개발자는 이러한 시스템 리소스를 사용할 때 해당 리소스를 획득하고 사용한 후에 릴리스해야 하므로 주의해야 합니다.
CLR은 자동 메모리 관리를 지원합니다. 관리되는 메모리(C# 연산 new
자를 사용하여 할당된 메모리)는 명시적으로 해제할 필요가 없습니다. 가비지 수집기(GC)에 의해 자동으로 해제됩니다. 이렇게 하면 개발자가 지루하고 어려운 메모리 해제 작업에서 벗어나 .NET Framework에서 제공하는 전례 없는 생산성의 주된 이유 중 하나가 되었습니다.
아쉽게도 관리되는 메모리는 여러 유형의 시스템 리소스 중 하나일 뿐입니다. 관리되는 메모리 이외의 리소스는 여전히 명시적으로 해제되어야 하며 관리되지 않는 리소스라고 합니다. GC는 관리되지 않는 리소스를 관리하도록 특별히 설계되지 않았으므로 관리되지 않는 리소스 관리에 대한 책임은 개발자의 손에 달려 있습니다.
CLR은 관리되지 않는 리소스를 해제하는 데 도움이 됩니다. System.Object 는 GC에서 개체의 메모리를 회수하기 전에 GC에서 호출하는 가상 메서드 Finalize (종료자라고도 함)를 선언하고 관리되지 않는 리소스를 해제하도록 재정의할 수 있습니다. 종료자를 재정의하는 형식을 종료 가능 형식이라고 합니다.
종료자는 일부 정리 시나리오에서 효과적이지만 다음과 같은 두 가지 중요한 단점이 있습니다.
종료자는 GC에서 개체가 컬렉션에 적합하다는 것을 감지할 때 호출됩니다. 이 문제는 리소스가 더 이상 필요하지 않은 후에 결정되지 않은 시간에 발생합니다. 개발자가 리소스를 해제할 수 있거나 해제하려는 경우와 종료자가 리소스를 실제로 릴리스하는 시간 사이의 지연은 많은 부족한 리소스(쉽게 소진될 수 있는 리소스)를 획득하는 프로그램이나 리소스를 계속 사용하기 위해 비용이 많이 드는 경우(예: 관리되지 않는 큰 메모리 버퍼)에서 허용되지 않을 수 있습니다.
CLR이 종료자를 호출해야 하는 경우 가비지 수집 사이클이 끝날 때까지 개체의 메모리 수집을 연기해야 합니다(종료자는 수집 간 간격 동안 실행됩니다). 즉, 개체의 메모리(및 개체가 참조하는 모든 개체)는 더 오랜 시간 동안 해제되지 않습니다.
관리되지 않는 리소스를 가능한 한 빨리 회수해야 하거나, 희소한 리소스를 다루어야 하거나, 종료의 GC 오버헤드 증가를 허용할 수 없는 매우 고성능의 시나리오에서는 종료자에만 의존하는 것이 적절하지 않을 수 있습니다.
프레임워크는 개발자가 필요하지 않은 즉시 관리되지 않는 리소스를 해제할 수 있는 수동 방법을 제공하기 위해 구현해야 하는 인터페이스를 제공합니다 System.IDisposable . 또한 개체가 수동으로 삭제되었으며 더 이상 종료할 필요가 없다는 것을 GC에 알릴 수 있는 메서드를 제공합니다 GC.SuppressFinalize . 이 경우 개체의 메모리를 이전에 회수할 수 있습니다. 인터페이스를 구현하는 형식을 IDisposable
일회용 형식이라고 합니다.
Dispose 패턴은 종료자 및 인터페이스의 사용 및 구현을 표준화하기 위한 것입니다 IDisposable
.
패턴의 주요 동기는 Finalize 및 Dispose 메서드의 구현 복잡성을 줄이는 것입니다. 복잡성은 메서드가 일부 코드 경로를 공유하지만 일부 코드 경로는 공유하지 않는다는 사실에서 비롯됩니다(차이점은 장 뒷부분에서 설명). 또한 결정적 리소스 관리를 위한 언어 지원의 진화와 관련된 패턴의 일부 요소에 대한 역사적 이유가 있습니다.
✓ 처분할 수 있는 형식의 인스턴스를 포함하는 형식에 대해 기본 Dispose 패턴을 구현하십시오. 기본 패턴에 대한 자세한 내용은 기본 Dispose 패턴 부문을 참조하세요.
형식이 다른 삭제 가능한 개체의 수명에 책임이 있다면, 개발자도 이를 삭제할 수 있는 방법이 필요합니다. 컨테이너의 Dispose
메서드를 사용하면 이 작업을 편리하게 수행할 수 있습니다.
✓ 기본 삭제 패턴을 구현하고 명시적으로 해제해야 하고 종료자가 없는 리소스를 보유하는 형식에 종료자를 제공합니다.
예를 들어 관리되지 않는 메모리 버퍼를 저장하는 형식에서 패턴을 구현해야 합니다. Finalizable Types 섹션에서는 종료자 구현과 관련된 지침을 설명합니다.
✓ 관리되지 않는 리소스 또는 일회용 개체를 보유하지 않지만 하위 형식이 있을 가능성이 있는 클래스에서 기본 Dispose 패턴을 구현하는 것이 좋습니다.
이 클래스의 좋은 예는 클래스입니다 System.IO.Stream . 리소스를 보유하지 않는 추상 기본 클래스이지만 대부분의 하위 클래스는 이를 수행하므로 이 패턴을 구현합니다.
기본 삭제 패턴
패턴의 기본 구현에는 System.IDisposable
인터페이스를 구현하고, Dispose(bool)
메서드와 선택적인 종료자 사이에서 리소스 정리 논리를 공유하는 Dispose
메서드를 선언하는 작업이 포함됩니다.
다음 예제에서는 기본 패턴의 간단한 구현을 보여 줍니다.
public class DisposableResourceHolder : IDisposable {
private SafeHandle resource; // handle to a resource
public DisposableResourceHolder() {
this.resource = ... // allocates the resource
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (disposing) {
if (resource!= null) resource.Dispose();
}
}
}
부울 매개 변수 disposing
는 메서드가 구현에서 호출되었는지 종료자에서 IDisposable.Dispose
호출되었는지 여부를 나타냅니다. 구현은 Dispose(bool)
다른 참조 개체(예: 이전 샘플의 리소스 필드)에 액세스하기 전에 매개 변수를 확인해야 합니다. 이러한 개체는 구현에서 IDisposable.Dispose
메서드가 호출될 때만 액세스해야 합니다(매개 변수가 true와 같을 때 disposing
). 종료자에서 메서드를 호출하는 경우, disposing
가 false일 때 다른 개체에 액세스해서는 안 됩니다. 그 이유는 개체가 예측할 수 없는 순서로 종료되므로 개체 또는 해당 종속성이 이미 종료되었을 수 있기 때문입니다.
또한 이 섹션은 아직 Dispose 패턴을 구현하지 않은 기본 클래스에 적용됩니다. 이미 패턴을 구현한 클래스를 상속하는 경우, 추가적인 리소스 정리를 위해 Dispose(bool)
메서드를 재정의하세요.
✓ 관리 되지 않는 리소스 해제와 관련된 모든 논리를 중앙 집중화하는 메서드를 선언 protected virtual void Dispose(bool disposing)
합니다.
이 메서드에서는 모든 리소스 정리가 수행되어야 합니다. 메서드는 종료자와 IDisposable.Dispose
메서드 모두에서 호출됩니다. 종료자 내부에서 호출되는 경우 매개 변수는 false입니다. 종료하는 동안 실행되는 코드가 다른 종료 가능한 개체에 액세스하지 않도록 하는 데 사용해야 합니다. 종료자 구현에 대한 자세한 내용은 다음 섹션에서 다룰 것입니다.
protected virtual void Dispose(bool disposing) {
if (disposing) {
if (resource!= null) resource.Dispose();
}
}
✓IDisposable
를 호출한 다음 Dispose(true)
를 호출하여 GC.SuppressFinalize(this)
인터페이스를 간단히 구현합니다.
SuppressFinalize
이 성공적으로 실행된 경우에만 Dispose(true)
을 호출해야 합니다.
public void Dispose(){
Dispose(true);
GC.SuppressFinalize(this);
}
X는 매개 변수가 없는 Dispose
메서드를 가상으로 만들지 않습니다.
메서드 Dispose(bool)
는 서브클래스로 재정의해야 하는 메서드입니다.
// bad design
public class DisposableResourceHolder : IDisposable {
public virtual void Dispose() { ... }
protected virtual void Dispose(bool disposing) { ... }
}
// good design
public class DisposableResourceHolder : IDisposable {
public void Dispose() { ... }
protected virtual void Dispose(bool disposing) { ... }
}
X 하지 마십시오Dispose
및 Dispose()
이외의 Dispose(bool)
메서드의 오버로드를 선언하지 마십시오.
Dispose
은 이 패턴을 명문화하고 구현자, 사용자 및 컴파일러 간의 혼동을 방지하는 데 도움이 되는 예약어로 간주되어야 합니다. 일부 언어는 특정 형식에서 이 패턴을 자동으로 구현하도록 선택할 수 있습니다.
✓ Dispose(bool)
메서드를 여러 번 호출하도록 허용하십시오. 메서드는 첫 번째 호출 후에 아무 작업도 수행하지 않도록 선택할 수 있습니다.
public class DisposableResourceHolder : IDisposable {
bool disposed = false;
protected virtual void Dispose(bool disposing) {
if (disposed) return;
// cleanup
...
disposed = true;
}
}
X 포함 프로세스가 누수, 일관되지 않은 공유 상태 등의 이유로 손상된 중요한 상황을 제외하고는 내부에서 Dispose(bool)
예외를 던지는 것을 피합니다.
사용자는 Dispose
호출이 예외를 발생시키지 않을 것으로 예상합니다.
Dispose
에서 예외가 발생할 수 있다면, 이후에 실행될 finally 블록의 정리 논리는 실행되지 않습니다. 이 문제를 해결하려면 사용자는 최종 블록 내에서 Dispose
에 대한 모든 호출을 각각의 try 블록으로 감싸야 하므로, 결과적으로 매우 복잡한 정리 처리기가 생깁니다.
Dispose(bool disposing)
메서드를 실행할 때, disposing 값이 false이면 예외를 throw하지 마십시오. 이렇게 하면 종료자 컨텍스트 내에서 실행되는 경우 프로세스가 종료됩니다.
✓ 개체가 ObjectDisposedException 삭제된 후 사용할 수 없는 멤버에서 throw합니다.
public class DisposableResourceHolder : IDisposable {
bool disposed = false;
SafeHandle resource; // handle to a resource
public void DoSomething() {
if (disposed) throw new ObjectDisposedException(...);
// now call some native methods using the resource
...
}
protected virtual void Dispose(bool disposing) {
if (disposed) return;
// cleanup
...
disposed = true;
}
}
✓ close가 영역의 표준 용어인 경우, 이외에 방법을 Close()
제공하는 Dispose()
.
이렇게 할 때, Close
구현을 Dispose
와 동일하게 만들고 IDisposable.Dispose
메서드를 명시적으로 구현하도록 고려하는 것이 중요합니다.
public class Stream : IDisposable {
IDisposable.Dispose() {
Close();
}
public void Close() {
Dispose(true);
GC.SuppressFinalize(this);
}
}
종료 가능한 타입
종료 가능 형식은 종료자를 재정의하고 메서드에서 종료 코드 경로를 Dispose(bool)
제공하여 기본 Dispose 패턴을 확장하는 형식입니다.
종료자는 실행 중에 시스템 상태에 대한 특정(일반적으로 유효한) 가정을 할 수 없기 때문에 올바르게 구현하기가 매우 어렵습니다. 다음 지침은 신중하게 고려해야 합니다.
일부 지침은 Finalize
메서드뿐만 아니라 최종 처리기에서 호출되는 모든 코드에 적용됩니다. 이전에 정의된 기본 삭제 패턴의 경우, Dispose(bool disposing)
매개 변수가 false일 때 disposing
내부에서 실행되는 논리를 의미합니다.
기본 클래스가 이미 종료할 수 있고 기본 Dispose 패턴을 구현하는 경우 Finalize
을(를) 다시 재정의해서는 안 됩니다. 대신 Dispose(bool)
메서드를 재정의하여 추가 리소스 정리 논리를 제공해야 합니다.
다음 코드는 종료 가능한 형식의 예제를 보여 주는 코드입니다.
public class ComplexResourceHolder : IDisposable {
private IntPtr buffer; // unmanaged memory buffer
private SafeHandle resource; // disposable handle to a resource
public ComplexResourceHolder() {
this.buffer = ... // allocates memory
this.resource = ... // allocates the resource
}
protected virtual void Dispose(bool disposing) {
ReleaseBuffer(buffer); // release unmanaged memory
if (disposing) { // release other disposable objects
if (resource!= null) resource.Dispose();
}
}
~ComplexResourceHolder() {
Dispose(false);
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
}
X 형식을 finalizable하게 만들지 마십시오.
종료자가 필요하다고 생각하는 경우에 대해 신중하게 고려하십시오. 성능 및 코드 복잡성 관점에서 종료자가 있는 인스턴스와 관련된 실제 비용이 있습니다. 가능한 경우 관리되지 않는 리소스를 캡슐화하는 것과 같은 SafeHandle 리소스 래퍼를 사용하는 것이 좋습니다. 이 경우 래퍼가 자체 리소스 정리를 담당하기 때문에 종료자가 불필요해집니다.
X는 값 형식을 종료할 수 없도록 합니다.
참조 형식만 실제로 CLR에 의해 종료되므로 값 형식에 종료자를 배치하려는 시도는 무시됩니다. C# 및 C++ 컴파일러는 이 규칙을 적용합니다.
✓ 형식이 자체 종료자가 없는 관리되지 않는 리소스를 해제할 책임이 있는 경우 형식을 종료할 수 있도록 합니다.
종료자를 구현할 때 Dispose(false)
를 호출하고 모든 리소스 정리 논리를 Dispose(bool disposing)
메서드에 배치하면 됩니다.
public class ComplexResourceHolder : IDisposable {
~ComplexResourceHolder() {
Dispose(false);
}
protected virtual void Dispose(bool disposing) {
...
}
}
✓ 완료 가능한 모든 형식에서 기본 삭제 패턴을 구현합니다.
이렇게 하면 형식의 사용자가 종료자가 담당하는 동일한 리소스의 결정적 정리를 명시적으로 수행할 수 있는 수단을 제공합니다.
X는 종료자 코드 경로에서 종료 가능한 개체에 액세스하지 않습니다. 이미 종료될 위험이 매우 크기 때문입니다.
예를 들어 종료 가능한 다른 개체 B에 대한 참조가 있는 종료 가능한 개체 A는 A의 종료자에서 B를 안정적으로 사용할 수 없거나 그 반대의 경우도 마찬가지입니다. 종료자는 임의 순서로 호출됩니다(중요한 종료를 위한 약한 순서 보장에 미치지 못 함).
또한 정적 변수에 저장된 개체는 애플리케이션 도메인을 언로드하는 동안 또는 프로세스를 종료하는 동안 특정 지점에서 수집됩니다. true를 반환하는 경우 종료 가능한 개체를 참조하는 정적 변수에 액세스하거나 정적 변수에 저장된 값을 사용할 수 있는 정적 메서드를 호출하는 것은 Environment.HasShutdownStarted 안전하지 않을 수 있습니다.
✓ 메서드 Finalize
를 보호하십시오.
컴파일러가 이 지침을 적용하는 데 도움이 되므로 C#, C++및 VB.NET 개발자는 이에 대해 걱정할 필요가 없습니다.
X는 시스템 위험 오류를 제외하고 예외가 종료자 논리에서 이스케이프되도록 허용하지 않습니다.
종료자에서 예외가 발생하면 CLR은 전체 프로세스를 종료합니다 (.NET Framework 버전 2.0부터), 이로 인해 다른 종료자가 실행되지 않고 리소스를 제어된 방식으로 해제하는 것을 방지합니다.
✓ 종료자가 강제 애플리케이션 도메인 언로드 및 스레드 중단에도 불구하고 반드시 실행되어야 하는 상황에 대해 중요한 종료 가능 개체(포함된 CriticalFinalizerObject형식 계층 구조가 있는 형식)를 만들고 사용하는 것이 좋습니다.
Microsoft Corporation의 일부 저작권 2005, 2009. 모든 권리 보유.
프레임워크 디자인 지침에서 Pearson Education, Inc.의 권한으로 재인쇄 : 재사용 가능한 .NET 라이브러리에 대한 규칙, 관용구 및 패턴, Krzysztof Cwalina 및 Brad Abrams의 제2판, Microsoft Windows 개발 시리즈의 일환으로 Addison-Wesley Professional이 2008년 10월 22일 출판했습니다.