메소드가 여러 유형의 확인 된 예외를 처리하지 않아야하는 이유는 무엇입니까?


47

SonarQube를 사용하여 Java 코드를 분석하고이 규칙 (중요하게 설정 됨)이 있습니다.

공개 메소드는 최대 하나의 확인 된 예외를 처리해야합니다.

확인 된 예외를 사용하면 메소드 호출자가 오류를 전파하거나 처리하여 오류를 처리해야합니다. 이것은 예외를 메소드 API의 일부로 만듭니다.

호출자의 복잡성을 합리적으로 유지하기 위해 메소드는 여러 종류의 확인 된 예외를 발생시키지 않아야합니다. "

Sonar의 또 다른 비트는 다음과 같습니다.

공개 메소드는 최대 하나의 확인 된 예외를 처리해야합니다.

확인 된 예외를 사용하면 메소드 호출자가 오류를 전파하거나 처리하여 오류를 처리해야합니다. 이것은 예외를 메소드 API의 일부로 만듭니다.

호출자의 복잡성을 합리적으로 유지하기 위해 메소드는 여러 종류의 확인 된 예외를 발생시키지 않아야합니다.

다음 코드 :

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

리팩토링해야합니다 :

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

재정의 메소드는이 규칙에 의해 점검되지 않으며 점검 된 여러 예외를 처리 할 수 ​​있습니다.

예외 처리에 대한 내 독서 에서이 규칙 / 권장 사항을 결코 읽지 않았으며 주제에 대한 표준, 토론 등을 찾으려고 시도했습니다. 내가 찾은 유일한 것은 CodeRach의 것입니다. 메소드는 얼마나 많은 예외를 던져야합니까?

이것이 잘 받아 들여지는 표준입니까?


7
당신은 어떻게 생각하세요? SonarQube에서 인용 한 설명은 합리적입니다. 의심 할만한 이유가 있습니까?
Robert Harvey

3
둘 이상의 예외를 발생시키는 코드를 많이 작성했으며 둘 이상의 예외를 발생시키는 많은 라이브러리를 사용했습니다. 또한 예외 처리에 관한 서적 / 문서에서 예외 수를 제한하는 주제는 일반적으로 나타나지 않습니다. 그러나 많은 사례들은 다수의 던지기 / 잡기가 실습에 대한 암묵적인 승인을 보여줍니다. 그래서 나는 그 규칙이 놀랍다는 것을 발견했고 예외 처리의 모범 사례 / 철학에 대한 기본적인 방법과 예제에 대한 더 많은 연구를 원했습니다.
sdoca

답변:


32

제공된 코드가있는 상황을 고려하십시오.

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

여기서의 위험은 호출하기 위해 작성한 코드 delete()가 다음과 같습니다.

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

이것도 나쁘다. 그리고 기본 Exception 클래스를 잡는 플래그를 지정하는 또 다른 규칙이 있습니다.

핵심은 다른 곳에서 잘못된 코드를 작성하도록하는 코드를 작성하지 않는 것입니다.

당신이 겪는 규칙은 다소 일반적인 규칙입니다. Checkstyle 은 디자인 규칙에 다음과 같이 있습니다.

ThrowsCount

명령문을 지정된 개수 (기본적으로 1)로 제한합니다.

이론적 근거 : 예외는 메소드 인터페이스의 일부이다. 너무 많은 다른 근본 예외를 처리하는 메소드를 선언하면 예외 처리가 번거로워지고 catch (Exception ex)와 같은 코드 작성과 같은 프로그래밍 방식이 잘못됩니다. 이 검사를 통해 개발자는 예외를 계층 구조에 넣어 가장 간단한 경우 호출자가 한 가지 유형의 예외 만 검사해야하지만 필요한 경우 하위 클래스를 구체적으로 잡을 수 있습니다.

이것은 문제와 문제가 무엇인지, 왜하지 말아야 하는지를 정확하게 설명합니다. 많은 정적 분석 도구가 식별하고 플래그를 지정하는 것이 널리 인정되는 표준입니다.

