Checked vs Unchecked vs No Exception… 반대 신념의 모범 사례


10

시스템이 예외를 올바르게 전달하고 처리하는 데 필요한 많은 요구 사항이 있습니다. 개념을 구현하기 위해 언어를 선택할 수있는 옵션이 많이 있습니다.

예외 요구 사항 (특별한 순서는 아님) :

  1. 문서화 : 언어에는 API에서 발생할 수있는 예외를 문서화 할 수단이 있어야합니다. 이상적으로이 설명서 매체는 컴파일러와 IDE가 프로그래머를 지원할 수 있도록 기계로 사용할 수 있어야합니다.

  2. 예외적 인 상황 전송 :이 기능은 피 호출 기능이 예상되는 동작을 수행하지 못하게하는 상황을 전달할 수있는 기능입니다. 제 생각에는 그러한 상황에는 세 가지 큰 범주가 있습니다.

    2.1 일부 데이터가 유효하지 않은 코드 버그.

    2.2 구성 또는 기타 외부 리소스 문제.

    2.3 본질적으로 신뢰할 수없는 리소스 (네트워크, 파일 시스템, 데이터베이스, 최종 사용자 등) 신뢰할 수없는 특성으로 인해 산발적 인 실패를 예상해야하기 때문에 이는 약간의 모퉁이 사례입니다. 이 경우 이러한 상황은 예외적 인 것으로 간주됩니까?

  3. 코드가 처리 할 수 있도록 충분한 정보를 제공 하십시오 . 예외는 수신자에게 상황에 대응하고 처리 할 수 ​​있도록 충분한 정보를 제공해야합니다. 또한이 예외가 기록 될 때 프로그래머가 문제의 진술을 식별하고 격리하고 솔루션을 제공 할 수있는 충분한 컨텍스트를 제공 할 수 있도록 정보가 충분해야합니다.

  4. 프로그래머에게 코드 실행 상태의 현재 상태에 대한 자신감을 제공하십시오 . 소프트웨어 시스템의 예외 처리 기능은 프로그래머가 방해하지 않고 필요한 안전 장치를 제공 할 수있을만큼 충분히 있어야합니다. 손.

이를 다루기 위해 다음 방법이 다양한 언어로 구현되었습니다.

  1. 확인 된 예외 예외 를 문서화하는 좋은 방법을 제공하며 이론적으로 올바르게 구현되면 모든 것이 양호하다는 충분한 확신을 제공해야합니다. 그러나 비용은 많은 사람들이 예외를 삼키거나 단순히 확인되지 않은 예외로 다시 던져서 단순히 우회하는 것이 더 생산적이라고 느끼게합니다. 부적절하게 검사 된 예외를 사용하면 유용성이 거의 없어집니다. 또한 확인 된 예외로 인해 시간이 안정적인 API를 작성하기가 어렵습니다. 특정 도메인 내에서 일반 시스템을 구현하면 예외적으로 확인 된 예외를 사용하여 유지 관리하기 어려운 예외적 인 상황이 발생합니다.

  2. 확인되지 않은 예외 -확인 된 예외보다 훨씬 더 다양한 기능은 주어진 구현의 예외 상황을 올바르게 문서화하지 못합니다. 그들은 임시 문서에 의존합니다. 이로 인해 매체의 신뢰할 수없는 특성이 안정성으로 보이는 API로 마스킹되는 상황이 발생합니다. 또한 이러한 예외가 발생하면 추상화 계층을 통해 다시 올라감에 따라 그 의미가 풀립니다. 문서화가 잘되어 있지 않기 때문에 프로그래머는 구체적으로 목표를 정할 수 없으며, 보조 시스템이 고장 나더라도 전체 시스템을 중단시키지 않도록하기 위해 필요한 것보다 훨씬 더 넓은 네트워크를 캐스팅해야하는 경우가 종종 있습니다. 제공된 삼키는 문제를 확인한 예외로 되돌아갑니다.

  3. 다중 상태 반환 유형 여기서는 예상치 못한 결과 또는 예외를 나타내는 객체를 반환하기 위해 분리 세트, 튜플 또는 기타 유사한 개념에 의존합니다. 여기에서 스택 해제, 코드 절단 없음, 모든 것이 정상적으로 실행되지만 반환 값은 계속하기 전에 오류가 있는지 확인해야합니다. 나는 실제로 이것으로 작업하지 않았기 때문에 경험에서 언급 할 수 없다. 나는 정상적인 흐름을 우회하는 예외 문제를 해결한다고 인정하지만 여전히 피곤하고 지속적으로 "당신의 얼굴에있는"체크 예외와 같은 문제로 고통받을 것입니다.

