DisposeAsync 메서드 구현

System.IAsyncDisposable 인터페이스는 C# 8.0의 일부로 도입되었습니다. IAsyncDisposable.DisposeAsync() 메서드는 Dispose 메서드를 구현할 때와 마찬가지로 리소스 정리를 수행하는 경우에 구현합니다. 그러나 중요한 차이점 하나는 이 구현에서는 비동기 정리 작업이 가능하다는 것입니다. DisposeAsync()는 비동기 삭제 작업을 나타내는 ValueTask를 반환합니다.

일반적으로 IAsyncDisposable 인터페이스를 구현할 때 클래스가 IDisposable 인터페이스도 구현합니다. IAsyncDisposable 인터페이스의 좋은 구현 패턴은 동기 또는 비동기 처리를 위해 준비하는 것이지만 필수 사항은 아닙니다. 클래스를 동기식으로 삭제할 수 없는 경우 IAsyncDisposable만 사용하는 것이 허용됩니다. 삭제 패턴을 구현하기 위한 모든 지침은 비동기 구현에도 적용됩니다. 이 문서에서는 Dispose 메서드를 구현하는 방법을 이미 잘 알고 있다고 가정합니다.

주의

IAsyncDisposable 인터페이스를 구현하고 IDisposable 인터페이스를 구현하지 않으면 앱에서 리소스가 누출될 수 있습니다. 클래스가 IAsyncDisposable을 구현하지만 IDisposable은 구현하지 않고 소비자가 Dispose만 호출하는 경우 구현에서는 결코 DisposeAsync를 호출하지 않습니다. 이로 인해 리소스 누수가 발생합니다.

종속성 주입과 관련하여 IServiceCollection에 서비스를 등록하면 서비스 수명이 사용자를 대신하여 암시적으로 관리됩니다. IServiceProvider 및 해당 IHost는 리소스 정리를 오케스트레이션합니다. 특히, IDisposableIAsyncDisposable의 구현은 지정된 수명이 끝나면 적절하게 삭제됩니다.

자세한 내용은 .NET에서 종속성 주입을 참조하세요.

DisposeAsyncDisposeAsyncCore 메서드 살펴보기

IAsyncDisposable 인터페이스는 매개 변수가 없는 단일 메서드 DisposeAsync()를 선언합니다. 봉인되지 않은 모든 클래스는 ValueTask도 반환하는 DisposeAsyncCore() 메서드를 정의해야 합니다.

  • 매개 변수가 없는 publicIAsyncDisposable.DisposeAsync() 구현

  • 시그니처가 다음과 같은 protected virtual ValueTask DisposeAsyncCore() 메서드

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

DisposeAsync 메서드

public 매개 변수가 없는 DisposeAsync() 메서드는 await using 문에서 암시적으로 호출되며 관리되지 않는 리소스를 해제하고, 일반 정리를 수행하고, 종료자(있는 경우)를 실행할 필요가 없음을 나타내기 위한 것입니다. 관리형 개체와 관련된 메모리를 해제하는 것은 항상 가비지 수집기의 영역입니다. 이로 인해 다음과 같은 표준 구현이 수행됩니다.

public async ValueTask DisposeAsync()
{
    // Perform async cleanup.
    await DisposeAsyncCore();

    // Dispose of unmanaged resources.
    Dispose(false);

    // Suppress finalization.
    GC.SuppressFinalize(this);
}

참고 항목

삭제 패턴과 비교해 비동기 삭제 패턴의 한 가지 주된 차이점은 Dispose(bool) 오버로드 메서드에 대한 DisposeAsync()의 호출에서 인수로 false가 지정된다는 것입니다. 그러나 IDisposable.Dispose() 메서드를 구현할 때는 대신 true가 전달됩니다. 이렇게 하면 동기 삭제 패턴과 기능적 동등성을 보장하고, 또한 종료자 코드 경로가 계속 호출되도록 할 수 있습니다. 즉, DisposeAsyncCore() 메서드에서 관리형 리소스를 비동기적으로 삭제하므로 동기적으로도 삭제할 필요가 없습니다. 따라서 Dispose(true) 대신 Dispose(false)를 호출합니다.