당신이 반면 수 있습니다 언어 설계에 따라 작업을 수행하고있을 는 옳은 일 때 배, 당신이보고 즉시 가야한다 무언가이다 "음, 내가 왜이 일을하고있다?" 모든 사람이 결코 징계 할 수 없을 정도로 훈련 된 내부 코드에는 허용 될 수 catch (Exception e) {}있지만, 특히 내부 상황에서 사람들이 모퉁이를 깎는 것을 보지 못한 경우가 많습니다.

수업을 이용하는 사람들이 나쁜 코드를 작성하도록하지 마십시오.


나는 이것의 중요성은 자바 SE 7 줄어들 단일 catch 문이 여러 예외를 잡을 수 나중에 있기 때문에 (것을 지적해야 여러 예외 유형 잡기 및 확인 향상된 유형과 예외 Rethrowing 오라클을).

Java 6 및 이전 버전에서는 다음과 같은 코드가 있습니다.

public void delete() throws IOException, SQLException {
  /* ... */
}

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

또는

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

Java 6에서 이러한 옵션 중 어느 것도 이상적이지 않습니다. 첫 번째 방법은 위반 DRY . 여러 예외가 같은 일을 반복해서 반복합니다-각 예외마다 한 번씩. 예외를 기록하고 다시 throw 하시겠습니까? 승인. 각 예외에 대해 동일한 코드 줄.

두 번째 옵션은 몇 가지 이유로 더 나쁩니다. 첫째, 그것은 당신이 모든 예외를 잡고 있음을 의미합니다. 널 포인터가 거기서 잡히지 않아야합니다. 또한, 당신은 코드를 다시 사용하는 사람들이 강제 로 스택을 더 혼란스럽게 만드는 Exception메소드 서명이 될 것을 의미 하는를 다시 던지고 있습니다 .deleteSomething() throws Exceptioncatch(Exception e)

자바 7, 이것은 아니다 으로 대신 할 수 있기 때문에 중요합니다 :

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

또한, 하나의 경우 검사 유형 않는 예외의 유형을 잡기가 발생되고 :

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

그 인식 유형 검사는 e 종류의 수 IOException또는 SQLException. 나는 여전히이 스타일의 사용에 대해 지나치게 열성적 인 것은 아니지만 Java 6에서와 같이 나쁜 코드를 일으키지 않습니다 (메소드 서명이 예외를 확장하는 수퍼 클래스가되도록 강요 할 것입니다).

이러한 모든 변경에도 불구하고 많은 정적 분석 도구 (Sonar, PMD, Checkstyle)는 여전히 Java 6 스타일 안내서를 시행하고 있습니다. 나쁜 것이 아닙니다. 나는 이것들이 여전히 시행 될 것이라는 경고에 동의하는 경향이 있지만, 당신은 당신의 팀이 우선 순위를 정하는 방법에 따라 우선 순위를 주 또는 부로 변경할 수 있습니다.

예외가 선택 또는 선택 취소해야하는 경우 ... 그의 문제입니다 g의 r에 전자 t의 논쟁 하나가 쉽게 인수의 각면을 차지 수많은 블로그 게시물을 찾을 수 있습니다. 그러나 확인 된 예외로 작업하는 경우 적어도 Java 6에서 여러 유형을 던지지 않아야합니다.


이것이 잘 받아 들여진 표준이라는 것에 대한 내 질문에 대답 할 때 이것을 대답으로 받아들입니다. 그러나 나는 아직도 Panzercrisis가 내 표준이 무엇인지 결정하기 위해 그의 답변에 제공된 링크로 시작하는 다양한 찬반 토론을 읽고 있습니다.
sdoca

"유형 검사기는 e가 IOException 또는 SQLException 유형일 수 있음을 인식합니다.": 이것은 무엇을 의미합니까? 다른 유형의 예외가 발생하면 어떻게됩니까 foo.delete()? 여전히 잡히고 다시 던져 집니까?
조르지오

@Giorgio delete해당 예제에서 IOException 또는 SQLException 이외의 확인 된 예외가 발생하면 컴파일 시간 오류가 발생합니다 . 내가 만들려고했던 요점은 rethrowException을 호출하는 메소드가 여전히 Java 7의 예외 유형을 가져 오는 것입니다. Java 6에서는 모든 것이 공통 Exception유형으로 병합되어 정적 분석 및 기타 코더가 슬프게됩니다.