그래서 질문은 :

이 문제에 대한 당신의 경험은 무엇이며, 당신에 따르면 언어가 가질 수있는 훌륭한 예외 처리 시스템을 만드는 가장 좋은 후보자는 무엇입니까?


편집 :이 질문을 쓴 후 몇 분 후에 나는 이 게시물 을 보았습니다.


2
"그것은 피곤하고 지속적으로 당신의 얼굴에 확인 된 예외와 같은 문제를 겪게 될 것입니다": 실제로 : 적절한 언어 지원을 사용하지 않고 "성공 경로"를 프로그래밍해야합니다. 오류.
Giorgio

"언어는 API가 던질 수있는 예외를 문서화 할 수단이 있어야합니다." -weeeel. C ++에서 "우리"는 이것이 실제로 작동하지 않는다는 것을 알게되었습니다. 실제로 유용하게 할 수있는 것은 API가 예외를 throw 수 있는지 여부를 나타내는 것 입니다. (긴 이야기를 짧게 자르지 만 noexceptC ++로 이야기를 보면 C # 및 Java에서도 EH에 대한 훌륭한 통찰력을 얻을 수 있다고 생각 합니다.)
Martin Ba

답변:


10

C ++ 초기에 우리는 일종의 일반 프로그래밍 없이는 강력한 유형의 언어가 매우 다루기 힘들다는 것을 발견했습니다. 또한 확인 된 예외와 일반 프로그래밍이 함께 작동하지 않으며 확인 된 예외는 본질적으로 버려졌습니다.

다중 집합 반환 유형은 훌륭하지만 예외를 대체하지는 않습니다. 예외없이 코드에는 오류 검사 소음이 가득합니다.

확인 된 예외의 다른 문제는 하위 수준 함수에 의해 발생 된 예외의 변경으로 인해 모든 호출자와 호출자의 변경이 연속적으로 발생한다는 것입니다. 이를 방지하는 유일한 방법은 각 수준의 코드가 하위 수준에서 발생하는 예외를 잡아서 새 예외로 감싸는 것입니다. 다시 말하지만, 매우 시끄러운 코드로 끝납니다.


2
제네릭은 OO 패러다임에 대한 언어 지원의 제한으로 인해 발생하는 모든 종류의 오류를 해결하는 데 도움이됩니다. 그럼에도 불구하고 대안은 대부분 오류 검사를 수행하거나 아무것도 잘못되기를 희망하는 코드를 가지고있는 것 같습니다. 당신은 당신의 얼굴에 끊임없이 예외적 인 상황이 있거나 중간에 큰 나쁜 늑대를 떨어 뜨릴 때 끔찍한 흰 토끼의 꿈의 땅에 살고 있습니다!
Newtopian

3
계단식 문제의 경우 +1 변경을 어렵게 만드는 모든 시스템 / 아키텍처는 저자가 생각한 디자인에 관계없이 원숭이 패치 및 지저분한 시스템으로 이어집니다.
Matthieu M.

2
@Newtopian : 템플릿은 일반 컨테이너에 정적 유형 안전을 제공하는 등 엄격한 객체 방향으로 수행 할 수없는 작업을 수행합니다.
David Thornley

2
"체크 된 예외"라는 개념을 가지고 있지만 Java와는 매우 다른 예외 시스템을보고 싶습니다. Checked-ness는 예외 유형 의 속성이 아니라 사이트, 캐치 사이트 및 예외 인스턴스를 처리해야합니다. 메소드가 점검 된 예외를 throw하는 것으로 보급 된 경우 두 가지 영향을 미칩니다. (1) 함수는 리턴시 특별한 무언가를 수행하여 점검 된 예외의 "throw"를 처리해야합니다 (예 : 캐리 플래그 설정 등). 정확한 플랫폼) 호출 코드를 준비해야합니다.
supercat

