다음을 통해 공유


예외에 대한 모범 사례

적절한 예외 처리는 애플리케이션 안정성에 필수적입니다. 앱이 충돌하지 않도록 예상 예외를 의도적으로 처리할 수 있습니다. 그러나 크래시된 앱은 정의되지 않은 동작을 가진 앱보다 더 안정적이고 진단 가능합니다.

이 문서에서는 예외를 처리하고 만드는 모범 사례를 설명합니다.

예외 처리

다음 모범 사례는 예외를 처리하는 방법에 관한 것입니다.

try/catch/finally 블록을 사용하여 오류 복구 또는 리소스 해제

잠재적으로 예외를 생성할 수 있는 코드와 앱이 해당 예외에서 복구할 수 있는 경우 코드 주위에 try/catch 블록을 사용합니다. catch 블록에서 항상 가장 파생된 것에서 가장 적게 파생된 예외로 예외를 정렬합니다. (모든 예외는 Exception 클래스에서 파생됩니다. 파생된 예외는 기본 예외 클래스에 대한 catch 절 앞에 오는 catch 절에서 처리되지 않습니다.) 코드가 예외에서 복구할 수 없는 경우 해당 예외를 catch하지 마세요. 가능하면 메서드를 호출 스택 위로 이동하여 복구할 수 있도록 설정합니다.

using 문 또는 finally 블록으로 할당된 리소스를 정리합니다. 예외가 발생하면 using 문을 사용하여 리소스를 자동으로 해제하는 것이 좋습니다. finally 블록을 사용하여 IDisposable구현하지 않는 리소스를 정리합니다. finally 절의 코드는 예외가 throw되는 경우에도 거의 항상 실행됩니다.

예외를 방지하기 위한 일반적인 조건 처리

발생할 가능성이 있지만 예외를 트리거할 수 있는 조건의 경우 예외를 방지하는 방식으로 처리하는 것이 좋습니다. 예를 들어, 이미 닫힌 연결을 닫으려고 하면 InvalidOperationException오류가 발생합니다. if 문을 사용하여 연결을 닫기 전에 연결 상태를 확인하여 방지할 수 있습니다.

if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

닫기 전에 연결 상태를 확인하지 않으면 InvalidOperationException 예외를 포착할 수 있습니다.

try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}
Try
    conn.Close()
Catch ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

선택하는 방법은 이벤트가 발생할 것으로 예상되는 빈도에 따라 달라집니다.

  • 이벤트가 자주 발생하지 않는 경우 예외 처리를 사용합니다. 즉, 이벤트가 실제로 예외적이고 예기치 않은 파일 끝과 같은 오류를 나타내는 경우입니다. 예외 처리를 사용하는 경우 일반 조건에서 더 적은 코드가 실행됩니다.

  • 이벤트가 정기적으로 발생하고 정상적인 실행의 일부로 간주될 수 있는 경우 코드에서 오류 조건을 확인합니다. 일반적인 오류 조건을 확인하면 예외를 방지하므로 코드가 더 적게 실행됩니다.

    메모

    선행 검사는 대부분의 경우 예외를 제거합니다. 그러나 검사와 작업 사이에 보호된 조건이 변경될 수 있는 경쟁 상태가 존재할 수 있으며, 이러한 경우에는 여전히 예외가 발생할 수 있습니다.

예외를 방지하기 위해 Try* 메서드 호출

예외의 성능 비용이 엄청나게 많은 경우 일부 .NET 라이브러리 메서드는 대체 형태의 오류 처리를 제공합니다. 예를 들어, 값이 너무 커서 Int32로 나타낼 수 없는 경우, Int32.ParseOverflowException을 던집니다. 그러나 Int32.TryParse 이 예외를 throw하지는 않습니다. 대신 논리값을 반환하며, 성공 시 구문 분석된 유효한 정수를 포함하는 out 매개 변수가 있습니다. Dictionary<TKey,TValue>.TryGetValue 사전에서 값을 가져오는 것과 비슷한 동작을 가지고 있습니다.

취소 및 비동기 예외 처리하기

비동기 메서드를 호출할 때는 OperationCanceledException에서 파생되는 TaskCanceledException을 잡는 대신 OperationCanceledException을 catch하는 것이 좋습니다. 취소가 요청되면 많은 비동기 메서드가 OperationCanceledException 예외를 throw합니다. 이러한 예외를 사용하면 실행을 효율적으로 중지하고 취소 요청이 관찰되면 호출 스택을 해제할 수 있습니다.

