예외 객체를 던지는 대신 반환하는 합법적 인 이유가 있습니까?


45

이 질문은 예외 처리를 지원하는 모든 OO 프로그래밍 언어에 적용됩니다. 설명 목적으로 만 C #을 사용하고 있습니다.

일반적으로 코드가 즉시 처리 할 수 ​​없다는 문제가 발생 catch하면 다른 위치 (일반적으로 외부 스택 프레임) 의 절에서 발견되는 예외가 발생합니다 .

Q : 예외가 발생하지 않고 잡히지 않고 단순히 메서드에서 반환 된 다음 오류 개체로 전달되는 합법적 인 상황이 있습니까?

이 질문은 .NET 4의 System.IObserver<T>.OnError방법이 다음과 같이 제안 하기 때문에 나에게 제기되었습니다 . 예외가 오류 객체로 전달됩니다.

다른 시나리오 인 유효성 검사를 살펴 보겠습니다. 나는 일반적인 통념을 따르고 있으며 따라서 오류 객체 유형 IValidationErrorValidationException예기치 않은 오류를보고하는 데 사용되는 별도의 예외 유형 을 구별한다고 가정 해 봅시다 .

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

( System.Component.DataAnnotations네임 스페이스 는 상당히 비슷한 일을합니다.)

이러한 유형은 다음과 같이 채택 될 수 있습니다.

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

이제 궁금합니다. 위를 위와 같이 단순화 할 수 없었습니다.

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

Q : 이 두 가지 접근 방식의 장단점은 무엇입니까?


두 가지 별도의 답변을 찾고 있다면 두 가지 별도의 질문을해야합니다. 그것들을 결합하면 대답하기가 훨씬 더 어려워집니다.
Bobson

2
@Bobson :이 두 가지 질문은 상호 보완 적이라고 생각합니다. 앞의 질문은 문제의 일반적인 맥락을 광범위하게 정의하는 반면, 후자는 독자가 자신의 결론을 도출 할 수있는 구체적인 정보를 요구합니다. 답변은 두 질문에 대해 별도의 명시 적 답변을 제공 할 필요는 없습니다. "중간"에 대한 답변도 환영합니다.
stakx

5
확실하지 않습니다 : 예외를 던지지 않을 경우 Exception에서 ValidationError를 발생시키는 이유는 무엇입니까?
pdr

@ pdr : AggregateException대신 던지는 경우를 의미 합니까? 좋은 지적.
stakx

3
실제로는 아닙니다. 던져 버릴 경우 예외를 사용하십시오. 반환하려면 오류 개체를 사용하십시오. 실제로 오류 인 예외를 예상 한 경우 예외를 잡아서 오류 개체에 넣습니다. 여러 오류 중 하나를 허용하는지 여부는 전적으로 관련이 없습니다.
pdr

답변:


31

예외가 발생하지 않고 잡히지 않고 단순히 메서드에서 반환 된 다음 오류 개체로 전달되는 합법적 인 상황이 있습니까?

결코 던져지지 않으면 예외가 아닙니다. derived동작을 따르지 않지만 Exception 클래스 의 객체 입니다. 그것을 예외라고 부르는 것은 순전히 의미 론적이지만, 그것을 던지지 않는 데 아무런 문제가 없습니다. 내 관점에서 예외를 던지지 않는 것은 내부 함수가이를 포착하여 전파하는 것을 방지하는 것과 똑같습니다.

함수가 예외 객체를 반환하는 것이 유효합니까?

물론. 다음은 이것이 적절한 예의 간단한 목록입니다.

  • 예외 공장.
  • 사용 가능한 예외로 이전 오류가 있었는지보고하는 컨텍스트 객체입니다.
  • 이전에 발견 된 예외를 유지하는 함수입니다.
  • 내부 유형의 예외를 생성하는 타사 API입니다.

그것을 던지지 않습니까?

예외를 던지는 것은 "감옥에 가십시오. 가십시오!" 보드 게임 독점에서. 컴파일러는 해당 소스 코드를 실행하지 않고 모든 소스 코드를 catch로 건너 뛰도록 컴파일러에 지시합니다. 오류, 버그보고 또는 중지와는 아무런 관련이 없습니다. 함수에 대한 "슈퍼 리턴"문으로 예외를 던지는 것을 생각하고 싶습니다. 원래 호출자보다 훨씬 높은 곳으로 코드 실행을 반환합니다.

여기서 중요한 것은 예외의 진정한 가치가 try/catch예외 객체의 인스턴스화가 아니라 패턴 에 있다는 것을 이해하는 것입니다 . 예외 개체는 상태 메시지 일뿐입니다.