내가 참조. 그것은 나에게 약간 복잡해 보입니다. 나는 금지하고 catch (Exception e)그것을 강제로 catch (IOException e)또는 catch (SQLException e)대신 하는 것이 더 직관적이라고 생각합니다 .
조르지오

@Giorgio 좋은 코드를 더 쉽게 작성할 수 있도록하기 위해 Java 6에서 발전한 단계입니다. 불행히도 잘못된 코드를 작성하는 옵션은 오랫동안 우리와 함께 할 것입니다. Java 7이 catch(IOException|SQLException ex)대신 할 수 있음을 기억하십시오 . 그러나 예외를 다시 발생 시키려면 유형 검사기가 코드의 단순화를 통해 실제 유형의 예외를 전파하도록 허용하는 것은 나쁘지 않습니다.

22

이상적으로 한 가지 유형의 예외 만 발생시키려는 이유는 단일 책임종속성 반전 원칙을 위반할 가능성이 있기 때문 입니다. 예제를 사용하여 시연 해 봅시다.

지속성에서 데이터를 가져 오는 방법이 있고 지속성이 파일 세트라고 가정 해 봅시다. 파일을 다루기 때문에 다음과 같이 할 수 있습니다 FileNotFoundException.

public String getData(int id) throws FileNotFoundException

이제 요구 사항이 변경되었으며 데이터베이스에서 데이터가 제공됩니다. FileNotFoundException(파일을 다루지 않기 때문에) 대신에 SQLException:

public String getData(int id) throws SQLException

이제 메소드를 사용하는 모든 코드를 살펴보고 확인해야 할 예외를 변경해야합니다. 그렇지 않으면 코드가 컴파일되지 않습니다. 우리의 방법이 광범위하고 광범위하게 호출된다면, 그것은 다른 사람들을 변화 시키거나 바꾸는 데 많은 도움이 될 수 있습니다. 많은 시간이 걸리고 사람들은 행복하지 않을 것입니다.

의존성 반전은 캡슐화하려고하는 내부 구현 세부 사항을 노출하기 때문에 이러한 예외 중 하나를 던져서는 안된다고 말합니다. 호출 코드는 레코드를 검색 할 수 있는지 실제로 걱정할 때 사용하는 지속성 유형을 알아야합니다. 대신 API를 통해 노출하는 것과 동일한 추상화 수준에서 오류를 전달하는 예외를 처리해야합니다.

public String getData(int id) throws InvalidRecordException

이제 내부 구현을 변경하면 해당 예외를 랩으로 감싸서 InvalidRecordException전달할 수 있습니다 (또는 랩핑하지 않고 그냥 새 던지기 InvalidRecordException). 외부 코드는 어떤 유형의 지속성이 사용되는지 알거나 신경 쓰지 않습니다. 모두 캡슐화되어 있습니다.


단일 책임에 관해서는 관련되지 않은 여러 예외를 발생시키는 코드에 대해 생각해야합니다. 다음과 같은 방법이 있다고 가정 해 봅시다.

public Record parseFile(String filename) throws IOException, ParseException

이 방법에 대해 무엇을 말할 수 있습니까? 우리는이 파일을 열 서명에서 알 수 그것을 구문 분석합니다. 메소드 설명에서 "and"또는 "or"와 같은 연결을 볼 때, 둘 이상의 작업을 수행한다는 것을 알고 있습니다. 그것은 하나 이상의 책임이 있습니다. 하나 이상의 책임이있는 방법은 책임이 변경 될 경우 변경 될 수 있으므로 관리하기가 어렵습니다. 대신, 우리는 단일 책임을 갖도록 메소드를 분리해야합니다.

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

데이터를 파싱하는 책임에서 파일을 읽는 책임을 추출했습니다. 이것의 한 가지 부작용은 이제 모든 인 메모리, 파일, 네트워크 등 모든 소스에서 파싱 데이터로 문자열 데이터를 전달할 수 있다는 것입니다. parse디스크에 파일이 필요하지 않기 때문에 더 쉽게 테스트 할 수도 있습니다. 에 대한 테스트를 실행합니다.