비동기 메서드는 실행 중에 발생하는 예외를 반환하는 태스크에 저장합니다. 반환된 작업에 예외가 저장된 경우, 그 작업을 대기할 때 해당 예외가 발생합니다. ArgumentException같은 사용 예외는 여전히 동기적으로 던져집니다. 자세한 내용은 비동기 예외를 참조하세요.

예외를 방지할 수 있도록 클래스 디자인

클래스는 예외를 트리거하는 호출을 방지할 수 있는 메서드 또는 속성을 제공할 수 있습니다. 예를 들어 FileStream 클래스는 파일의 끝에 도달했는지 여부를 확인하는 데 도움이 되는 메서드를 제공합니다. 이러한 메서드를 호출하여 파일의 끝을 지나 읽는 경우 throw되는 예외를 방지할 수 있습니다. 다음 예제에서는 예외를 트리거하지 않고 파일의 끝까지 읽는 방법을 보여줍니다.

class FileRead
{
    public static void ReadAll(FileStream fileToRead)
    {
        ArgumentNullException.ThrowIfNull(fileToRead);

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead.Length; i++)
        {
            b = fileToRead.ReadByte();
            Console.Write(b.ToString());
            // Or do something else with the byte.
        }
    }
}
Class FileRead
    Public Sub ReadAll(fileToRead As FileStream)
        ' This if statement is optional
        ' as it is very unlikely that
        ' the stream would ever be null.
        If fileToRead Is Nothing Then
            Throw New System.ArgumentNullException()
        End If

        Dim b As Integer

        ' Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin)

        ' Read each byte to the end of the file.
        For i As Integer = 0 To fileToRead.Length - 1
            b = fileToRead.ReadByte()
            Console.Write(b.ToString())
            ' Or do something else with the byte.
        Next i
    End Sub
End Class

예외를 방지하는 또 다른 방법은 예외를 throw하는 대신 가장 일반적인 오류 사례에 대한 null(또는 기본값)를 반환하는 것입니다. 일반적인 오류 사례는 일반적인 제어 흐름으로 간주될 수 있습니다. 이러한 경우 null(또는 기본값)를 반환하면 앱에 대한 성능 영향을 최소화할 수 있습니다.

값 형식의 경우 앱의 오류 표시기로 Nullable<T> 또는 default 사용할지 여부를 고려합니다. Nullable<Guid>사용하면 defaultGuid.Empty대신 null 됩니다. 경우에 따라 Nullable<T> 추가하면 값이 있거나 없는 경우 더 명확하게 할 수 있습니다. Nullable<T> 추가하면 필요하지 않은지 확인하는 추가 사례가 생성되고 잠재적인 오류 원본을 만드는 데만 도움이 될 수 있습니다.

예외로 인해 메서드가 완료되지 않은 경우의 복원 상태

호출자는 메서드에서 예외가 발생할 때 부작용이 없다는 점을 가정할 수 있어야 합니다. 예를 들어 한 계좌에서 인출하고 다른 계좌에 입금하여 돈을 이체하는 코드가 있을 때, 입금 과정 중에 예외가 발생하는 경우 인출이 완료되지 않도록 해야 합니다.