DisposeAsyncCore 메서드

DisposeAsyncCore() 메서드는 관리되는 리소스의 비동기 정리를 수행하거나 DisposeAsync()에 대한 계단식 호출에 사용됩니다. 서브클래스가 IAsyncDisposable 구현인 기본 클래스를 상속하는 경우 일반적인 비동기 정리 작업을 캡슐화합니다. 파생 클래스가 재정의에서 사용자 지정 정리를 정의할 수 있도록 DisposeAsyncCore() 메서드는 virtual입니다.

IAsyncDisposable 구현이 sealed인 경우에는 DisposeAsyncCore() 메서드가 필요하지 않으며 IAsyncDisposable.DisposeAsync() 메서드에서 직접 비동기 정리를 수행할 수 있습니다.

비동기 삭제 패턴 구현

모든 봉인되지 않은 클래스는 상속될 수 있기 때문에 잠재적 기본 클래스로 간주해야 합니다. 잠재적 기본 클래스에 대한 비동기 삭제 패턴을 구현하는 경우 protected virtual ValueTask DisposeAsyncCore() 메서드를 제공해야 합니다. 다음 예 중 일부에서는 다음과 같이 정의된 NoopAsyncDisposable 클래스를 사용합니다.

public sealed class NoopAsyncDisposable : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}

다음은 NoopAsyncDisposable 형식을 사용하는 비동기 Dispose 패턴의 구현 예입니다. 이 형식은 ValueTask.CompletedTask를 반환하여 DisposeAsync를 구현합니다.

public class ExampleAsyncDisposable : IAsyncDisposable
{
    private IAsyncDisposable? _example;

    public ExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        GC.SuppressFinalize(this);
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_example is not null)
        {
            await _example.DisposeAsync().ConfigureAwait(false);
        }

        _example = null;
    }
}

앞의 예에서:

  • ExampleAsyncDisposableIAsyncDisposable 인터페이스를 구현하는 봉인되지 않은 클래스입니다.
  • 여기에는 생성자에서 초기화되는 프라이빗 IAsyncDisposable 필드인 _example이 포함되어 있습니다.
  • DisposeAsync 메서드는 DisposeAsyncCore 메서드에 위임하고 GC.SuppressFinalize를 호출하여 종료자가 실행될 필요가 없음을 가비지 수집기에 알립니다.
  • 여기에는 _example.DisposeAsync() 메서드를 호출하고 필드를 null로 설정하는 DisposeAsyncCore() 메서드가 포함되어 있습니다.
  • DisposeAsyncCore() 메서드는 virtual이며, 이를 통해 하위 클래스가 사용자 지정 동작으로 이를 재정의할 수 있습니다.

봉인된 대체 비동기 Dispose 패턴

구현 클래스가 sealed일 수 있는 경우 IAsyncDisposable.DisposeAsync() 메서드를 재정의하여 비동기 Dispose 패턴을 구현할 수 있습니다. 다음 예에서는 봉인된 클래스에 대한 비동기 Dispose 패턴을 구현하는 방법을 보여 줍니다.

public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
    private readonly IAsyncDisposable _example;

    public SealedExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public ValueTask DisposeAsync() => _example.DisposeAsync();
}

앞의 예에서:

  • SealedExampleAsyncDisposableIAsyncDisposable 인터페이스를 구현하는 봉인된 클래스입니다.
  • 포함된 _example 필드는 readonly이며 생성자에서 초기화됩니다.
  • DisposeAsync 메서드는 _example.DisposeAsync() 메서드를 호출하여 포함 필드를 통해 패턴을 구현합니다(계단식 삭제).

삭제 패턴과 비동기 삭제 패턴 둘 다 구현

특히 클래스 범위에 이러한 구현의 인스턴스가 포함된 경우 IDisposable 인터페이스와 IAsyncDisposable 인터페이스를 둘 다 구현해야 할 수도 있습니다. 이렇게 하면 정리 호출을 적절히 계단식 배열할 수 있습니다. 다음은 두 인터페이스를 모두 구현하는 예제 클래스이며 정리에 대한 적절한 지침을 보여 줍니다.