귀하의 질문 에이 두 가지 사용법을 혼동하는 것 같습니다 : 예외 처리기로 점프하고 예외가 나타내는 오류 상태. 오류 상태를 취하여 예외로 래핑했다고해서 try/catch패턴이나 그 이점을 따르는 것은 아닙니다 .


4
"그것은 결코 던져지지 않으면 예외는 아니다. 그것은 Exception클래스 에서 파생 된 객체 이지만 동작을 따르지 않는다"고 말했다. -그리고 그것은 정확히 문제입니다 : Exception객체 생성 자나 호출 메소드에 의해 전혀 발생하지 않는 상황에서 파생 된 객체 를 사용하는 것이 합법적인가요? 여기서 "슈퍼 리턴"제어 흐름없이 "상태 메시지"만 전달 되는가? 아니면 내가 무언의 위반입니다 try/ catch(Exception)나는이 작업을 수행하지 않으며, 대신 별도의 비 사용 안 너무 심하게 계약 Exception유형을?
stakx

10
@stakx 프로그래머는 다른 프로그래머에게는 혼란 스럽지만 기술적으로 잘못된 것은 아닙니다. 그것이 내가 의미론이라고 말한 이유입니다. 법적 답변은 No계약을 위반하지 않는다는 것입니다. 은 popular opinion당신이 그렇게해서는 안 될 것이다. 프로그래밍은 소스 코드가 아무 문제가 없지만 모두가 싫어하는 예제로 가득합니다. 의도가 무엇인지 명확하게 알 수 있도록 항상 소스 코드를 작성해야합니다. 그런 식으로 예외를 사용하여 다른 프로그래머를 실제로 돕고 있습니까? 그렇다면, 그렇게하십시오. 그렇지 않으면 싫어하는 경우 놀라지 마십시오.
Reactgular

@cgTag 기울임 꼴로 인라인 코드 블록을 사용하면 뇌가 아프게됩니다.
Frayt

51

예외를 throw하는 대신 예외를 반환하면 상황을 분석하고 호출자가 해당 예외를 반환 할 수있는 도우미 메서드가있는 경우 의미가 의미가 있습니다 ( "예외 팩토리"라고 할 수 있음). 이 오류 분석기 기능에서 예외를 발생시키는 것은 분석 자체에서 문제가 발생한 것을 의미하며 예외를 반환하면 오류의 종류가 성공적으로 분석되었음을 의미합니다.

하나의 가능한 사용 사례는 HTTP 응답 코드 를 예외로 바꾸는 함수일 수 있습니다 .

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

참고 던지는 방법이 잘못 사용되거나 동안 내부 오류가 발생한 것을 예외 수단 반환 오류 코드가 성공적으로 확인 된 것을 예외 수단.


또한 컴파일러가 코드 흐름을 이해하는 데 도움이됩니다. 메서드가 "바람직한"예외를 발생시키기 때문에 올바르게 반환되지 않고 컴파일러가이를 알지 못하는 return경우 컴파일러가 더 이상 불만을 제기 하지 않도록 불필요한 명령문이 필요할 수 있습니다 .
Joachim Sauer

@JoachimSauer 그래. 한 번 이상 C #에 "never"라는 반환 유형을 추가하기를 원합니다. 함수를 던져서 종료해야 함을 나타냅니다. 때로는 유일한 작업이 예외를 일으키는 코드가 있습니다.
Loren Pechtel

2
@LorenPechtel 반환하지 않는 함수는 함수가 아닙니다. 터미네이터입니다. 이와 같은 함수를 호출하면 호출 스택이 지워집니다. 전화하는 것과 비슷합니다 System.Exit(). 함수 호출 후 코드는 실행되지 않습니다. 그것은 함수에 대한 좋은 디자인 패턴처럼 들리지 않습니다.
Reactgular

5
@MathewFoscarini 비슷한 방법으로 오류가 발생할 수있는 여러 메서드가있는 클래스를 고려하십시오. 예외 상황의 일부로 일부 상태 정보를 덤프하여 붐이 발생했을 때 무엇이 ​​씹 었는지 표시하려고합니다. DRY로 인해 예선 코드의 사본 하나를 원합니다. 그러면 예쁘게 만드는 함수를 호출하고 결과를 던지기에 사용하거나 미리 작성된 텍스트를 작성한 다음 던지는 함수를 의미합니다. 나는 일반적으로 후자가 우월하다고 생각합니다.
Loren Pechtel

1
@LorenPechtel 아, 그래 나도 그렇게 했어. 알았어 이제 이해 했어
Reactgular

5