때로는 메소드에서 예외가 발생할 수있는 예외가 두 개 이상 있지만, SRP와 DIP를 고수하면 이러한 상황이 발생하는 시간이 더 드 become니다.


귀하의 예에 따라 하위 수준 예외를 래핑하는 것에 전적으로 동의합니다. 우리는 정기적으로 그렇게하고 MyAppExceptions의 편차를 발생시킵니다. 여러 예외가 발생하는 예 중 하나는 데이터베이스에서 레코드를 업데이트하려고 할 때입니다. 이 메소드는 RecordNotFoundException을 발생시킵니다. 그러나 레코드가 특정 상태 인 경우에만 레코드를 업데이트 할 수 있으므로이 메소드는 InvalidRecordStateException도 발생시킵니다. 나는 이것이 유효하고 발신자에게 가치있는 정보를 제공한다고 생각합니다.
sdoca

@sdoca update메소드가 가능한 한 원자적이고 예외가 적절한 추상화 레벨에 있다면, 두 가지 예외가 있기 때문에 두 가지 유형의 예외를 던져야하는 것처럼 들립니다. 이것은 (때로는 임의의) 린터 규칙이 아니라 몇 개의 예외를 던질 수 있는지를 측정해야합니다.
cbojar 2014

2
그러나 스트림에서 데이터를 읽고 그에 따라 구문 분석하는 방법이 있다면 전체 스트림을 버퍼로 읽지 않으면이 두 함수를 분리 할 수 ​​없으므로 다루기가 어려울 수 있습니다. 또한 예외를 올바르게 처리하는 방법을 결정하는 코드는 읽기 및 구문 분석을 수행하는 코드와 분리 될 수 있습니다. 읽기 및 구문 분석 코드를 작성할 때 내 코드를 호출하는 코드가 두 가지 유형의 예외를 처리하는 방법을 알 수 없으므로 둘 다 처리해야합니다.
user3294068

+1 :이 답변이 특히 마음에 듭니다. 특히 모델링 관점에서 문제를 해결하기 때문입니다. catch (IOException | SQLException ex)실제 문제가 프로그램 모델 / 디자인에 있기 때문에 종종 다른 관용구 (예 :)를 사용할 필요는 없습니다 .
조르지오

3

Java를 잠시 재생할 때이 문제를 조금이라도 해결하는 것을 기억하지만, 질문을 읽을 때까지 확인 된 것과 확인되지 않은 것의 구별을 실제로 의식하지 않았습니다. Google 에서이 기사를 매우 빨리 발견했으며 명백한 논쟁 중 일부에 해당합니다.

http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

즉,이 사람이 확인 된 예외로 언급 한 문제 중 하나는 throws메소드 선언의 절에 여러 가지 확인 된 예외를 계속 추가하면 (Java에서 처음부터 개인적으로 실행했습니다) 더 높은 수준의 메소드로 이동할 때 더 많은 상용구 코드를 넣어야하지만 더 많은 예외 유형을 더 낮은 레벨의 메소드에 도입하려고 할 때 더 큰 혼란을 일으키고 호환성을 손상시킵니다. 확인 된 예외 유형을 하위 레벨 메소드에 추가하는 경우 코드를 다시 실행하고 다른 여러 메소드 선언도 조정해야합니다.

이 기사에서 언급 한 완화의 한 가지 점은 저자가 개인적으로 좋아하지 않았기 때문에 기본 클래스 예외를 작성하고 throws절만 사용하도록 절을 제한 한 다음 내부적으로 하위 클래스를 올리는 것이 었습니다. 이렇게하면 모든 코드를 다시 실행하지 않고도 새로운 확인 된 예외 유형을 만들 수 있습니다.

이 기사의 저자는 이것을 좋아하지는 않았지만 개인적인 경험 (특히 모든 하위 클래스가 무엇인지 찾을 수있는 경우)에서 완벽하게 이해가되며, 그 이유는 당신에게 주어진 조언이 하나의 확인 된 예외 유형에 모든 것을 제한하십시오. 또한 언급 한 조언은 실제로 비공개 메소드에서 여러 가지 확인 된 예외 유형을 허용하므로 이것이 동기 (또는 그 밖의 경우)라면 완벽하게 이해됩니다. 어쨌든 개인적인 방법이거나 비슷한 것이면 작은 것을 바꿀 때 코드베이스의 절반을 넘지 않을 것입니다.