class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
    IDisposable? _disposableResource = new MemoryStream();
    IAsyncDisposable? _asyncDisposableResource = new MemoryStream();

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        Dispose(disposing: false);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _disposableResource?.Dispose();
            _disposableResource = null;

            if (_asyncDisposableResource is IDisposable disposable)
            {
                disposable.Dispose();
                _asyncDisposableResource = null;
            }
        }
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_asyncDisposableResource is not null)
        {
            await _asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
        }

        if (_disposableResource is IAsyncDisposable disposable)
        {
            await disposable.DisposeAsync().ConfigureAwait(false);
        }
        else
        {
            _disposableResource?.Dispose();
        }

        _asyncDisposableResource = null;
        _disposableResource = null;
    }
}

IDisposable.Dispose() 구현과 IAsyncDisposable.DisposeAsync() 구현은 둘 다 간단한 상용구 코드입니다.

Dispose(bool) 오버로드 메서드에서 IDisposable 인스턴스는 null이 아닌 경우 조건적으로 삭제됩니다. IAsyncDisposable 인스턴스는 IDisposable로 캐스팅되고 null이 아닌 경우에도 삭제됩니다. 그런 다음, 두 인스턴스가 모두 null에 할당됩니다.

DisposeAsyncCore() 메서드를 사용하여 동일한 논리 접근 방식을 따릅니다. IAsyncDisposable 인스턴스가 null이 아니면 DisposeAsync().ConfigureAwait(false) 호출을 기다립니다. IDisposable 인스턴스가 IAsyncDisposable의 구현이기도 한 경우에도 비동기적으로 삭제됩니다. 그런 다음, 두 인스턴스가 모두 null에 할당됩니다.

각 구현에서는 삭제 가능한 모든 개체를 삭제하려고 합니다. 이렇게 하면 정리가 적절하게 계단식으로 진행됩니다.

비동기 삭제 가능 사용

IAsyncDisposable 인터페이스를 구현하는 개체를 제대로 사용하려면 awaitusing 키워드를 함께 사용합니다. ExampleAsyncDisposable 클래스를 인스턴스화한 후에 await using 문에 래핑하는 다음 예제를 살펴보세요.

class ExampleConfigureAwaitProgram
{
    static async Task Main()
    {
        var exampleAsyncDisposable = new ExampleAsyncDisposable();
        await using (exampleAsyncDisposable.ConfigureAwait(false))
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

Important

IAsyncDisposable 인터페이스의 ConfigureAwait(IAsyncDisposable, Boolean) 확장 메서드를 사용하면 작업 연속이 원래 컨텍스트 또는 스케줄러에서 마샬링되는 방법을 구성할 수 있습니다. ConfigureAwait에 대한 자세한 내용은 ConfigureAwait FAQ를 참조하세요.

ConfigureAwait를 사용하지 않아도 되는 상황에서는 await using 문을 다음과 같이 간소화할 수 있습니다.

class ExampleUsingStatementProgram
{
    static async Task Main()
    {
        await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

또한 using 선언의 암시적 범위를 사용하도록 작성할 수 있습니다.

class ExampleUsingDeclarationProgram
{
    static async Task Main()
    {
        await using var exampleAsyncDisposable = new ExampleAsyncDisposable();

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

한 줄에 여러 개의 대기 키워드가 있음

때로는 await 키워드가 한 줄에 여러 번 나타날 수도 있습니다. 예를 들어, 다음 코드를 고려하세요.

await using var transaction = await context.Database.BeginTransactionAsync(token);

앞의 예에서:

  • BeginTransactionAsync 메서드가 기다리고 있습니다.
  • 반환 형식은 IAsyncDisposable을 구현하는 DbTransaction입니다.
  • transaction은 비동기식으로 사용되며 대기 중입니다.

스택형 using

IAsyncDisposable을 구현하는 여러 개체를 만들고 사용하는 상황에서는 ConfigureAwait를 사용하여 await using 문을 스택하면 잘못된 조건에서 DisposeAsync() 호출을 방지할 수 있습니다. DisposeAsync()가 항상 호출되게 하려면 스택을 방지해야 합니다. 다음 세 가지 코드 예제는 대신 사용할 수 있는 패턴을 보여 줍니다.

허용 가능한 패턴 1


class ExampleOneProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.

            var objTwo = new ExampleAsyncDisposable();
            await using (objTwo.ConfigureAwait(false))
            {
                // Interact with the objOne and/or objTwo instance(s).
            }
        }

        Console.ReadLine();
    }
}

앞의 예제에서 각 비동기 정리 작업은 await using 블록에서 명시적으로 범위가 지정됩니다. 외부 범위는 objOneobjTwo를 둘러싸는 중괄호를 설정하는 방법을 따르므로 objTwo가 먼저 삭제된 후 objOne이 삭제됩니다. 두 IAsyncDisposable 인스턴스 모두 해당 DisposeAsync() 메서드가 대기 중이므로 각 인스턴스가 비동기 정리 작업을 수행합니다. 호출은 스택되지 않고 중첩됩니다.

허용 가능한 패턴 2

class ExampleTwoProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.
        }

        var objTwo = new ExampleAsyncDisposable();
        await using (objTwo.ConfigureAwait(false))
        {
            // Interact with the objTwo instance.
        }

        Console.ReadLine();
    }
}

