삭제 패턴

참고 항목

이 콘텐츠는 Pearson Education, Inc.의 허가를 받아 프레임워크 디자인 지침: 재사용 가능한 .NET 라이브러리에 대한 규칙, 관용어 및 패턴, 2판에서 재인쇄되었습니다. 이 버전은 2008년에 출판되었으며 이후 3판에서 완전히 개정되었습니다. 이 페이지의 정보 중 일부는 최신 정보가 아닐 수 있습니다.

모든 프로그램은 실행 과정에서 메모리, 시스템 핸들 또는 데이터베이스 연결과 같은 하나 이상의 시스템 리소스를 획득합니다. 개발자는 이러한 시스템 리소스를 사용할 때 주의를 기울여야 합니다. 이러한 리소스를 획득 및 사용한 후에는 해제해야 하기 때문입니다.

CLR은 자동 메모리 관리를 지원합니다. 관리되는 메모리(C# 연산자 new를 사용하여 할당된 메모리)는 명시적으로 해제할 필요가 없습니다. GC(가비지 수집기)에 의해 자동으로 해제됩니다. 이렇게 하면 개발자가 지루하고 까다로운 메모리 해제 작업에서 벗어나 .NET Framework가 제공하는 전례 없는 생산성의 주된 이유 중 하나가 되었습니다.

아쉽게도 관리되는 메모리는 여러 유형의 시스템 리소스 중 하나일 뿐입니다. 관리되는 메모리 이외의 리소스는 여전히 명시적으로 해제해야 하며 이를 관리되지 않는 리소스라고 합니다. GC는 관리되지 않는 리소스를 관리하도록 설계되지 않았으므로 관리되지 않는 리소스를 관리할 책임은 개발자에게 있습니다.

CLR은 관리되지 않는 리소스를 해제하는 데 도움이 됩니다. System.Object는 GC가 개체의 메모리를 회수하기 전에 호출하는 가상 메서드 Finalize(종료자라고도 함)를 선언하고 관리되지 않는 리소스를 해제하도록 재정의할 수 있습니다. 종료자를 재정의하는 형식을 종료 가능 형식이라고 합니다.

종료자는 일부 정리 시나리오에서 효과적이지만 다음과 같은 두 가지 중대한 단점이 있습니다.

  • 종료자는 GC가 수집에 적합한 개체를 감지할 때 호출됩니다. 이는 리소스가 더 이상 필요하지 않은 시점으로부터 일정 기간 후에 발생합니다. 개발자가 리소스를 해제할 수 있거나 해제하려는 시점과 종료자가 리소스를 실제로 해제하는 시간 사이의 지연은 많은 희소한 리소스(쉽게 소진될 수 있는 리소스)를 획득하는 프로그램에서 또는 리소스를 계속 사용하기 위해 비용이 많이 드는 경우(예: 대규모의 관리되지 않는 메모리 버퍼)에는 허용되지 않을 수 있습니다.

  • CLR이 종료자를 호출해야 하는 경우 다음 가비지 수집 라운드(종료자가 수집 간에 실행됨)까지 개체의 메모리 회수를 연기해야 합니다. 즉, 개체의 메모리(및 참조하는 모든 개체)가 더 오랜 시간 동안 해제되지 않습니다.

따라서 관리되지 않는 리소스를 최대한 빨리 회수하거나, 희소한 리소스를 해결하거나, 종료에 GC 오버헤드를 추가할 수 없는 고성능 시나리오에서는 종료자에만 의존하는 것이 적절하지 않을 수 있습니다.

Framework는 개발자가 필요하지 않은 즉시 관리되지 않는 리소스를 수동으로 해제할 수 있는 방법을 제공하기 위해 구현해야 하는 System.IDisposable 인터페이스를 제공합니다. 또한 개체가 수동으로 삭제되어 더 이상 종료할 필요가 없음을 GC에 알릴 수 있는 GC.SuppressFinalize 메서드를 제공합니다. 그러면 개체의 메모리를 더 빨리 회수할 수 있습니다. IDisposable 인터페이스를 구현하는 형식을 삭제 가능 형식이라고 합니다.

Dispose 패턴은 종료자 및 IDisposable 인터페이스의 사용 및 구현을 표준화하기 위한 것입니다.

이 패턴의 주요 동기는 FinalizeDispose 메서드의 구현 복잡성을 줄이는 것입니다. 복잡성은 메서드가 일부 코드 경로만 공유하고 모든 코드 경로를 공유하지는 않는다는 데서 비롯됩니다(차이점은 이 장의 뒷부분에서 설명). 또한 패턴의 일부 요소에서 결정론적 리소스 관리에 대한 언어 지원이 진화한 역사적 이유가 있습니다.

삭제 가능 형식의 인스턴스를 포함하는 형식에 대해 기본 삭제 패턴을 구현하세요. 기본 패턴에 대한 자세한 내용은 기본 Dispose 패턴 섹션을 참조하세요.

형식이 다른 삭제 가능한 개체의 수명을 담당하는 경우 개발자는 이러한 개체를 삭제할 방법도 필요합니다. 컨테이너의 Dispose 메서드를 사용하는 것이 이 작업을 가능하게 하는 편리한 방법입니다.

기본 Dispose 패턴을 구현하고 명시적으로 해제해야 하는 리소스를 보유하고 종료자가 없는 형식에 종료자를 제공하세요.

예를 들어 관리되지 않는 메모리 버퍼를 저장하는 형식에서 이 패턴을 구현해야 합니다. 종료 가능 형식 섹션에서는 종료자 구현과 관련된 지침을 설명합니다.

자체에서 관리되지 않는 리소스 또는 삭제 가능한 개체를 보유하지 않지만 하위 형식이 있을 가능성이 있는 클래스에서 기본 삭제 패턴을 구현하는 것이 좋습니다.

System.IO.Stream 클래스가 좋은 예입니다. 이 추상 기본 클래스는 아무 리소스도 보유하지 않지만 대부분의 하위 클래스는 리소스를 보유하기 때문에 이 패턴을 구현합니다.

기본 Dispose 패턴

이 패턴의 기본 구현에는 System.IDisposable 인터페이스를 구현하고 Dispose 메서드와 선택적 종료자 간에 공유할 모든 리소스 정리 논리를 구현하는 Dispose(bool) 메서드를 선언하는 작업이 포함됩니다.

다음 예는 기본 패턴의 간단한 구현을 나타냅니다.

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 구현에서 호출될 때만 액세스해야 합니다(disposing 매개 변수가 true). 메서드가 종료자(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();
    }
}