이 표준이 받아 들여질 표준인지는 거의 묻지 않았지만, 당신이 언급 한 연구, 합리적으로 생각한 기사, 그리고 개인적인 프로그래밍 경험으로 말하면 분명히 어떤 식 으로든 두드러지지 않는 것 같습니다.


2
Throwable자신 만의 모든 계층 구조를 발명하는 대신 throws를 선언 하고 처리해야합니까?
중복 제거기

@Deduplicator 저자가 그 아이디어를 좋아하지 않는 이유이기도하다. 그는 단지 당신이 그렇게 할 것이라면 체크하지 않은 것을 모두 사용할 것이라고 생각했습니다. 그러나 API를 사용하는 사람 (귀하의 동료)이 기본 클래스에서 파생 된 모든 하위 클래스 예외 목록을 가지고 있다면 적어도 예상되는 모든 예외가 있음을 알리는 데 약간의 이점이 있음을 알 수 있습니다 특정 서브 클래스 세트 내에서; 만약 그들이 그들 중 하나가 다른 것보다 "취급하기 쉽다"고 느낀다면, 그것들에 특정한 핸들러를 포기하는 경향이 없을 것입니다.
Panzercrisis

일반적으로 확인 된 예외가 잘못된 업장 인 이유는 간단합니다. 바이러스 성이므로 모든 사용자를 감염시킵니다. 의도적으로 가장 넓은 사양을 지정하는 것은 모든 예외가 선택 해제되어 혼란을 피하는 방법입니다. 예, 당신이 처리 할 수 있습니다 무엇을 문서화하는 것은 좋은 생각이다 문서에 대한 : 그냥 함수가 엄격하게 제한 가치가에서 올 수도 (따로 없음에서 모든 / 어쩌면,하지만 자바 어쨌든 것을 허용하지 않습니다)하는 예외 알고 .
중복 제거기

1
@ 중복 제거기 나는 아이디어를지지하지 않으며, 반드시 그것을 좋아하지는 않습니다. 방금 OP가 제공 한 조언이 나온 방향에 대해 이야기하고 있습니다.
Panzercrisis

1
링크 주셔서 감사합니다. 이 주제에 대해 읽을 수있는 좋은 출발점이되었습니다.
sdoca

-1

여러 가지 확인해야 할 예외가 발생하면 합리적인 여러 조치가있을 때 의미가 있습니다.

예를 들어 방법이 있다고 가정 해 봅시다.

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

이것은 pne 예외 규칙을 위반하지만 의미가 있습니다.

불행히도, 평소에 일어나는 일은 다음과 같은 방법입니다.

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

여기에서는 호출자가 예외 유형에 따라 특정 작업을 수행 할 가능성이 거의 없습니다. 따라서 만약 우리가이 방법들이 때때로 잘못 될 수 있다는 것을 깨닫고 자한다면, SomethingMightGoWrongException을 던지는 것은 아주 훌륭합니다.

따라서 최대 하나의 확인 된 예외가 규칙입니다.

그러나 프로젝트에서 의미있는 전체 검사 예외가 여러 개있는 디자인을 사용하는 경우이 규칙이 적용되지 않습니다.

참고 : 실제로 거의 모든 곳에서 문제가 발생할 수 있으므로? RuntimeException을 확장하지만 "우리 모두 실수를 저지 릅니다"와 "이것은 외부 시스템과 대화하며 때때로 다운되어 처리합니다"사이에는 차이가 있습니다.


1
"복수의 합리적인 일들"은 srp를 거의 따르지 않습니다 –이 점은 이전 답변
gnat

그렇습니다. 하나의 함수를 처리 (한 측면을 처리)하고 다른 하나의 문제를 처리 (한 측면을 처리)하는 하나의 문제 (즉,이 두 함수를 호출)에서 하나의 문제를 감지 할 수 있습니다. 그리고 호출자는 한 계층의 try catch (한 함수에서) 한 문제를 처리하고 다른 문제를 위쪽으로 전달하고 그 자체 만 처리 할 수 ​​있습니다.
user470365
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.