7
"예외 없이는 코드에 오류 검사 소음이 가득합니다.": 확실하지 않습니다. Haskell에서는이를 위해 모나드를 사용할 수 있으며 모든 오류 검사 소음이 사라졌습니다. "멀티 스테이트 리턴 타입"에 의해 발생되는 노이즈는 솔루션 자체보다 프로그래밍 언어의 한계입니다.
Giorgio

9

오랫동안 OO 언어에서는 예외를 사용하는 것이 사실상 오류를 전달하는 표준이었습니다. 그러나 기능적 프로그래밍 언어는 Scott Wlaschin이 설명한 것처럼 모나드 (사용하지 않은) 또는보다 가벼운 "철도 지향 프로그래밍"을 사용하는 것과 같은 다른 접근 방법을 제공합니다.

실제로 다중 상태 결과 유형의 변형입니다.

  • 함수는 성공 또는 오류를 반환합니다. 튜플의 경우와 같이 둘 다 반환 할 수 없습니다.
  • 가능한 모든 오류가 간결하게 문서화되었습니다 (적어도 F #에서는 결과 조합이 차별적 조합으로 표시됨).
  • 호출자가 결과가 성공 또는 실패 인 경우 고려하지 않고 결과를 사용할 수 없습니다.

결과 유형은 다음과 같이 선언 될 수 있습니다.

type Result<'TSuccess,'TFail> =
| Success of 'TSuccess
| Fail of 'TFail

따라서이 유형을 반환하는 함수의 결과는 a Success또는 Failtype입니다. 둘 다 될 수는 없습니다.

보다 명령 지향적 인 프로그래밍 언어에서 이러한 종류의 스타일은 호출자 사이트에 많은 양의 코드가 필요할 수 있습니다. 그러나 함수형 프로그래밍을 사용하면 바인딩 함수 또는 연산자를 구성하여 여러 함수를 함께 묶을 수 있으므로 오류 검사가 코드의 절반을 차지하지 않습니다. 예로서:

// Create an updateUser function that takes an id, and new state
// as input, and updates an existing user.
let updateUser id input =
    validateInput input
    >>= loadUser id
    >>= updateUser input
    >>= saveUser id
    >>= notifyAboutUserUpdated

updateUser함수는 이러한 각 함수를 연속적으로 호출하며 각 함수는 실패 할 수 있습니다. 모두 성공하면 마지막으로 호출 된 함수의 결과가 반환됩니다. 함수 중 하나가 실패하면 해당 함수의 결과가 전체 updateUser함수 의 결과가 됩니다. 이것은 모두 custom >> = 연산자에 의해 처리됩니다.

위의 예에서 오류 유형은

type UserValidationErrorType =
| InvalidEmail of string
| MissingFirstName of string
... etc

type DbErrorType =
| RecordNotFound of int
| ConcurrencyError of int

type UpdateUserErrorType =
| InvalidInput of UserValidationErrorType
| DbError of DbErrorType

호출자가 updateUser함수에서 가능한 모든 오류를 명시 적으로 처리하지 않으면 컴파일러에서 경고를 발행합니다. 그래서 모든 것이 문서화되었습니다.

Haskell do에는 코드를 더 깨끗하게 만들 수 있는 표기법이 있습니다.


2
아주 좋은 답변과 참고 자료 (철도 지향 프로그래밍), +1. Haskell의 do표기법 을 언급 하면 결과 코드가 더 깨끗해집니다.
조르지오

1
@Giorgio-지금은 해냈지만 F # 만 Haskell과 함께 일하지 않았으므로 실제로 그것에 대해 많이 쓸 수 없었습니다. 그러나 원하는 경우 답변에 추가 할 수 있습니다.
피트

고마워, 나는 작은 예를 썼지 만 대답에 추가 할만 큼 작지 않았기 때문에 완전한 답을 썼다 (일부 배경 정보가 있음).
조르지오

2
Railway Oriented Programming정확히 모나드 동작입니다.
데니스

5

내가 찾을 수 피트의 대답은 아주 좋은 나는 몇 가지 고려 사항과 하나의 예를 추가하고 싶습니다. 예외 사용과 특수 오류 값 반환에 관한 매우 흥미로운 논의 는 표준 ML 프로그래밍, Robert Harper ( 243 페이지 243, 244 쪽)에서 찾을 수 있습니다 .

문제는 f어떤 유형의 값을 반환하는 부분 함수를 구현하는 것 t입니다. 한 가지 해결책은 함수가 유형을 갖도록하는 것입니다

f : ... -> t

가능한 결과가 없으면 예외를 던집니다. 두 번째 해결책은 유형으로 함수를 구현하는 것입니다

f : ... -> t option

SOME v성공과 NONE실패로 돌아갑니다 .

다음은이 책의 텍스트이며, 텍스트를보다 일반적으로 만들기 위해 약간만 수정 한 것입니다 (책은 특정 예를 나타냄). 수정 된 텍스트는 이탤릭체로 작성됩니다 .

두 솔루션 간의 상충 관계는 무엇입니까?

  1. 옵션 유형을 기반으로 한 솔루션은 기능 유형 f에서 실패 가능성을 명시합니다 . 이렇게하면 호출 결과에 대한 사례 분석을 사용하여 프로그래머가 명시 적으로 실패를 테스트해야합니다. 타입 체커는 t optionat 가 예상되는 곳에서 사용할 수 없도록합니다 . 예외 기반 솔루션은 해당 유형의 실패를 명시 적으로 나타내지 않습니다. 그럼에도 불구하고 프로그래머는 실패를 처리해야합니다. 그렇지 않으면 컴파일 타임이 아니라 런타임에 포착되지 않은 예외 오류가 발생합니다.
  2. 옵션 유형을 기반으로하는 솔루션에는 각 호출 결과에 대한 명확한 사례 분석이 필요합니다. "가장 많은"결과가 성공하면 검사가 중복되어 너무 많은 비용이 듭니다. 예외를 기반으로하는 솔루션에는 이러한 오버 헤드 t가 없습니다 . 결과 를 전혀 반환하지 않는 "실패"사례가 아니라 "정상"사례를 반환하는 경향이 있습니다 . 예외 구현은 실패와 성공에 비해 드문 경우 처리기 사용이 명시 적 사례 분석보다 효율적임을 보장합니다.

[cut] 일반적으로 효율성이 가장 중요한 경우, 실패가 드문 경우 예외를 선호하고 실패가 비교적 일반적인 경우에는 옵션을 선호하는 경향이 있습니다. 반면에 정적 검사가 가장 중요한 경우, 유형 검사기는 오류가 런타임에만 발생하지 않고 프로그래머가 오류를 검사해야한다는 요구 사항을 적용하므로 옵션을 사용하는 것이 유리합니다.

예외와 옵션 반환 유형 중 하나를 선택해야합니다.

반환 유형의 오류를 나타내는 아이디어는 코드 전체에 오류 검사가 적용된다는 사실과 관련이 있습니다. 다음은 Haskell의 작은 예입니다.

두 숫자를 파싱하고 첫 번째 숫자를 두 번째 숫자로 나누고 싶다고 가정 해보십시오. 따라서 각 숫자를 구문 분석하는 동안 또는 나누기 (0으로 나누기) 중에 오류가 발생할 수 있습니다. 따라서 각 단계 후에 오류를 확인해야합니다.

import Text.Read

parseInt :: String -> Maybe Int
parseInt s = readMaybe s :: Maybe Int

safeDiv :: Int -> Int -> Maybe Int
safeDiv n d = if d /= 0 then Just (n `div` d) else Nothing

toString :: Maybe Int -> String
toString (Just i) = show i
toString Nothing  = "error"

main = do
         -- Get two lines from the terminal.
         nStr <- getLine
         dStr <- getLine

         -- Parse each string and divide.
         let r = do n <- parseInt nStr
                    d <- parseInt dStr
                    safeDiv n d

         -- Print the result.
         putStrLn $ toString r

구문 분석 및 나누기는 let ...블록 에서 수행됩니다 . Maybe모나드와 do표기법 을 사용하면 성공 경로 만 지정됩니다. Maybe모나드 의 시맨틱은 오류 값 ( Nothing)을 암시 적으로 전파합니다 . 프로그래머에게는 오버 헤드가 없습니다.


2
유용한 오류 메시지를 인쇄하려는 경우 이와 같은 경우 Either유형이 더 적합 할 것이라고 생각합니다. Nothing여기에 오면 어떻게합니까? "오류"메시지가 나타납니다. 디버깅에별로 도움이되지 않습니다.
사라

1

Checked Exceptions의 열렬한 팬이되었으며 사용 시점에 대한 일반적인 규칙을 공유하고 싶습니다.

내 코드에서 처리 해야하는 기본적으로 두 가지 유형의 오류가 있다는 결론에 도달했습니다. 코드가 실행되기 전에 테스트 할 수있는 오류가 있으며 코드가 실행되기 전에 테스트 할 수없는 오류가 있습니다. 코드가 NullPointerException에서 실행되기 전에 테스트 할 수있는 오류에 대한 간단한 예입니다.

//... bad code below.  the runnable variable
// tries to call the run() method before the variable
// is instantiated.  Running the code below will cause
// a NullPointerException.
Runnable runnable = null;
runnable.run();

간단한 테스트는 다음과 같은 오류를 피할 수 있습니다 ...

Runnable runnable = null;
...
if (runnable != null)
{   runnable.run(); }

안전을 보장하기 위해 코드를 실행하기 전에 하나 이상의 테스트를 실행할 수있는 컴퓨팅 환경이 있으며 여전히 예외가 발생합니다. 예를 들어, 데이터를 드라이브에 쓰기 전에 파일 시스템을 테스트하여 하드 드라이브에 충분한 디스크 공간이 있는지 확인하십시오. 오늘날 사용되는 것과 같은 멀티 프로세싱 운영 체제에서 프로세스는 디스크 공간을 테스트 할 수 있고 파일 시스템은 충분한 공간이 있음을 나타내는 값을 리턴 한 다음 다른 프로세스로의 컨텍스트 전환은 운영에 사용 가능한 나머지 바이트를 쓸 수 있습니다. 체계. 운영 체제 컨텍스트를 내용을 디스크에 쓰는 실행중인 프로세스로 다시 전환하면 파일 시스템에 디스크 공간이 충분하지 않기 때문에 예외가 발생합니다.

위의 시나리오는 Checked Exception의 완벽한 사례로 간주합니다. 코드를 완벽하게 작성할 수는 있지만 나쁜 것을 처리하도록 강요하는 코드의 예외입니다. '예외 삼키기'와 같은 나쁜 일을하기로 선택한다면, 당신은 나쁜 프로그래머입니다. 그건 그렇고, 나는 예외를 삼키는 것이 합리적인 경우를 발견했지만 예외가 삼킨 이유에 대한 의견을 코드에 남겨주십시오. 예외 처리 메커니즘은 책임이 없습니다. 나는 보통 심장 박동기를 예외 확인이있는 언어로 작성하는 것을 선호한다고 농담합니다.

코드의 테스트 가능 여부를 결정하기가 어려운 경우가 있습니다. 예를 들어, 인터프리터를 작성 중이고 구문상의 이유로 코드가 실행되지 않을 때 SyntaxException이 발생하는 경우 SyntaxException이 Checked Exception 또는 (Java) RuntimeException이어야합니까? 코드가 실행되기 전에 인터프리터가 코드의 구문을 확인하면 예외가 RuntimeException이어야합니다. 인터프리터가 단순히 코드 'hot'을 실행하고 구문 오류를 발견하면 예외는 Checked Exception이어야한다고 말하고 싶습니다.

해야 할 일이 확실하지 않은 시간이 있기 때문에 Checked Exception을 잡거나 던져야하는 것이 항상 기쁘지 않다는 것을 인정합니다. Checked Exceptions는 프로그래머가 발생할 수있는 잠재적 인 문제를 염두에 두는 방법입니다. Java로 프로그래밍하는 이유 중 하나는 예외 확인이 있기 때문입니다.


1
차라리 심장 박동 조절 장치는 예외가 전혀없는 언어로 작성되었으며 모든 코드 줄은 리턴 코드를 통해 오류를 처리했습니다. 예외가 발생하면 "모두 잘못되었다"고 말하고 처리를 계속할 수있는 유일한 안전한 방법은 중지했다가 다시 시작하는 것입니다. 유효하지 않은 상태로 쉽게 종료되는 프로그램은 중요한 소프트웨어에 적합하지 않습니다 (그리고 Java는 EULA에서 중요 소프트웨어에 대한 사용을 명시 적으로 허용하지 않습니다)
gbjbaanb

예외를 사용하고 검사하지 않고 리턴 코드를 사용하고 결국 검사하지 않으면 모두 동일한 심정지를 초래합니다.
Newtopian

-1

나는 현재 다소 큰 OOP 기반 프로젝트 / API의 중간에 있으며 예외 의이 레이아웃을 사용했습니다. 그러나 그것은 모두 예외 처리 등으로 얼마나 깊이 가고 싶어하는지에 달려 있습니다.

ExpectedException
-AuthorisedException
-EmptySetException
-NoRemainingException
-NoRowsException
-NotFoundException
-ValidationException

UnexpectedException
-ConnectivityException
-EnvironmentException
-ProgrammerException
-SQLException

   $valid_types = array('mysql', 'oracle', 'sqlite');
       if (!in_array($type, $valid_types)) {
           throw new ecProgrammerException(
        'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
    $type,
    join(', ', $valid_types)
    );
}

11
예외가 예상되는 경우 실제로 예외는 아닙니다. "NoRowsException"? 제어 흐름이 나에게 전달되므로 예외를 잘못 사용합니다.
quentin-starin

1
@qes : 함수가 값을 계산할 수 없을 때마다 (예 : double Math.sqrt (double v) 또는 User findUser (long id)) 예외를 발생시키는 것이 좋습니다. 이를 통해 호출자는 각 호출 후에 확인하는 대신 편리한 위치에서 오류를 포착하고 처리 할 수있는 자유를 갖게됩니다.
케빈 클라인

1
예상 = 제어 흐름 = 예외 패턴. 제어 흐름에는 예외를 사용해서는 안됩니다. 특정 입력에 대해 오류가 발생할 것으로 예상되면 반환 값의 일부로 전달됩니다. 그래서 우리는 NAN또는 NULL.
Eonil

1
@Eonil ... 또는 Option <T>
Maarten Bodewes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.