오류를 조기에“잡을”도구로 예외를 사용하는 것이 괜찮습니까?


29

나는 예외를 사용하여 문제를 조기에 포착합니다. 예를 들면 다음과 같습니다.

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

내 프로그램은 null이 기능을 전달해서는 안됩니다 . 나는 그것을 의도하지 않았다. 그러나 우리 모두 알다시피 의도하지 않은 것은 프로그래밍에서 발생합니다.

이 문제가 발생하면 예외를 던지면 프로그램의 다른 위치에서 더 많은 문제가 발생하기 전에 문제를 발견하고 해결할 수 있습니다. 예외로 인해 프로그램이 중지되고 "나쁜 일이 발생했습니다. 수정하십시오"라고 알려줍니다. 이 null프로그램 주위를 이동 하는 대신 다른 곳에서 문제가 발생합니다.

자, 당신은 옳습니다.이 경우에는 즉시 당장 null발생할 NullPointerException수 있으므로 가장 좋은 예는 아닙니다.

그러나 다음과 같은 방법을 고려하십시오.

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

이 경우 a null매개 변수가 프로그램 주위로 전달되어 훨씬 나중에 오류가 발생할 수 있으며 이로 인해 원래 위치로 추적하기가 어렵습니다.

다음과 같이 기능을 변경하십시오.

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

다른 곳에서 이상한 오류가 발생하기 전에 문제를 훨씬 많이 발견 할 수 있습니다.

또한 null매개 변수로서의 참조는 단지 예일뿐입니다. 잘못된 논증에서 다른 것에 이르기까지 많은 종류의 문제가 될 수 있습니다. 항상 일찍 발견하는 것이 좋습니다.


그래서 내 질문은 간단합니다 :이 좋은 습관입니까? 문제 예방 도구로 예외를 사용하는 것이 좋습니까? 이것이 합법적 인 예외 적용입니까, 아니면 문제입니까?


2
Downvoter가이 질문의 문제점을 설명 할 수 있습니까?
조르지오

2
"충돌 조기 충돌, 종종 충돌"
Bryan Chen

16
말 그대로 예외가 있습니다.
raptortech97

코딩 계약을 통해 이러한 많은 오류를 정적으로 감지 할 수 있습니다. 이것은 일반적으로 런타임에 예외를 던지는 것보다 우수합니다. 코딩 계약의 지원 및 효과는 언어마다 다릅니다.
Brian

3
IllegalArgumentException이 발생하므로 " 입력 사람" 이라고하는 것은 중복 입니다.
kevin cline

답변:


29

그렇습니다. "초기 실패"는 매우 좋은 원칙이며, 이는이를 구현하는 한 가지 방법 일뿐입니다. 그리고 특정 값을 반환 해야하는 메서드에는 의도적으로 실패 할 있는 다른 방법이 거의 없습니다 . 예외를 던지거나 어설 션을 트리거합니다. 예외는 '예외'조건을 나타내는 것으로 가정되며 프로그래밍 오류를 감지하는 것은 예외적입니다.


오류나 상태에서 복구 할 방법이 없으면 조기에 실패하는 방법입니다. 예를 들어 파일을 복사해야하고 소스 또는 대상 경로가 비어 있으면 예외를 즉시 처리해야합니다.
Michael Shopsin

또한 "빠른 실패"로 알려져
keuleJ

8

예, 예외를 던지는 것이 좋습니다. 일찍 던지고, 자주 던지고, 열심히 던지십시오.

디버깅 / 테스트 빌드보다는 런타임에 "컴파일"될 수있는 어설 션에 의해 처리되는 예외적 인 동작 (특히 프로그래밍 오류를 반영하는 것으로 생각되는 동작)에 대한 "예외 대 어설 션"토론이 있다는 것을 알고 있습니다. 그러나 몇 가지 추가 정확성 검사에서 소비되는 성능의 양은 최신 하드웨어에서 최소이며, 추가 비용은 정확하고 손상되지 않은 결과를 갖는 가치보다 훨씬 중요합니다. 나는 실제로 (대부분의) 검사가 런타임에 제거되기를 원하는 응용 프로그램 코드베이스를 결코 만나지 못했습니다.

나는 숫자가 많은 코드의 빡빡한 루프 내에서 많은 추가 검사와 조건을 원하지 않을 것이라고 유혹하고 있습니다 ... 그러나 실제로 많은 숫자 오류가 생성되는 곳이며, 잡히지 않으면 바깥으로 전파됩니다 모든 결과에 영향을 미칩니다. 따라서 거기에서도 수표를 사용할 가치가 있습니다. 실제로 가장 효율적인 가장 효율적인 수치 알고리즘 중 일부는 오류 평가를 기반으로합니다.