개념적으로 예외 개체가 작업의 예상 결과이면 예입니다. 그러나 내가 생각할 수있는 경우에는 항상 특정 시점에서 스로 잡기가 필요합니다.

  • "Try"패턴의 변형 (메소드가 두 번째 예외 발생 메소드를 캡슐화하지만 예외를 포착하고 대신 성공을 나타내는 부울을 리턴 함). 부울을 반환하는 대신 throw 된 예외 (성공한 경우 null)를 반환하여 catch-less 성공 또는 실패 표시를 유지하면서 최종 사용자에게 더 많은 정보를 제공 할 수 있습니다.

  • 워크 플로우 오류 처리 실제로 "패턴 시도"메소드 캡슐화와 유사하게, 일련의 명령 패턴으로 워크 플로우 단계를 추상화 할 수 있습니다. 체인의 링크에서 예외가 발생하는 경우 추상 객체에서 해당 예외를 포착 한 다음 해당 작업의 결과로 반환하여 워크 플로 엔진과 실제 코드가 워크 플로 단계로 실행되므로 광범위한 시도가 필요하지 않습니다. -자신의 논리를 잡으십시오.


주목할만한 사람들이 "Try"패턴에서 이러한 변형을 옹호하는 것을 보지 못했지만 부울을 반환하는 "권장"접근 방식은 매우 빈약 한 것으로 보입니다. 속성 "성공", 메서드 및 메서드 ( 널이 아닌 경우 예외를 throw 함)를 OperationResult가진 추상 클래스 를 갖는 것이 더 좋을 것이라고 생각합니다 . 갖는 속성이 많은 말을하는 것이 유용 없었다 경우에 대한 고정 불변의 오류 객체의 사용을 허용 것보다는 방법합니다. boolGetExceptionIfAnyAssertSuccessfulGetExceptionIfAnyGetExceptionIfAny
supercat

5

일부 스레드 풀에서 실행되도록 일부 작업을 대기열에 추가했다고 가정하겠습니다. 이 작업에서 예외가 발생하면 다른 스레드이므로 잡을 수 없습니다. 실행 된 스레드는 그냥 죽습니다.

이제 무언가 (해당 작업의 코드 또는 스레드 풀 구현)가 해당 예외를 작업과 함께 저장하고 작업이 완료된 것으로 간주합니다 (불필요). 이제 작업이 완료되었는지 여부를 묻거나 스레드에서 다시 던질 수 있는지 또는 스레드에서 다시 던질 수 있는지 (또는 원본으로 원인이있는 새로운 예외) 질문 할 수 있습니다.

수동으로 수행하면 새 예외를 작성하여 던지고 잡은 다음 다른 스레드에서 던지기, 잡기 및 반응을 검색하여 저장하고 있음을 알 수 있습니다. 따라서 던지기와 잡기를 건너 뛰고 저장하고 작업을 마친 다음 예외가 있는지 묻고 반응하는 것이 합리적입니다. 그러나 같은 장소에서 실제로 예외가 발생할 수 있으면 더 복잡한 코드가 발생할 수 있습니다.

