예외는 .NET을 사용할 때 피할 수 없는 문제이지만 API 설계 관점에서 이 문제를 고려하지 않는 개발자가 너무 많습니다. 대부분의 작업에서 그들은 처음부터 끝까지 어떤 예외를 포착해야 하는지, 어떤 예외를 글로벌 로그에 기록해야 하는지 알고 있습니다. 예외를 올바르게 사용할 수 있는 API를 설계하면 결함을 수정하는 데 걸리는 시간을 크게 줄일 수 있습니다.
누구의 잘못인가?
예외 설계의 기본 이론은 "누구의 잘못인가?"라는 질문에서 시작됩니다. 이 기사의 목적에 따라 이 질문에 대한 대답은 항상 다음 세 가지 중 하나입니다.
라이브러리
애플리케이션
환경
"라이브러리"에 문제가 있다는 것은 현재 실행되는 메소드 중 하나에 내부 결함이 있다는 의미입니다. 이 경우 "애플리케이션"은 라이브러리 메서드를 호출하는 코드입니다. (라이브러리와 애플리케이션 코드가 동일한 어셈블리에 있을 수 있기 때문에 다소 혼란스럽습니다.) 마지막으로 "환경"은 애플리케이션 외부의 모든 것을 나타냅니다. 통제할 수 없습니다.
라이브러리 결함
가장 일반적인 라이브러리 결함은 NullReferenceException입니다. 라이브러리가 애플리케이션에서 감지할 수 있는 null 참조 예외를 발생시킬 이유가 없습니다. null이 발생하면 라이브러리 코드는 항상 null이 무엇인지, 문제를 해결하는 방법을 설명하는 보다 구체적인 예외를 발생시켜야 합니다. 매개변수의 경우 이는 분명히 ArgumentNullException입니다. 속성이나 필드가 비어 있으면 일반적으로 InvalidOperationException이 더 적합합니다.
정의에 따르면 라이브러리 결함을 나타내는 모든 예외는 수정이 필요한 해당 라이브러리의 버그입니다. 이는 애플리케이션 코드에 버그가 없다는 의미가 아니라 라이브러리의 버그를 먼저 수정해야 한다는 의미입니다. 그래야만 애플리케이션 개발자도 자신도 실수를 했다는 사실을 알 수 있습니다.
이유는 같은 라이브러리를 이용하는 사람이 많을 수 있기 때문입니다. 한 사람이 실수로 null을 전달해서는 안 되는 곳에 실수로 전달하면 다른 사람도 같은 실수를 저지르게 됩니다. NullReferenceException을 무엇이 잘못되었는지 명확하게 보여주는 예외로 대체함으로써 애플리케이션 개발자는 무엇이 잘못되었는지 즉시 알 수 있습니다.
“성공의 구덩이”
.NET 디자인 패턴에 대한 초기 문헌을 읽으면 “성공의 구덩이”라는 문구를 자주 접하게 될 것입니다. 기본 아이디어는 다음과 같습니다. 코드를 올바르게 사용하기 쉽게 만들고, 잘못 사용하기 어렵게 만들고, 예외를 통해 무엇이 잘못되었는지 알 수 있도록 하는 것입니다. 이 API 설계 철학을 따르면 개발자는 처음부터 올바른 코드를 작성할 수 있다는 것이 거의 보장됩니다.
이것이 주석 처리되지 않은 NullReferenceException이 그토록 나쁜 이유입니다. 스택 추적(라이브러리 코드에 매우 깊이 있을 수 있음) 외에는 개발자가 무엇을 잘못하고 있는지 판단하는 데 도움이 되는 정보가 없습니다. 반면 ArgumentNullException 및 InvalidOperationException은 라이브러리 작성자가 응용 프로그램 개발자에게 문제 해결 방법을 설명할 수 있는 방법을 제공합니다.
기타 라이브러리 결함
다음 라이브러리 결함은 DivideByZeroException, FiniteNumberException 및 OverflowException을 포함한 ArithmeticException 계열입니다. 다시 말하지만 이는 해당 결함이 단지 매개변수 유효성 검사가 누락된 것일지라도 항상 라이브러리 메서드의 내부 결함을 의미합니다.
라이브러리 결함의 또 다른 예는 IndexOutOfRangeException입니다. 의미상으로는 ArgumentOutOfRangeException과 다르지 않습니다. IList.Item을 참조하세요. 하지만 배열 인덱서에서만 작동합니다. 애플리케이션 코드는 일반적으로 기본 배열을 사용하지 않으므로 이는 사용자 정의 컬렉션 클래스에 버그가 있음을 의미합니다.
ArrayTypeMismatchException은 .NET 2.0에 일반 목록이 도입된 이후 거의 발생하지 않았습니다. 이 예외를 발생시키는 상황은 다소 이상합니다. 문서에 따르면
ArrayTypeMismatchException은 시스템이 배열 요소를 선언된 배열 유형으로 변환할 수 없을 때 발생합니다. 예를 들어 String 유형의 요소는 두 유형 간에 변환이 없기 때문에 Int32 배열에 저장할 수 없습니다. 일반적으로 애플리케이션에서는 이러한 예외를 발생시킬 필요가 없습니다.
이를 위해서는 앞서 언급한 Int32 배열을 Object[] 유형의 변수에 저장해야 합니다. 원시 배열을 사용한 경우 라이브러리에서 이를 확인해야 합니다. 이러한 이유와 기타 여러 고려 사항으로 인해 원시 배열을 사용하지 않고 이를 적절한 컬렉션 클래스로 캡슐화하는 것이 좋습니다.
일반적으로 다른 캐스팅 문제는 InvalidCastException 예외에 반영됩니다. 주제로 돌아가서 유형 검사는 InvalidCastException이 발생하지 않지만 ArgumentException 또는 InvalidOperationException이 호출자에게 발생함을 의미합니다.
MemberAccessException은 다양한 반사 기반 오류를 처리하는 기본 클래스입니다. 리플렉션을 직접 사용하는 것 외에도 COM 상호 운용성 및 동적 키워드를 잘못 사용하면 이 예외가 발생할 수 있습니다.
앱 결함
일반적인 애플리케이션 결함은 ArgumentException과 해당 하위 클래스인 ArgumentNullException 및 ArgumentOutOfRangeException입니다. 다음은 여러분이 알지 못할 수도 있는 다른 하위 클래스입니다.
System.ComponentModel.InvalidAsynchronousStateException
System.ComponentModel.InvalidEnumArgumentException
System.DuplicateWaitObjectException
System.Globalization.CultureNotFoundException
System.IO.Log.ReservationNotFoundException
System.Text.DecoderFallbackException
System.Text.EncoderFallbackException
이들 모두는 애플리케이션이 있음을 명확하게 나타냅니다. 오류이며 문제는 라이브러리 메서드를 호출하는 줄에 있습니다. 해당 진술의 두 부분이 모두 중요합니다. 다음 코드를 고려하세요.
foo.Customer = null; foo.Save();
위 코드에서 ArgumentNullException이 발생하면 애플리케이션 개발자는 혼란스러워질 것입니다. 현재 줄 이전에 문제가 발생했음을 나타내는 InvalidOperationException을 발생시켜야 합니다.
예외 문서화
일반적인 프로그래머는 적어도 애초에 문서를 읽지 않습니다. 대신 공개 API를 읽고 일부 코드를 작성하고 실행합니다. 코드가 제대로 실행되지 않으면 Stack Overflow에서 예외 정보를 검색해 보세요. 프로그래머가 운이 좋다면 올바른 문서에 대한 링크와 함께 답변을 쉽게 찾을 수 있습니다. 그러나 그럼에도 불구하고 프로그래머는 실제로 그것을 읽지 않을 것입니다.
그렇다면 도서관 저자로서 우리는 이 문제를 어떻게 해결해야 할까요? 첫 번째 단계는 부분 문서를 예외에 직접 복사하는 것입니다.
추가 개체 상태 예외
InvalidOperationException에는 잘 알려진 하위 클래스 ObjectDisposedException이 있습니다. 그 목적은 분명하지만, 이 예외를 발생시키는 것을 잊어버린 파괴 가능한 클래스는 거의 없습니다. 잊어버린 경우 일반적인 결과는 NullReferenceException입니다. 이 예외는 파괴 가능한 자식 개체를 null로 설정하는 Dispose 메서드로 인해 발생합니다.
InvalidOperationException과 밀접하게 관련된 것은 NotSupportedException 예외입니다. 두 가지 예외는 쉽게 구별할 수 있습니다. InvalidOperationException은 "지금은 해당 작업을 수행할 수 없습니다"를 의미하고, NotSupportedException은 "이 클래스에서 해당 작업을 절대 수행할 수 없습니다"를 의미합니다. 이론적으로 NotSupportedException은 추상 인터페이스를 사용할 때만 발생해야 합니다.
예를 들어, 변경 불가능한 컬렉션은 IList.Add 메서드가 발생할 때 NotSupportedException을 발생시켜야 합니다. 이와 대조적으로, 고정 가능한 컬렉션은 고정 상태에서 이 메서드를 만나면 InvalidOperationException을 발생시킵니다.
NotSupportedException의 점점 더 중요해지는 하위 클래스는 PlatformNotSupportedException입니다. 이 예외는 작업이 일부 운영 환경에서는 수행될 수 있지만 다른 운영 환경에서는 수행될 수 없음을 나타냅니다. 예를 들어 .NET에서 UWP 또는 .NET Core로 코드를 이식할 때 이 예외를 사용해야 할 수 있습니다. 왜냐하면 .NET Framework의 모든 기능을 제공하지 않기 때문입니다.
알기 힘든 형식 예외
Microsoft는 .NET의 첫 번째 버전을 설계할 때 몇 가지 실수를 범했습니다. 예를 들어, 논리적으로 FormatException은 매개변수 예외 유형이며 문서에도 "이 예외는 매개변수 형식이 유효하지 않을 때 발생합니다"라고 나와 있습니다. 그러나 어떤 이유로든 실제로 ArgumentException을 상속하지 않습니다. 또한 매개변수 이름을 저장할 장소도 없습니다.
임시 제안은 FormatException을 발생시키는 것이 아니라 "ArgumentFormatException" 또는 유사한 효과를 갖는 다른 이름으로 이름을 지정할 수 있는 ArgumentException의 하위 클래스를 직접 만드는 것입니다. 이를 통해 매개변수 이름, 실제 사용된 값 등 필요한 정보를 제공할 수 있어 디버깅 시간이 단축됩니다.
이것은 "뛰어난 디자인"이라는 원래 주제로 다시 돌아옵니다. 예, 자체 개발 파서가 문제를 감지하면 FormatException을 발생시킬 수 있지만 이는 라이브러리를 사용하려는 애플리케이션 개발자에게 도움이 되지 않습니다.
이 프레임워크 설계 결함의 또 다른 예는 IndexOutOfRangeException입니다. 의미상으로는 ArgumentOutOfRangeException과 다르지 않지만 이 특별한 경우는 배열 인덱서에만 적용됩니까? 아니, 그렇게 생각하는 것은 잘못된 것입니다. IList.Item의 인스턴스 세트를 보면 이 메서드는 ArgumentOutOfRangeException만 발생시킵니다.
환경적 결함
환경적 결함은 데이터 다운타임, 웹 서버 무응답, 파일 손실 등 세상이 완벽하지 않다는 사실에서 비롯됩니다. 버그 보고서에 환경적 결함이 나타나면 고려해야 할 두 가지 측면이 있습니다.
애플리케이션이 결함을 올바르게 처리했습니까?
이 환경에서 결함이 발생하는 원인은 무엇입니까?
일반적으로 이는 분업을 포함합니다. 첫째, 애플리케이션 개발자는 자신의 질문에 대한 답을 가장 먼저 찾아야 합니다. 이는 오류를 처리하고 복구하는 것뿐만 아니라 유용한 로그를 생성하는 것을 의미합니다.
你可能想知道,为什么要从应用程序开发人员开始。应用程序开发人员要对运维团队负责。如果一次Web服务器调用失败,则应用程序开发人员不能只是甩手大叫“不是我的问题”。他或她首先需要确保异常提供了足够的细节信息,让运维人员可以开展他们的工作。如果异常仅仅提供了“服务器连接超时”的信息,那么他们怎么能知道涉及了哪台服务器?
专用异常
NotImplementedException
NotImplementedException表示且仅表示一件事:这项特性还在开发过程中。因此,NotImplementedException提供的信息应该总是包含一个任务跟踪软件的引用。例如:
throw new NotImplementedException("参见工单#42.");
你可以提供更详细的信息,但实际上,你记录的任何信息几乎立刻就会过期。因此,最好是只将读者导向工单,他们可以在那里看到诸如该特性按计划将会在何时实现这样的信息。
AggregateException
AggregateException是必要之恶,但很难使用。它本身不包含任何有价值的信息,所有的细节信息都隐藏在它的InnerExceptions集合中。
由于AggregateException通常只包含一个项,所以在库中将它解封装并返回真正的异常似乎是合乎逻辑的。一般来说,你不能在没有销毁原始堆栈跟踪的情况下再次抛出一个内部异常,但从.NET 4.5开始,该框架提供了使用ExceptionDispatchInfo的方法。
解封装AggregateException
catch (AggregateException ex) { if (ex.InnerExceptions.Count == 1) //解封装 ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); else throw; //我们真的需要AggregateException }
无法回答的情况
有一些异常无法简单地纳入这个主题。例如,AccessViolationException表示读取非托管内存时有问题。对,那可能是由原生库代码所导致的,也可能是由应用程序错误地使用了同样的代码库所导致的。只有通过研究才能揭示这个Bug的本质。
如果可能,你就应该在设计时避免无法回答的异常。在某些情况下,Visual Studio的静态代码分析器甚至可以分析该规则所涵盖的标识冲突。
例如,ApplicationException实际上已经废弃。Framework设计指南明确指出,“不要抛出或继承ApplicationException。”为此,应用程序不必抛出ApplicationException异常。虽说初衷如此,但看下下面这些子类:
Microsoft.JScript.BreakOutOfFinally
Microsoft.JScript.ContinueOutOfFinally
Microsoft.JScript.JScriptException
Microsoft.JScript.NoContextException
Microsoft.JScript.ReturnOutOfFinally
System.Reflection.InvalidFilterCriteriaException
System.Reflection.TargetException
System.Reflection.TargetInvocationException
System.Reflection.TargetParameterCountException
System.Threading.WaitHandleCannotBeOpenedException
显然,这些子类中有一些应该是参数异常,而其他的则表示环境问题。它们全都不是“应用程序异常”,因为他们只会被.NET Framework的库抛出。
同样的道理,开发人员不应该直接使用SystemException。同ApplicationException一样,SystemException的子类也是各不相同,包括ArgumentException、NullReferenceException和AccessViolationException。微软甚至建议忘掉SystemException的存在,而只使用其子类。
无法回答的情况有一个子类别,就是基础设施异常。我们已经看过AccessViolationException,以下是其他的基础设施异常:
CannotUnloadAppDomainException
BadImageFormatException
DataMisalignedException
TypeLoadException
TypeUnloadedException
这些异常通常很难诊断,可能会揭示出库或调用它的代码中存在的难以理解的Bug。因此,和ApplicationException不同,把它们归为无法回答的情况是合理的。
实践:重新设计SqlException
请记住这些原则,让我们看下SqlException。除了网络错误(你根本无法到达服务器)外,在SQL Server的master.dbo.sysmessages表中有超过11000个不同的错误代码。因此,虽然该异常包含了你需要的所有底层信息,但是,除了简单地捕获&记录外,你实际上难以做任何事。
如果我们要重新设计SqlException,那么我们会希望,根据我们期望用户或开发人员做什么,将其分解成多个不同的类别。
SqlClient.NetworkException会表示所有说明数据库服务器本身之外的环境存在问题的错误代码。
SqlClient.InternalException에는 서버의 심각한 오류(예: 데이터베이스 손상 또는 하드 디스크에 액세스할 수 없음)를 나타내는 오류 코드가 포함됩니다.
SqlClient.SyntaxException은 ArgumentException과 동일합니다. 이는 (직접적으로 또는 ORM 버그로 인해) 잘못된 SQL을 서버에 전달하고 있음을 의미합니다.
SqlClient.MissingObjectException은 구문은 올바르지만 데이터베이스 개체(테이블, 뷰, 저장 프로시저 등)가 존재하지 않는 경우에 발생합니다.
SqlClient.DeadlockException은 동일한 정보를 수정하려고 할 때 두 개 이상의 프로세스가 충돌할 때 발생합니다.
이러한 각 변칙은 조치 과정을 의미합니다.
SqlClient.NetworkException: 작업을 다시 시도합니다. 자주 발생하는 경우에는 운영 및 유지보수 담당자에게 문의하시기 바랍니다.
SqlClient.InternalException: 즉시 DBA에게 문의하세요.
SqlClient.SyntaxException: 애플리케이션 또는 데이터베이스 개발자에게 알립니다.
SqlClient.MissingObjectException: 운영 및 유지 관리 담당자에게 마지막 데이터베이스 배포에서 손실된 것이 있는지 확인하도록 요청하세요.
SqlClient.DeadlockException: 작업을 다시 시도합니다. 자주 발생하는 경우 설계 오류를 찾으십시오.
실제 작업에서 이 작업을 수행하려면 11,000개 이상의 SQL Server 오류 코드를 모두 해당 범주 중 하나에 매핑해야 하며 이는 특히 어려운 작업입니다. SqlException이 현재의 상태인 이유를 설명합니다.
요약
API를 설계할 때 문제 수정을 용이하게 하려면 수행해야 하는 작업 유형에 따라 예외를 구성해야 합니다. 이를 통해 자체 검사 코드를 더 쉽게 작성하고, 보다 정확한 로그를 유지하며, 문제를 적합한 사람이나 팀에 더 빠르게 전달할 수 있습니다.
저자 소개
Jonathan Allen은 1990년대 후반부터 진료실을 위한 MIS 프로젝트에 참여하기 시작하여 점차적으로 Access 및 Excel에서 엔터프라이즈 수준으로 승격되었습니다. 해결책. 그는 고급 사용자 인터페이스 개발로 전환하기로 결정하기 전에 금융 산업을 위한 자동화된 거래 시스템을 코딩하는 데 5년을 보냈습니다. 여가 시간에는 15세기부터 17세기까지의 서양 전투 기술을 연구하고 글을 쓰는 것을 좋아합니다.
위 내용은 .NET 예외 설계 원칙의 내용입니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(m.sbmmt.com)를 참고해주세요!