추가 코드를 가장 많이 인식해야하는 마지막 장소는 대기 시간에 민감한 코드이며, 추가 조건으로 인해 파이프 라인이 중단 될 수 있습니다. 따라서 운영 체제, DBMS 및 기타 미들웨어 커널 및 저수준 통신 / 프로토콜 처리의 중간에 있습니다. 그러나 다시 말하지만, 오류가 가장 많이 발생하는 장소 중 일부이며 보안 (정확성, 정확성 및 데이터 무결성)에 가장 큰 영향을 미칩니다.

내가 찾은 개선 사항 중 하나는 기본 수준 예외 만 throw하지 않는 것입니다. IllegalArgumentException좋지만 본질적으로 어디서나 나올 수 있습니다. 대부분의 언어에서 사용자 정의 예외를 추가하는 데별로 걸리지 않습니다. 개인 취급 모듈의 경우 다음과 같이 말합니다.

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

그런 다음 누군가가 볼 때 PersonArgumentException어디에서 왔는지 분명합니다. 엔터티를 불필요하게 곱하지 않기 때문에 (Occam 's Razor) 추가 할 사용자 지정 예외 수에 대한 균형 조정 작업이 있습니다. "이 모듈은 올바른 데이터를 얻지 못하고 있습니다!" 또는 "이 모듈은해야 할 일을 할 수 없습니다!" 구체적이고 맞춤형이지만 지나치게 정밀하지 않은 방식으로 전체 예외 계층을 다시 구현해야합니다. 재고 예외로 시작한 다음 코드를 스캔하고 "이 N 개 장소에서 재고 예외를 제기하고 있지만 데이터를 얻지 못한다는 상위 수준의 아이디어로 귀결된다는 사실을 깨닫고 작은 맞춤 예외 세트를 자주 사용합니다. 그들은 필요하다.


I've never actually met an application codebase for which I'd want (most) checks removed at runtime. 그런 다음 성능이 중요한 코드를 수행하지 않았습니다. 나는 지금 당장 37M의 ops와 42M로 컴파일 된 어설 션을 사용하여 무언가를하고 있습니다. 어설 션은 외부 입력의 유효성을 검사하지 않으며 코드가 올바른지 확인합니다. 내 물건이 파손되지 않은 것에 만족하면 고객은 13 % 증가한 것을 기쁘게 생각합니다.
Blrfl

커스텀 예외가있는 코드베이스를 피하는 것은 피하는 것이 좋지만, 도움이 될 수는 있지만 다른 사람들과 친해져야하는 것은 아닙니다. 일반적으로 일반적인 예외를 확장하고 구성원이나 기능을 추가하지 않으면 실제로 필요하지 않습니다. 귀하의 예에서도조차 PersonArgumentException명확하지 않습니다 IllegalArgumentException. 후자는 불법 인수가 통과 될 때 보편적으로 발생하는 것으로 알려져 있습니다. 실제로 Person호출에 대해 잘못된 상태 ( InvalidOperationExceptionC #과 유사)에 있으면 전자가 throw 될 것으로 예상합니다 .
Selali Adobor

조심하지 않으면 함수의 다른 곳에서 동일한 예외를 트리거 할 수 있지만 이는 일반적인 예외의 성격이 아니라 코드의 결함입니다. 그리고 디버깅 및 스택 추적이 발생합니다. 예외를 "레이블"하려는 경우 메시지 생성자를 사용하는 것과 같이 가장 일반적인 예외가 제공하는 기존 기능을 사용하십시오 (정확히 존재하는 것임). 일부 예외는 생성자에게 문제가되는 인수의 이름을 얻기위한 추가 매개 변수를 제공하지만, 동일한 기능을 통해 올바른 형식의 메시지를 제공 할 수 있습니다.
Selali Adobor

기본적으로 동일한 예외라는 개인 레이블에 공통 예외를 브랜드로 변경하는 것은 약한 예이며 거의 노력할 가치가 없다는 것을 인정합니다. 실제로 나는 더 높은 의미 수준에서 모듈의 의도와 더 밀접하게 연결된 사용자 정의 예외를 방출하려고합니다.
Jonathan Eunice

수치 / HPC 및 OS / 미들웨어 영역에서 성능에 민감한 작업을 수행했습니다. 13 %의 성능 향상은 작은 것이 아닙니다. 군사 지휘관이 공칭 원자로 출력의 105 %를 요구하는 것과 같은 방식으로 점검을 끌 수 있습니다. 그러나 수표, 백업 및 기타 보호 기능을 해제하는 이유로 자주 "이 방법으로 더 빨리 실행됩니다"를 보았습니다. 추가 성능을 위해 기본적으로 복원력과 안전 (데이터 무결성을 포함한 많은 경우)과 균형을 이룹니다. 그 가치가 있는지 여부는 판단 요청입니다.
Jonathan Eunice

3

응용 프로그램을 디버깅 할 때 가능한 빨리 실패하는 것이 좋습니다. 레거시 C ++ 프로그램에서 특정 세그먼테이션 결함을 기억합니다. 버그가 발견 된 곳은 버그가 소개 된 곳과 관련이 없습니다 (널 포인터가 메모리에서 한 곳에서 다른 곳으로 행복하게 이동하여 마침내 문제가 발생했습니다) ). 이 경우 스택 추적이 도움이되지 않습니다.

그렇습니다. 방어 프로그래밍은 버그를 신속하게 감지하고 수정하는 효과적인 방법입니다. 반면에, 특히 null 참조를 사용하면 초과 될 수 있습니다.

예를 들어, 특정 참조가 null 인 경우 NullReferenceException한 사람의 나이를 얻으려고 할 때 다음 명령문에서 발생합니다. 기본적 으로 직접 시스템 을 검사 할 필요는 없습니다 . 기본 시스템에서 이러한 오류를 포착하고 예외를 발생 시킵니다.

보다 현실적인 예를 들면 다음과 같은 assert명령문 을 사용할 수 있습니다 .

  1. 읽고 쓰는 것이 더 짧습니다.

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. 접근 방식에 맞게 특별히 설계되었습니다. 어설 션과 예외가 모두있는 세계에서는 다음과 같이 구분할 수 있습니다.

    • 가정 오류프로그래밍 오류 에 대한 것입니다 ( "/ *이 경우 절대 발생하지 않아야 * /").
    • 예외적 인 경우예외입니다 (예외적이지만 가능한 상황).

따라서 어설 션을 사용하여 응용 프로그램의 입력 및 / 또는 상태에 대한 가정을 노출하면 다음 개발자가 코드의 목적을 조금 더 이해할 수 있습니다.

정적 분석기 (예 : 컴파일러)도 더 행복 할 수 있습니다.

마지막으로 단일 스위치를 사용하여 배포 된 응용 프로그램에서 어설 션을 제거 할 수 있습니다. 그러나 일반적으로 말하자면 효율성을 향상시킬 것으로 기대하지 마십시오. 런타임시 어설 션 검사는 무시할 수 있습니다.


1

내가 아는 한 다른 프로그래머는 하나의 솔루션을 선호합니다.

첫 번째 솔루션은 일반적으로 더 간결하기 때문에 선호됩니다. 특히 다른 기능에서 동일한 조건을 반복해서 확인할 필요가 없습니다.

예를 들어 두 번째 해결책을 찾습니다.

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

더 견고하기 때문에

  1. registerPerson()호출 포인터의 어딘가에 널 포인터 예외가 발생하지 않는 한 가능한 빨리 오류를 잡습니다 . 디버깅이 훨씬 쉬워집니다. 우리는 무효 값이 코드를 통해 버그로 표시되기 전에 코드를 통과 할 수있는 거리를 알고 있습니다.
  2. 함수 사이의 결합을 줄입니다 registerPerson(). person인수 를 사용하여 어떤 함수가 끝나고 어떻게 사용할 것인지에 대한 가정을하지 않습니다 null. 오류 결정 은 로컬에서 수행됩니다.

따라서 코드가 다소 복잡한 경우이 두 번째 접근 방식을 선호합니다.


1
이유를 알려 주면 ( "입력 인이 널입니다.") 라이브러리의 일부 모호한 메소드가 실패 할 때와 달리 문제점을 이해하는 데 도움이됩니다.
Florian F

1

일반적으로 그렇습니다. "조기 실패"하는 것이 좋습니다. 그러나 특정 예에서 명시 적 기능 IllegalArgumentExceptionNullReferenceException조작 된 두 오브젝트가 이미 함수에 대한 인수로 전달되기 때문에-에 비해 크게 개선되지 않습니다 .

그러나 약간 다른 예를 살펴 보겠습니다.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

생성자에 인수 검사가없는 경우을 NullReferenceException호출 하면 됩니다 Calculate.

그러나 깨진 코드 조각은 Calculate함수도 아니고 함수 소비자도 아닙니다 Calculate. 깨진 코드 조각은 PersonCalculatornull 을 사용하여 구성하려고 시도하는 코드 Person이므로 예외가 발생하는 곳입니다.

명시 적 인수 확인을 제거하면 호출 NullReferenceException시 발생 원인을 파악해야합니다 Calculate. 그리고 null사람 과 함께 객체가 만들어진 이유를 추적하는 것은 특히 계산기를 구성하는 코드가 실제로 Calculate함수를 호출하는 코드에 근접하지 않은 경우 까다로울 수 있습니다 .


0

당신이 제시 한 예에는 없습니다.

당신이 말했듯이, 그 후에 곧 예외를 얻으려고 할 때 명시 적으로 던지는 것이 많이 얻지 못합니다. 많은 사람들은 좋은 메시지로 명시적인 예외를 갖는 것이 더 좋다고 주장 할 것입니다. 시험판 시나리오에서는 스택 추적이 충분합니다. 출시 후 시나리오에서 콜 사이트는 종종 함수 내부보다 더 나은 메시지를 제공 할 수 있습니다.

두 번째 양식은 함수에 너무 많은 정보를 제공하는 것입니다. 이 함수는 다른 함수가 널 입력을 던질 것이라는 것을 반드시 알 필요는 없습니다. 그들이 널 (null) 입력에 투사 할 경우에도 지금 , 그것은 리팩토링 매우 번잡하게해야 널 검사 이후의 경우 인 것을 정지 코드 전반에 걸쳐 확산이다.

그러나 일반적으로 무언가가 잘못되었다고 판단되면 (DRY를 존중하면서) 일찍 던져야합니다. 그러나 이것은 아마도 그 좋은 예는 아닙니다.


-3

귀하의 예제 기능에서, 나는 당신이 검사를하지 않고 단지 NullReferenceException발생 하는 것을 선호합니다 .

우선, 어쨌든 null을 전달하는 것은 의미가 없으므로을 던지는 즉시 문제를 알아 낼 것입니다 NullReferenceException.

두 번째로, 어떤 종류의 명백하게 잘못된 입력이 제공되었는지에 따라 모든 함수가 약간 다른 예외를 던지면 곧 18 가지 유형의 예외를 던질 수있는 함수가 있고 곧 자신도 그렇게 말하는 것입니다. 처리해야 할 작업이 많았으며 어쨌든 모든 예외를 억제했습니다.

함수에서 디자인 타임 오류 상황을 돕기 위해 실제로 할 수있는 일은 없으므로 변경하지 않고 오류가 발생하도록하십시오.


나는이 원칙에 기반한 코드 기반으로 몇 년 동안 일해 왔습니다. 포인터는 필요할 때 단순히 참조 해제되고 NULL에 대해 거의 확인되지 않고 명시 적으로 처리되지 않습니다. 예외 (또는 코어 덤프)가 훨씬 나중에 논리적 오류가 발생한 지점에서 발생하기 때문에이 코드를 디버깅하는 데는 시간이 많이 걸리고 번거 롭습니다. 말 그대로 몇 주 동안 특정 버그를 찾는 데 낭비했습니다. 이러한 이유로, 나는 널 포인터 예외가 어디서 오는지 즉시 알 수없는 복잡한 코드에 대해 가능한 빨리 실패 스타일을 선호합니다.
Giorgio

"어쨌든 null을 전달하는 것은 의미가 없으므로 NullReferenceException 발생에 따라 문제를 즉시 알아낼 것입니다.": 반드시 Prog의 두 번째 예에서 볼 수있는 것은 아닙니다.
Giorgio

"두 가지, 어떤 종류의 명백한 잘못된 입력이 제공되었는지에 따라 모든 함수가 약간 다른 예외를 throw하는 경우 곧 18 가지의 다른 유형의 예외를 throw 할 수있는 함수를 갖게됩니다 ...":이 경우 모든 함수에서 동일한 예외 (NullPointerException조차도)를 사용할 수 있습니다. 중요한 것은 각 함수에서 다른 예외를 발생시키지 않고 일찍 던지는 것입니다.
Giorgio

이것이 RuntimeExceptions의 목적입니다. "발생하지 않아야하지만"코드가 작동하지 못하게하는 모든 조건이 발생합니다. 메소드 서명에서 선언 할 필요는 없습니다. 그러나 특정 시점에서이를 포착하여 요청 된 조치가 실패한 기능을보고해야합니다.
Florian F

1
질문은 언어를 지정하지 않으므로 예를 들어 의존하는 RuntimeException것이 반드시 유효한 가정 일 필요는 없습니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.