앞의 예제에서 각 비동기 정리 작업은 await using 블록에서 명시적으로 범위가 지정됩니다. 각 블록의 끝에서 해당 IAsyncDisposable 인스턴스는 DisposeAsync() 메서드를 대기시키므로 비동기 정리 작업을 수행합니다. 호출은 스택되지 않고 순차적입니다. 이 시나리오에서는 objOne이 먼저 삭제된 후 objTwo가 삭제됩니다.

허용 가능한 패턴 3

class ExampleThreeProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using var ignored1 = objOne.ConfigureAwait(false);

        var objTwo = new ExampleAsyncDisposable();
        await using var ignored2 = objTwo.ConfigureAwait(false);

        // Interact with objOne and/or objTwo instance(s).

        Console.ReadLine();
    }
}

앞의 예제에서 각 비동기 정리 작업은 포함하는 메서드 본문에서 암시적으로 범위가 지정됩니다. 바깥쪽 블록의 끝에서 IAsyncDisposable 인스턴스는 비동기 정리 작업을 수행합니다. 이 예는 선언된 순서와 반대 순서로 실행됩니다. 즉, objTwoobjOne보다 먼저 삭제됩니다.

허용되지 않는 패턴

다음 코드에서 강조 표시된 줄은 "스택 사용"이 무엇을 의미하는지 보여 줍니다. AnotherAsyncDisposable 생성자에서 예외가 throw되면 두 개체 모두 적절하게 삭제되지 않습니다. 생성자가 성공적으로 완료되지 않았기 때문에 변수 objTwo가 할당되지 않았습니다. 결과적으로 AnotherAsyncDisposable의 생성자는 예외가 throw되기 전에 할당된 모든 리소스를 삭제해야 합니다. ExampleAsyncDisposable 형식에 종료자가 있으면 종료할 수 있습니다.

class DoNotDoThisProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        // Exception thrown on .ctor
        var objTwo = new AnotherAsyncDisposable();

        await using (objOne.ConfigureAwait(false))
        await using (objTwo.ConfigureAwait(false))
        {
            // Neither object has its DisposeAsync called.
        }

        Console.ReadLine();
    }
}

예기치 않은 동작이 발생할 수 있으므로 이 패턴을 사용하지 마세요. 허용되는 패턴 중 하나를 사용하면 삭제되지 않은 개체의 문제가 존재하지 않습니다. using 문이 스택되지 않으면 정리 작업이 올바르게 수행됩니다.

참고 항목

IDisposableIAsyncDisposable의 이중 구현 예제는 GitHub에서Utf8JsonWriter 소스 코드를 참조하세요.