단순히 Dispose(true)GC.SuppressFinalize(this)를 차례로 호출하여 IDisposable 인터페이스를 구현하세요.

SuppressFinalizeDispose(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) { ... }
}

XDispose()Dispose(bool) 이외에는 Dispose의 오버로드를 선언하지 마세요.

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) 내에서 예외를 throw하지 않는 것이 좋습니다.

사용자는 Dispose 호출이 예외를 발생시키지 않을 것으로 예상합니다.

Dispose가 예외를 발생시킬 수 있는 경우 finally 블록 정리 논리가 실행되지 않습니다. 이 문제를 해결하려면 사용자가 try 블록(finally 블록 내)에서 모든 Dispose 호출을 래핑해야 하므로 매우 복잡한 정리 처리기가 필요합니다. Dispose(bool 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;
    }
}

종료가 해당 영역의 표준 용어인 경우 Dispose() 외에도 Close() 메서드를 제공하는 것이 좋습니다.

이 경우 Close 구현을 Dispose와 동일하게 만들고 IDisposable.Dispose 메서드를 명시적으로 구현하는 것이 좋습니다.

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

종료 가능 형식

종료 가능 형식은 종료자를 재정의하고 Dispose(bool) 메서드에서 종료 코드 경로를 제공하여 기본 Dispose 패턴을 확장하는 형식입니다.