public void TransferFunds(Account from, Account to, decimal amount)
{
    from.Withdrawal(amount);
    // If the deposit fails, the withdrawal shouldn't remain in effect.
    to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    from.Withdrawal(amount)
    ' If the deposit fails, the withdrawal shouldn't remain in effect.
    [to].Deposit(amount)
End Sub

앞의 메서드는 예외를 직접 throw하지 않습니다. 그러나 입금 작업이 실패할 경우 인출이 취소되도록 메서드를 작성해야 합니다.

이 상황을 처리하는 한 가지 방법은 예금 거래에 의해 발생한 예외를 잡아내어 인출을 취소하는 것입니다.

private static void TransferFunds(Account from, Account to, decimal amount)
{
    string withdrawalTrxID = from.Withdrawal(amount);
    try
    {
        to.Deposit(amount);
    }
    catch
    {
        from.RollbackTransaction(withdrawalTrxID);
        throw;
    }
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    Dim withdrawalTrxID As String = from.Withdrawal(amount)
    Try
        [to].Deposit(amount)
    Catch
        from.RollbackTransaction(withdrawalTrxID)
        Throw
    End Try
End Sub

이 예제에서는 throw 사용하여 원래 예외를 다시 throw하여 호출자가 InnerException 속성을 검사하지 않고도 문제의 실제 원인을 쉽게 확인할 수 있도록 합니다. 대안은 새 예외를 throw하고 원래 예외를 내부 예외로 포함하는 것입니다.

catch (Exception ex)
{
    from.RollbackTransaction(withdrawalTrxID);
    throw new TransferFundsException("Withdrawal failed.", innerException: ex)
    {
        From = from,
        To = to,
        Amount = amount
    };
}
Catch ex As Exception
    from.RollbackTransaction(withdrawalTrxID)
    Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
    {
        .From = from,
        .[To] = [to],
        .Amount = amount
    }
End Try

예외를 적절히 포착하고 다시 던집니다.

예외가 발생하면 포함된 정보 중 일부는 스택 추적입니다. 스택 추적은 예외를 throw하는 메서드에서 시작하여 예외를 catch하는 메서드로 끝나는 메서드 호출 계층 구조의 목록입니다. throw 문에서 예외를 지정하여 예외를 다시 throw하는 경우(예: throw e) 스택 추적은 현재 메서드에서 다시 시작되고 예외를 throw한 원래 메서드와 현재 메서드 간의 메서드 호출 목록이 손실됩니다. 예외를 제외하고 원래 스택 추적 정보를 유지하려면 예외를 다시 발생시키는 위치에 따라 두 가지 옵션이 있습니다.

  • 예외 인스턴스를 catch한 처리기(catch 블록) 내에서 예외를 다시 throw하는 경우 예외를 지정하지 않고 throw 문을 사용합니다. CA2200 코드 분석 규칙은 실수로 스택 추적 정보를 잃을 수 있는 위치를 코드에서 찾는 데 도움이 됩니다.
  • 처리기(예:catch 블록) 이외의 위치에서 예외를 다시 throw하는 경우, ExceptionDispatchInfo.Capture(Exception)을 사용하여 처리기에서 예외를 캡처하고, 다시 throw하려는 경우 ExceptionDispatchInfo.Throw()를 사용하세요. ExceptionDispatchInfo.SourceException 속성을 사용하여 캡처된 예외를 검사할 수 있습니다.

다음 예제에서는 ExceptionDispatchInfo 클래스를 사용할 수 있는 방법 및 출력의 모양을 보여 줄 수 있습니다.

ExceptionDispatchInfo? edi = null;
try
{
    var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
    edi = ExceptionDispatchInfo.Capture(e);
}

// ...

Console.WriteLine("I was here.");

if (edi is not null)
    edi.Throw();

예제 코드의 파일이 없으면 다음 출력이 생성됩니다.

I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

예외를 던지기

다음 모범 사례는 예외를 throw하는 방법에 관한 것입니다.

미리 정의된 예외 형식 사용

미리 정의된 예외 클래스가 적용되지 않는 경우에만 새 예외 클래스를 도입합니다. 예를 들어:

  • 개체의 현재 상태를 고려할 때 속성 집합 또는 메서드 호출이 적절하지 않은 경우 InvalidOperationException 예외를 throw합니다.
  • 잘못된 매개 변수가 전달되면 ArgumentException 예외를 발생시키거나 ArgumentException에서 파생된 미리 정의된 클래스 중 하나를 발생시킵니다.

메모

가능한 경우 미리 정의된 예외 형식을 사용하는 것이 가장 좋지만, AccessViolationException, IndexOutOfRangeException, NullReferenceExceptionStackOverflowException같은 일부 예약된 예외 형식을 발생시켜서는 안 됩니다. 자세한 내용은 CA2201: 예약된 예외 유형을 발생시키지 마세요.

예외 작성기 메서드 사용

클래스는 구현의 여러 위치에서 동일한 예외를 throw하는 것이 일반적입니다. 과도한 코드를 방지하려면 예외를 만들고 반환하는 도우미 메서드를 만듭니다. 예를 들어:

class FileReader
{
    private readonly string _fileName;

    public FileReader(string path)
    {
        _fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
        return results;
    }

    static FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
Class FileReader
    Private fileName As String


    Public Sub New(path As String)
        fileName = path
    End Sub

    Public Function Read(bytes As Integer) As Byte()
        Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
        If results Is Nothing
            Throw NewFileIOException()
        End If
        Return results
    End Function

    Function NewFileIOException() As FileReaderException
        Dim description As String = "My NewFileIOException Description"

        Return New FileReaderException(description)
    End Function
End Class

일부 주요 .NET 예외 형식에는 예외를 할당하고 throw하는 정적 throw 도우미 메서드가 있습니다. 해당 예외 형식을 생성하고 throw하는 대신 다음 메서드를 호출해야 합니다.

다음 코드 분석 규칙을 사용하면 이러한 정적 throw 도우미를 활용할 수 있는 위치를 코드에서 찾을 수 있습니다. CA1510, CA1511, CA1512CA1513.

비동기 메서드를 구현하는 경우 취소가 요청되었는지 확인한 다음 OperationCanceledException생성 및 throw하는 대신 CancellationToken.ThrowIfCancellationRequested() 호출합니다. 자세한 내용은 CA2250참조하세요.

지역화된 문자열 메시지 포함

사용자가 보는 오류 메시지는 예외 클래스의 이름이 아니라 throw된 예외의 Exception.Message 속성에서 파생됩니다. 일반적으로 Exception 생성자message 인수에 메시지 문자열을 전달하여 Exception.Message 속성에 값을 할당합니다.

지역화된 애플리케이션의 경우 애플리케이션에서 throw할 수 있는 모든 예외에 대해 지역화된 메시지 문자열을 제공해야 합니다. 리소스 파일을 사용하여 지역화된 오류 메시지를 제공합니다. 애플리케이션을 지역화하고 지역화된 문자열을 검색하는 방법에 대한 자세한 내용은 다음 문서를 참조하세요.

적절한 문법 사용

명확한 문장을 작성하고 끝에 문장 부호를 포함하세요. Exception.Message 속성에 할당된 문자열의 각 문장은 마침표로 끝나야 합니다. 예를 들어, "로그 테이블이 오버플로되었습니다."라는 문장은 올바른 문법과 문장 부호를 사용합니다.

throw 문을 적절하게 배치하세요

throw 문을 스택 추적에 도움이 되는 위치에 배치하세요. 스택 트레이스는 예외가 throw되는 문에서 시작해서 예외를 catch하는 catch 문에서 끝납니다.

finally 절에서는 예외를 발생시키지 마세요.

finally 절에서 예외를 발생시키지 마십시오. 자세한 내용은 CA2219 코드 분석 규칙을 참조하세요.

예기치 않은 위치에서 예외를 발생시키지 마세요.

Equals, GetHashCodeToString 메서드, 정적 생성자 및 같음 연산자와 같은 일부 메서드는 예외를 throw해서는 안 됩니다. 자세한 내용은 CA1065 코드 분석 규칙을 참조하세요.

인수 유효성 검사 예외를 동기적으로 발생시키다

태스크 반환 메서드에서는 메서드의 비동기 부분을 입력하기 전에 인수의 유효성을 검사하고 ArgumentExceptionArgumentNullException같은 해당 예외를 throw해야 합니다. 메서드의 비동기 부분에서 발생하는 예외는 반환된 작업에 저장되고, 작업이 대기될 때까지 나타나지 않는다. 자세한 내용은 작업 반환 메서드 예외를 참조하세요.

사용자 지정 예외 형식

다음 모범 사례는 사용자 지정 예외 유형과 관련이 있습니다.

Exception 사용하여 예외 클래스 이름 종료

사용자 지정 예외가 필요한 경우 적절하게 이름을 지정하고 Exception 클래스에서 파생합니다. 예를 들어:

public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

3개의 생성자 포함

사용자 고유의 예외 클래스를 만들 때 매개 변수가 없는 생성자, 문자열 메시지를 사용하는 생성자, 문자열 메시지와 내부 예외를 사용하는 생성자 등 세 가지 이상의 공통 생성자를 사용합니다.

예를 들어 방법:사용자 정의 예외 만들기를 참조하세요.

필요에 따라 추가 속성 제공

추가 정보가 유용한 프로그래밍 방식 시나리오가 있는 경우에만 예외에 대한 추가 속성을 제공합니다(사용자 지정 메시지 문자열 외에). 예를 들어 FileNotFoundExceptionFileName 속성을 제공합니다.

참고