예외에 대한 모범 사례

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

이 자료에서는 예외를 처리하고 만들기 위한 최선의 방법을 설명합니다.

예외 처리

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

Try/catch/finally 블록을 사용하여 오류를 복구하거나 리소스를 해제합니다.

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

using 문 또는 finally 블록으로 할당된 리소스를 정리합니다. 예외가 throw될 때 리소스를 자동으로 정리하려면 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 예외를 catch할 수 있습니다.

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.Parse은(는) OverflowException을(를) throw합니다. 그러나 Int32.TryParse은(는) 이 예외를 throw하지는 않습니다. 대신 부울을 반환하고 성공 시 구문 분석된 유효한 정수를 포함하는 out 매개 변수가 있습니다. Dictionary<TKey,TValue>.TryGetValue은(는) 사전에서 값을 가져오는 것과 비슷한 동작을 사용합니다.

캐치 취소 및 비동기 예외

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

비동기 메서드는 반환하는 태스크에서 실행 중에 throw되는 예외를 저장합니다. 예외가 반환된 작업에 저장되면 태스크가 대기될 때 해당 예외가 throw됩니다. ArgumentException 같은 사용 예외는 여전히 동기적으로 throw됩니다. 자세한 내용은 비동기 예외를 참조하세요.

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

클래스는 예외를 트리거하는 호출을 방지할 수 있도록 하는 메서드 또는 속성을 제공할 수 있습니다. 예를 들어, 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>를 추가하여 불필요한지를 확인하는 추가 사례를 만들고 오류의 잠재적 원인을 만드는 역할만 할 수도 있습니다.

예외로 인해 메서드가 완료되지 않을 때의 상태 복원

호출자가 메서드에서 예외가 throw될 때 의도하지 않은 결과가 발생하지 않는다고 가정할 수 있어야 합니다. 예를 들어 하나의 계좌에서 출금한 후 다른 계좌에 입금하여 돈을 이체하는 코드가 있고 입금을 실행하는 동안 예외가 발생할 경우 출금이 적용되기를 원하지 않을 것입니다.

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하지 않습니다. 다만, 입금 작업이 실패할 경우 인출이 취소되도록 메서드를 작성해야 합니다.

이 상황을 처리하는 한 가지 방법은 입금 트랜잭션에서 throw된 예외를 catch하고 출금을 롤백하는 것입니다.

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

예외를 제대로 캡처하고 rethrow

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

  • 예외 인스턴스를 캐치한 처리기(catch 블록) 내에서 예외를 다시 throw하는 경우 예외를 지정하지 않고 throw 문을 사용합니다. CA2200 코드 분석 규칙은 실수로 스택 추적 정보를 잃을 수 있는 위치를 코드에서 찾는 데 도움이 됩니다.
  • 처리기가 아닌 다른 곳에서 예외를 rethrow하는 경우(catch 블록), 처리기에서 예외를 캡처할 때는 ExceptionDispatchInfo.Capture(Exception)을(를) 사용하고 rethrow하고 싶을 때는 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에서 파생된 미리 정의된 클래스 중 하나를 throw합니다.

참고 항목

가능한 경우 미리 정의된 예외 형식을 사용하는 것이 가장 좋은 반면 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 속성에서 파생된 메시지입니다. 일반적으로 예외 생성자message 인수에 메시지 문자열을 전달하여 값을 Exception.Message 속성에 할당합니다.

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

적절한 문법 사용

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

throw 문을 잘 배치

throw 문을 배치하면 스택 추적에 도움이 됩니다. 스택 추적은 예외가 throw되는 문에서 시작하여 예외를 catch하는 catch 문까지 수행됩니다.

finally 절에서 예외 발생시키기 금지

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

예기치 않은 위치에서 예외 발생시키기 금지

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

동기적으로 인수 유효성 검사 예외 throw

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

사용자 지정 예외 형식

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

예외 클래스 이름을 Exception(으)로 마침

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

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

세 개의 생성자 포함

사용자 고유의 예외 클래스를 만들 때 최소한 다음 세 가지 일반 생성자를 사용합니다. 즉, 매개 변수 없는 생성자, 문자열 메시지를 사용하는 생성자, 문자열 메시지와 내부 예외를 사용하는 생성자입니다.

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

필요에 따라 추가 속성 제공

추가 정보가 유용한 프로그래밍 시나리오에 대해서만 예외에 사용자 지정 메시지 문자열 이외의 추가 속성을 제공합니다. 예를 들어, FileNotFoundExceptionFileName 속성을 제공합니다.

참고 항목