종료자는 올바로 구현하기가 매우 어렵습니다. 실행 중인 시스템의 상태에 대한 특정(일반적으로 유효한) 가정을 할 수 없기 때문입니다. 다음 지침을 신중하게 고려해야 합니다.

일부 지침은 Finalize 메서드뿐만 아니라 종료자에서 호출된 모든 코드에 적용됩니다. 이전에 정의된 기본 Dispose 패턴의 경우 disposing 매개 변수가 false일 때 Dispose(bool 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 형식을 종료 가능하게 지정하지 않는 것이 좋습니다.

종료자가 필요하다고 생각되는 사례는 항상 신중하게 고려해야 합니다. 성능 및 코드 복잡성 관점에서 종료자가 있는 인스턴스는 실제 비용이 발생합니다. SafeHandle와 리소스 래퍼를 사용하여 가능하면 관리되지 않는 리소스를 캡슐화하는 것이 좋습니다. 이 경우 래퍼가 자체 리소스 정리를 담당하기 때문에 종료자가 불필요해집니다.

X 값 형식을 종료 가능하게 지정하지 마세요.

실제로 참조 형식만 CLR에 의해 종료되므로 값 형식에 종료자를 배치하려는 시도는 무시됩니다. C# 및 C++ 컴파일러에서 이 규칙을 적용합니다.

형식이 자체 종료자가 없는 관리되지 않는 리소스를 해제할 책임이 있는 경우 형식을 종료 가능하게 지정하세요.

종료자를 구현하는 경우 Dispose(false)를 호출하고 Dispose(bool disposing) 메서드 내에 모든 리소스 정리 논리를 배치하기만 하면 됩니다.

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

모든 종료 가능 형식에서 기본 Dispose 패턴을 구현하세요.

이는 형식의 사용자에게 종료자가 담당하는 동일한 리소스에 대한 결정론적 정리를 명시적으로 수행할 수 있는 수단을 제공합니다.

X 종료자 코드 경로에서 종료 가능한 개체에 액세스하지 마세요. 이미 종료되었을 위험이 매우 크기 때문입니다.

예를 들어 종료 가능한 개체 A가 종료 가능한 다른 개체 B에 대한 참조가 있는 경우 A의 종료자에서 B를 안정적으로 사용할 수 없을 수 있고 그 반대의 경우도 마찬가지입니다. 종료자는 임의 순서로 호출됩니다(중요한 종료를 위한 약한 순서 보장에 미달).

또한 정적 변수에 저장된 개체는 애플리케이션 도메인 언로드 중 또는 프로세스를 종료하는 동안 특정 지점에서 수집됩니다. Environment.HasShutdownStarted가 true를 반환하는 경우 종료 가능한 개체를 참조하는 정적 변수에 액세스하거나 정적 변수에 저장된 값을 사용할 수 있는 정적 메서드를 호출하는 것은 안전하지 않을 수 있습니다.

Finalize 메서드를 보호하세요.

컴파일러가 이 지침을 적용하는 데 도움이 되므로 C#, C++ 및 VB.NET 개발자는 이에 대해 걱정할 필요가 없습니다.

X 시스템 중요 오류를 제외하고 예외가 종료자 논리에서 이스케이프되도록 하지 마세요.

종료자에서 예외가 throw되면 CLR은 전체 프로세스(.NET Framework 버전 2.0부터)를 종료하므로 다른 종료자가 실행되지 않고 리소스가 제어된 방식으로 해제되지 않습니다.

강제 애플리케이션 도메인 언로드 및 스레드 중단에도 불구하고 종료자가 절대적으로 실행되어야 하는 상황에 대해 중요한 종료 가능한 개체(CriticalFinalizerObject를 포함하는 형식 계층 구조가 있는 형식)를 만들고 사용하는 것이 좋습니다.

Portions © 2005, 2009 Microsoft Corporation. All rights reserved.

Pearson Education, Inc의 동의로 재인쇄. 출처: Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition 작성자: Krzysztof Cwalina 및 Brad Abrams, 출판 정보: Oct 22, 2008 by Addison-Wesley Professional as part of the Microsoft Windows Development Series.

참고 항목