추신 : 이것은 Java에 대한 경험으로 작성되었으며 예외를 만들 때 스택 추적 정보가 생성됩니다 (던질 때 C #이 생성되는 것과 달리). 따라서 자바에서 예외 접근 방식은 C #보다 느리게 처리되지만 (사전 생성 및 재사용되지 않는 한) 스택 추적 정보를 사용할 수 있습니다.

일반적으로 프로파일 링 후 성능 최적화가 병목 현상을 지적하지 않는 한 예외를 작성하지 말고 절대 예외를 피하십시오. 적어도 예외 생성이 비싼 Java에서는 (스택 추적). C #에서는 가능하지만 IMO는 놀랍기 때문에 피해야합니다.


이것은 종종 오류 리스너 객체에서도 발생합니다. 대기열은 작업을 실행하고 예외를 포착 한 다음 해당 예외를 담당 오류 리스너에게 전달합니다. 이 경우 작업에 대해 잘 모르는 작업 스레드를 종료하는 것은 일반적으로 의미가 없습니다. 아이디어의이 종류는 하나 개의 스레드가 서로 예외를 전달받을 수있는 자바 API에 내장되어 있습니다 : docs.oracle.com/javase/1.5.0/docs/api/java/lang/...
랜스 Nanek에게

1

그것은 당신의 디자인에 달려 있으며, 일반적으로 호출자에게 예외를 반환하지 않을 것입니다. 일반적으로 코드는 조기에 빠르게 실패하도록 작성되었습니다. 예를 들어 파일을 열고 처리하는 경우 (C # PsuedoCode)를 고려하십시오.

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

이 경우 레코드가 잘못되면 오류가 발생하여 파일 처리가 중지됩니다.

그러나 요구 사항은 하나 이상의 라인에 오류가 있어도 파일 처리를 계속하고 싶다고 말합니다. 코드는 다음과 같이 보일 수 있습니다.

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

따라서이 경우 예외를 다시 반환하지는 않지만 예외가 포착되어 이미 처리되었으므로 일종의 오류 객체가 발생하지만 예외를 호출자에게 다시 반환합니다. 이것은 예외를 돌려주는 것에 해를 끼치 지 않지만, 예외 객체는 그 행동이 있기 때문에 바람직하지 않은 예외를 다시 던질 수 있습니다. 호출자가 다시 던질 수있는 기능을 원하면 예외를 리턴하고, 그렇지 않으면 예외 오브젝트에서 정보를 가져 와서 더 작은 경량 오브젝트를 구성하여 대신 리턴하십시오. 빠른 실패는 일반적으로 더 깨끗한 코드입니다.

유효성 검사 예제의 경우 유효성 검사 오류가 일반적 일 수 있으므로 예외 클래스에서 상속하지 않거나 예외를 throw하지 않을 수 있습니다. 사용자의 50 %가 첫 번째 시도에서 양식을 올바르게 작성하지 못하면 예외를 던지기보다는 객체를 반환하는 것이 오버 헤드가 줄어 듭니다.


1

Q : 예외가 발생하지 않고 잡히지 않고 단순히 메서드에서 반환 된 다음 오류 개체로 전달되는 합법적 인 상황이 있습니까?

예. 예를 들어, 최근에 ASMX 웹 서비스에서 발생한 예외에 결과 SOAP 메시지에 요소가 포함되어 있지 않아서이를 생성해야하는 상황이 발생했습니다.

예시 코드 :

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function

1

대부분의 언어 (afaik)에서는 예외가 추가로 제공됩니다. 대부분 현재 스택 추적의 스냅 샷이 객체에 저장됩니다. 이것은 디버깅에는 좋지만 메모리에 영향을 줄 수 있습니다.

@ThinkingMedia가 이미 말했듯이 실제로 예외를 오류 객체로 사용하고 있습니다.

코드 예제에서는 주로 코드 재사용 및 구성을 피하기 위해이 작업을 수행하는 것 같습니다. 개인적으로 이것이 이것이 좋은 이유라고 생각하지 않습니다.

또 다른 가능한 이유는 스택 추적이있는 오류 객체를 제공하도록 언어를 속이는 것입니다. 이것은 오류 처리 코드에 추가 컨텍스트 정보를 제공합니다.

반면에 스택 추적을 유지하는 데 메모리 비용이 있다고 가정 할 수 있습니다. 예를 들어 이러한 개체를 어딘가에 모으기 시작하면 불쾌한 메모리 영향이 나타날 수 있습니다. 물론 이것은 주로 언어 / 엔진에서 예외 스택 추적이 어떻게 구현되는지에 달려 있습니다.

좋은 생각입니까?

지금까지 나는 그것이 사용되거나 권장되는 것을 보지 못했습니다. 그러나 이것은별로 의미가 없습니다.

문제는 : 스택 추적이 오류 처리에 실제로 유용합니까? 또는 더 일반적으로 개발자 나 관리자에게 어떤 정보를 제공 하시겠습니까?

일반적인 throw / try / catch 패턴은 스택 트레이스가 필요합니다. 반환 된 객체의 경우 항상 호출 된 함수에서 가져옵니다. 스택 추적에는 개발자가 필요로하는 모든 정보가 포함될 수 있지만 덜 무겁고 구체적인 정보로 충분할 수 있습니다.


-4

대답은 '예'입니다. 참조 http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions을 예외에 구글의 C ++ 스타일 가이드에 대한 화살표를 엽니 다. 그들이 결정을 내리는 데 사용했던 주장을 볼 수 있지만, 다시 결정해야한다면 아마도 결정했을 것입니다.

그러나 Go 언어는 비슷한 이유로 관용적으로 예외를 사용하지 않습니다.


1
죄송하지만이 가이드 라인이 질문에 답변하는 데 어떻게 도움이되는지 모르겠습니다. 예외 처리를 완전히 피하는 것이 좋습니다. 이것은 내가 요구하는 것이 아닙니다. 내 질문은 결과 예외 개체가 발생하지 않는 상황에서 오류 유형을 설명하기 위해 예외 유형을 사용하는 것이 합법적인지 여부입니다. 이 가이드 라인에는 이에 관한 내용이 없습니다.
stakx
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.