예외 : 왜 일찍 던지나요? 왜 늦게 잡을까요?


156

격리 예외 처리에 대한 잘 알려진 모범 사례가 많이 있습니다. 나는 "해야 할 것과하지 말아야 할 것"을 충분히 알고 있지만 더 큰 환경에서 모범 사례 나 패턴에 관해서는 상황이 복잡해집니다. "얼마나 일찍 늦게까지 잡아라"-여러 번 들었지만 여전히 혼란 스럽습니다.

하위 수준 계층에서 null 포인터 예외가 발생하면 왜 일찍 던지고 늦게 잡아야합니까? 상위 계층에서 왜 잡아야합니까? 비즈니스 계층과 같은 상위 수준에서 하위 수준 예외를 포착하는 것은 의미가 없습니다. 각 계층의 우려를 위반하는 것 같습니다.

다음 상황을 상상해보십시오.

그림을 계산하는 서비스가 있습니다. 그림을 계산하기 위해 서비스는 저장소에 액세스하여 원시 데이터 및 일부 다른 서비스가 계산을 준비하도록합니다. 데이터 검색 레이어에서 문제가 발생하면 왜 DataRetrievalException을 더 높은 수준으로 던져야합니까? 반면에 예외를 의미있는 예외 (예 : CalculationServiceException)로 랩핑하는 것을 선호합니다.

왜 일찍 던지고 왜 늦게 잡는가?


104
"늦게 잡기"의 개념은 가능한 한 빨리 잡는 것이 아니라 가능한 한 빨리 잡는 것입니다. 예를 들어 파일 파서가있는 경우 파일을 찾을 수없는 지점이 없습니다. 그와 무슨 관계가 있습니까? 복구 경로는 무엇입니까? 하나도 없으므로 잡지 마십시오. 당신의 어휘 분석기로 가십시오, 당신은 거기서 무엇을합니까, 프로그램을 계속할 수 있도록 어떻게 이것을 복구합니까? 그것은 예외를 통과시키지 못합니다. 스캐너가이를 어떻게 처리 할 수 ​​있습니까? 통과시킬 수 없습니다. 호출 코드는 이것을 어떻게 처리 할 수 ​​있습니까? 다른 파일 경로를 시도하거나 사용자에게 경고 할 수 있으므로 catch하십시오.
Phoshi

16
NullPointerException (NPE가 의미하는 것으로 가정)이 발생하는 경우는 거의 없습니다. 가능하면 우선 피해야합니다. NullPointerExceptions가 발생하면 수정해야 할 코드가 깨진 것입니다. 쉽게 고칠 수도 있습니다.
Phil

6
이 질문을 중복으로 끝내라고 제안하기 전에 다른 질문에 대한 답변 이이 질문에 잘 대답하지 않는지 확인하십시오.
Doc Brown

1
(Citebot) today.java.net/article/2003/11/20/… 이것이 인용의 기원이 아닌 경우 가장 가능성이 높은 인용이라고 생각되는 출처에 대한 참조를 제공하십시오.
rwong

1
누구나이 질문에 도달하고 Android 개발을하고 있음을 상기시켜줍니다. Android에서는 예외가 처음 발견 된 것과 동일한 기능으로 로컬에서 예외를 포착하고 처리해야합니다. 이는 예외가 메시지 핸들러간에 전파되지 않기 때문에 발생하는 경우 애플리케이션이 종료됩니다. 따라서 Android 개발을 수행 할 때이 조언을 인용해서는 안됩니다.
rwong

답변:


118

내 경험상 오류가 발생하는 시점에서 예외를 던지는 것이 가장 좋습니다. 예외가 발생한 이유를 가장 잘 알고 있기 때문에이 작업을 수행합니다.

예외로 인해 계층이 백업 될 때 catch 및 rethrowing은 예외에 컨텍스트를 추가하는 좋은 방법입니다. 이는 다른 유형의 예외를 발생시키는 것을 의미 할 수 있지만이를 수행 할 때 원래 예외를 포함하십시오.

결국 예외는 코드 흐름에 대한 결정을 내릴 수있는 계층에 도달합니다 (예 : 사용자에게 조치 요청). 이 시점에서 마지막으로 예외를 처리하고 정상적인 실행을 계속해야합니다.

코드 기반에 대한 실습과 경험을 통해 오류에 컨텍스트를 추가 할시기와 실제로 가장 합리적인 위치를 판단하는 것이 매우 쉬워집니다.

캐치 → 다시 던지기

개발자가 문제를 이해하기 위해 모든 계층을 통해 작업해야하는 정보를 유용하게 추가 할 수있는 위치에서이 작업을 수행하십시오.

캐치 → 핸들

소프트웨어를 통한 적절하지만 다른 실행 흐름에 대한 최종 결정을 내릴 수있는 위치에서이를 수행하십시오.

캐치 → 오류 반환

이것이 적절한 상황이 있지만 Catch → Rethrow 구현으로 리팩토링하기 위해 예외를 포착하고 호출자에게 오류 값을 리턴하는 것을 고려해야합니다.


예, 나는 이미 오류를 알고 있으며 오류가 발생한 시점에서 예외를 던져야한다는 것을 알고 있습니다. 그런데 왜 NPE를 잡아서 Stacktrace 위로 올라가지 말아야합니까? 나는 항상 NPE를 잡아서 의미있는 예외로 포장합니다. 또한 서비스 또는 UI 계층까지 DAO 예외를 발생시켜야하는 이점을 볼 수 없습니다. 나는 항상 그것을 서비스 계층에서 잡아서 추가 세부 정보와 함께 서비스 예외로 래핑합니다. 왜 서비스 호출이 실패했는지.
shylynx

8
@shylynx 예외를 포착 한 다음보다 의미있는 예외를 다시 던지는 것이 좋습니다. 하지 말아야 할 일은 너무 일찍 예외를 잡아서 다시 던지지 않는 것입니다. 그 말이 경고하는 실수는 너무 일찍 예외를 잡아서 코드의 잘못된 계층에서 처리하려고 시도하는 것입니다.
Simon B

예외를받는 시점에 컨텍스트를 명확하게하면 팀의 개발자가 더 쉽게 생활 할 수 있습니다. NPE는 문제를 이해하기 위해 추가 조사가 필요합니다
Michael Shaw

4
@shylynx 하나는 "당신은 왜 코드를 던질 있는 요점 이 NullPointerException있습니까? 왜 나쁜 사람 이 어디로 갔는지 정확하게 알 수 있도록 null예외를 IllegalArgumentException더 빨리 확인 하고 던지지 않는 이유 는 무엇입니까 null?" 나는 그 말의 "초기 던져"부분이 제안 할 것이라고 생각합니다.
jpmc26

2
@jpmc 나는 계층과 예외에 대한 우려를 강조하기 위해 NPE를 예로 들었다. IllegalArgumentException으로 바꿀 수도 있습니다.
shylynx

56

원인을 찾기가 더 쉽기 때문에 가능한 빨리 예외를 처리하려고합니다. 예를 들어, 특정 인수로 실패 할 수있는 방법을 고려하십시오. 인수의 유효성을 검사하고 메소드의 맨 처음에 실패하면 호출 코드에 오류가 있음을 즉시 알 수 있습니다. 실패하기 전에 인수가 필요할 때까지 기다리는 경우 실행을 수행하여 버그가 호출 코드 (잘못된 인수)에 있는지 또는 메소드에 버그가 있는지 알아 내야합니다. 더 일찍 예외를 던질수록 근본적인 원인에 더 가깝고 문제가 발생한 위치를 파악하기가 더 쉽습니다.

예외가 높은 수준에서 처리되는 이유는 낮은 수준이 오류를 처리하기위한 적절한 조치 과정을 모르기 때문입니다. 실제로 호출 코드가 무엇인지에 따라 동일한 오류를 처리하는 여러 가지 적절한 방법이있을 수 있습니다. 예를 들어 파일을 열어보십시오. 구성 파일을 열려고하는데없는 경우 예외를 무시하고 기본 구성을 진행하는 것이 적절한 응답 일 수 있습니다. 프로그램 실행에 중요한 개인 파일을 여는 중이고 누락 된 경우 유일한 옵션은 아마도 프로그램을 닫는 것입니다.

올바른 유형의 예외를 감싸는 것은 순수한 직교 관심사입니다.


1
다른 레벨이 중요한 이유를 명확하게 설명하는 +1 파일 시스템 오류에 대한 훌륭한 예입니다.
Juan Carlos Coto

24

다른 사람들은 왜 일찍 던지는 지 꽤 잘 요약했습니다 . 대신 늦게 잡는 이유에 집중하도록하겠습니다 . 그 맛에 대해서는 만족스러운 설명을 보지 못했습니다.

예외는 무엇입니까?

왜 예외가 처음에 존재하는지에 대해서는 상당히 혼란스러워 보입니다. 예외의 이유와 예외 처리는 ... ABSTRACTION 입니다.

다음과 같은 코드를 보셨습니까?

static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

그것은 예외가 사용되는 방식이 아닙니다. 위와 같은 코드는 실제로 존재하지만 수차에 가깝고 실제로 예외입니다. 예를 들어 순수한 수학에서도 나눗셈 의 정의 는 조건부입니다. 입력 도메인을 제한하기 위해 예외적으로 0을 처리해야하는 것은 항상 "호출자 코드"입니다. 못 생겼어 항상 발신자에게 고통입니다. 그럼에도 불구하고 그러한 상황에서 점검 후 패턴은 자연스럽게 갈 수있는 방법입니다.

static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

또는 다음과 같이 OOP 스타일에 대한 특공대를 취할 수 있습니다.

static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

보시다시피, 호출자 코드는 사전 점검의 부담이 있지만 이후 예외 처리는 수행하지 않습니다. 이 경우 ArithmeticException지금 전화에서 오는 divide또는 eval다음, 그 것이다 당신은 당신이를 잊었 때문에, 예외 처리를하고 코드를 수정하는 사람 check(). 비슷한 이유로 a를 잡는 NullPointerException것은 거의 항상 잘못된 일입니다.

이제 메소드 / 함수 서명에서 예외적 인 경우를보고 싶다고 말하는 사람들이 있습니다. 즉, 명시 적으로 출력 도메인을 확장하려는 것 입니다. 그들은 예외 를 선호하는 사람들입니다 . 물론 출력 도메인을 변경하면 직접 호출자 코드를 강제로 적용해야하며 실제로는 예외를 확인해야합니다. 그러나 예외는 필요하지 않습니다! 그렇기 때문에 Nullable<T> 일반 클래스 , 사례 클래스 , 대수 데이터 형식공용체 형식이 있습니다 . 일부 OO 사람들 은 다음과 같은 간단한 오류 사례 로 돌아 가기 null선호 할 수도 있습니다 .

static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

기술적으로 예외 위와 같은 목적으로 사용될 있지만 여기에 요점이 있습니다 . 그러한 용도에는 예외가 없습니다 . 프로 추상화는 예외입니다. 간접적 인 예외는 예외입니다. 예외는 직접 클라이언트 계약 을 위반하지 않고 "결과"도메인을 확장 하고 오류 처리를 "어딘가에"지연 시키는 것을 허용합니다 . 코드 사이에 추상화 계층이없는 동일한 코드의 직접 호출자에서 처리되는 예외가 발생하면 잘못 하고 있습니다.

늦게 잡는 방법?

그래서 여기 있습니다. 위의 시나리오에서 예외를 사용하는 것이 예외가 사용되는 방식이 아니라는 것을 보여주기 위해 내 길을 주장했습니다. 그러나 예외 처리에 의해 제공되는 추상화 및 간접적 인 지시가 반드시 필요한 실제 사용 사례가 있습니다. 이러한 사용을 이해하는 것은 이해하는 데 도움이됩니다 캐치에게 말 도 추천.

유스 케이스는 다음과 같습니다. 자원 추상화에 대한 프로그래밍 ...

예, 비즈니스 로직은 구체적인 구현이 아닌 추상화대해 프로그래밍 해야합니다 . 최상위 레벨 IOC "배선"코드는 자원 추상화의 구체적인 구현을 인스턴스화하여 비즈니스 로직으로 전달합니다. 여기에 새로운 것은 없습니다. 그러나 이러한 자원 추상화의 구체적인 구현으로 인해 구현 고유의 예외 가 발생할 수 있습니다.

그렇다면 누가 구현 관련 예외를 처리 할 수 ​​있습니까? 비즈니스 로직에서 자원 별 예외를 처리 할 수 ​​있습니까? 아뇨. 비즈니스 로직은 추상화에 대해 프로그래밍되며, 구현 특정 예외 세부 사항에 대한 지식은 제외됩니다.

"아하!"라고 말할 수 있습니다. "그렇기 때문에 예외를 서브 클래 싱하고 예외 계층을 만들 수 있습니다"( Spring !를 확인하십시오 ). 내가 말해 줄게, 그것은 잘못된 것입니다. 첫째, OOP에 대한 모든 합리적인 책은 구체적인 상속이 나쁘다고 말하지만 어떻게 든 JVM의 핵심 구성 요소 인 예외 처리는 구체적인 상속과 밀접한 관련이 있습니다. 아이러니하게도 Joshua Bloch 는 작동하는 JVM에 대한 경험을 얻기 전에 효과적인 Java 책 을 작성할 수 없었습니다. 이 책은 다음 세대를위한 "학습자가 배우는"책입니다. 둘째, 더 중요한 것은 예외적 인 예외를 잡으면 어떻게 처리 할 것인가?PatientNeedsImmediateAttentionException: 그녀에게 약을 주거나 다리를 절단해야합니까!? 가능한 모든 서브 클래스에 대한 switch 문은 어떻습니까? 당신의 다형성이 있고, 추상화가 있습니다. 당신은 요점을 얻었다.

그렇다면 누가 리소스 별 예외를 처리 할 수 ​​있습니까? 결석을 아는 사람이어야합니다! 자원을 인스턴스화 한 사람! 물론 "배선"코드! 이것 좀 봐:

추상화에 대해 코딩 된 비즈니스 로직 ... 정확한 리소스 오류 처리가 없습니다!

static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}

한편 다른 곳에서 구체적인 구현은 ...

static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}

그리고 마지막으로 배선 코드 ... 누가 구체적인 자원 예외를 처리합니까? 그들에 대해 아는 사람!

static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}

이제 나와 함께하십시오. 위의 코드 단순합니다. 여러 범위의 IOC 컨테이너 관리 자원이있는 엔터프라이즈 애플리케이션 / 웹 컨테이너가 있고 세션 또는 요청 범위 자원 등의 자동 재시도 및 재 초기화가 필요하다고 말할 수 있습니다. 하위 레벨 범위의 배선 로직에는 정확한 구현을 알지 못하는 리소스를 만듭니다. 더 높은 수준의 스코프 만이 이러한 낮은 수준의 리소스가 어떤 예외를 throw 할 수 있는지 알 수 있습니다. 잠깐만!

불행하게도 예외는 호출 스택을 통한 간접적 인 허용 만 허용하며 서로 다른 카디널리티를 가진 다른 범위는 일반적으로 여러 다른 스레드에서 실행됩니다. 예외를 제외하고는 의사 소통 할 방법이 없습니다. 여기에 더 강력한 것이 필요합니다. 답변 : 비동기 메시지 전달 . 하위 수준 범위의 루트에서 모든 예외를 포착하십시오. 아무것도 무시하지 말고 미끄러지지 마십시오. 현재 범위의 호출 스택에 생성 된 모든 리소스가 닫히고 처리됩니다. 그런 다음 예외 처리 루틴에서 메시지 대기열 / 채널을 사용하여 Concretion이 알려진 수준에 도달 할 때까지 오류 메시지를 더 높은 범위로 전파하십시오. 그것을 처리하는 방법을 알고있는 사람입니다.

서머 서머 럼

그래서 내 해석에 따라 캐치 후반에 가장 편리한 장소에서 예외를 포착하는 것을 의미 더 이상 추상을 깨는되지 않습니다 . 너무 일찍 잡지 마십시오! 자원 추상화의 인스턴스를 발생시키는 구체적인 예외를 생성하는 계층에서 예외를 포착하십시오.이 계층은 추상화의 생성을 알고 있습니다. "배선"레이어.

HTH. 행복한 코딩!


인터페이스를 제공하는 코드가 인터페이스를 사용하는 코드보다 잘못 될 수있는 것에 대해 더 많이 알고 있지만 메소드가 동일한 인터페이스 유형의 두 가지 자원을 사용하고 실패를 다르게 처리해야한다고 가정하십니까? 또는 제작자에게 알려지지 않은 구현 세부 사항으로 이러한 리소스 중 하나가 내부적으로 동일한 유형의 다른 중첩 리소스를 사용하는 경우? 비즈니스 계층을 던지 WrappedFirstResourceException거나 WrappedSecondResourceException문제의 근본 원인을 파악하기 위해 해당 예외 내부를 살펴보기 위해 "배선"계층이 필요합니다.
supercat

... icky 일 수 있지만 FailingInputResource예외가 작업의 결과 라고 가정하는 것보다 낫습니다 in1. 실제로, 많은 경우에 올바른 접근 방식은 배선 계층이 예외 처리 오브젝트를 전달하고 비즈니스 계층에 catch해당 오브젝트의 handleException메소드 를 호출하도록 하는 것입니다. 이 방법은 기본 데이터를 다시 던지거나 제공하거나 "중지 / 재시도 / 실패"프롬프트를 표시하고 운영자가 응용 프로그램의 요구 사항에 따라 수행 할 작업 등을 결정할 수 있도록합니다.
supercat

@ supercat 나는 당신이 무슨 말을하는지 이해합니다. 구체적인 자원 구현이 예외를 처리 할 책임이 있다고 말합니다. 모든 것을 지정할 필요는 없지만 ( 정의되지 않은 동작 이라고 함 ) 모호성이 없는지 확인해야합니다. 또한 확인되지 않은 런타임 예외도 문서화해야합니다. 문서와 모순되면 버그입니다. 호출자 코드가 예외에 대해 합리적인 조치를 취할 것으로 예상되는 경우 최소한 UnrecoverableInternalExceptionHTTP 500 오류 코드와 유사한 리소스로 리소스를 래핑해야 합니다.
Daniel Dinnyes

@supercat 설정 가능한 에러 핸들러에 대한 제안 : 정확히! 마지막 예제에서 오류 처리 로직은 하드 코드되어 정적 doMyBusiness메소드를 호출합니다 . 이것은 간결성을위한 것이며보다 역동적으로 만들 수 있습니다. 이러한 Handler클래스는 일부 입력 / 출력 리소스로 인스턴스화되며을 handle구현하는 클래스를받는 메서드가 ReusableBusinessLogicInterface있습니다. 그런 다음 위의 어딘가에있는 배선 계층에서 서로 다른 핸들러, 리소스 및 비즈니스 로직 구현을 사용하도록 결합 / 구성 할 수 있습니다.
Daniel Dinnyes

10

이 질문에 올바르게 대답하기 위해 물러서서 더 근본적인 질문을하겠습니다.

처음에 예외가있는 이유는 무엇입니까?

우리는 메소드 호출자에게 요청받은 것을 할 수 없다는 것을 알리기 위해 예외를 던집니다. 예외 유형에 따라 원하는 작업을 수행 할 수없는 이유 가 설명 됩니다.

몇 가지 코드를 살펴 보자.

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

이 코드 PropertyB는 null 인 경우 분명히 null 참조 예외를 throw 할 수 있습니다 . 이 경우이 상황에 대해 "수정"하기 위해 할 수있는 두 가지가 있습니다. 우리는 할 수 있었다 :

  • PropertyB가 없으면 자동으로 PropertyB를 작성하십시오. 또는
  • 예외가 호출 메소드까지 버블 링되도록하십시오.

여기에 PropertyB를 작성하는 것은 매우 위험 할 수 있습니다. 이 메소드가 PropertyB를 작성해야하는 이유는 무엇입니까? 분명히 이것은 단일 책임 원칙에 위배됩니다. 아마도 PropertyB가 존재하지 않으면 문제가 발생한 것입니다. 부분적으로 생성 된 객체에서 메서드를 호출하거나 PropertyB가 null로 잘못 설정되었습니다. 여기에 PropertyB를 작성하면 나중에 데이터를 손상시키는 버그와 같이 훨씬 더 큰 버그를 숨길 수 있습니다.

대신 널 참조 버블을 내 놓으면이 메소드를 호출 한 개발자에게 가능한 한 빨리 무언가 잘못되었음을 알리는 것입니다. 이 메소드를 호출하는 필수 전제 조건이 누락되었습니다.

사실상, 우리는 우려를 훨씬 더 잘 분리시키기 때문에 일찍 던지고 있습니다. 결함이 발생하자마자, 우리는 업스트림 개발자에게 그 사실을 알려줍니다.

우리가 "늦게 잡는"이유는 다른 이야기입니다. 우리는 실제로 늦게 붙잡기를 원하지 않고 문제를 올바르게 처리하는 방법을 알고있는 한 빨리 붙잡기를 원합니다. 이 중 일부는 나중에 15 개의 추상화 계층이되고 일부는 작성 시점에있게됩니다.

요점은 추상화 계층에서 예외를 포착하여 예외를 올바르게 처리하는 데 필요한 모든 정보가있는 지점에서 예외를 처리 할 수 ​​있다는 것입니다.


나는 당신이 잘못된 의미에서 업스트림 개발자 를 사용하고 있다고 생각합니다 . 또한 단일 책임 원칙을 위반한다고 말했지만 실제로 많은 온 디맨드 초기화 및 값 캐싱이 그런 방식으로 구현됩니다 (물론 적절한 동시 제어를 통해)
Daniel Dinnyes

주어진 예에서 다음과 같이 빼기 연산 전에 null을 검사하는 것은 if(PropertyB == null) return 0;
어떻습니까?

1
당신은 또한 당신의 마지막 단락에 정교한, 특히 당신은 '무엇을 의미합니까 추상화 계층 '.
user1451111

IO 작업을 수행하는 경우 IO 예외를 포착하기위한 추상화 계층이 작업을 수행하는 위치입니다. 이 시점에서 메시지 상자를 다시 시도하거나 사용자에게 던지거나 기본값 집합을 사용하여 개체를 만들지 여부를 결정하는 데 필요한 모든 정보가 있습니다.
Stephen

"주어진 예제에서 빼기 연산 전에 null을 확인하는 방법은 다음과 같습니다. if (PropertyB == null) return 0;" 왝. 그것은 호출 방법에 내가 빼야 할 유효한 것들이 있다고 말하는 것입니다. 물론 이것은 상황에 따라 다르지만 대부분의 경우 내 오류 확인을 수행하는 것은 좋지 않습니다.
Stephen

6

객체를 유효하지 않은 상태로 두지 않도록 던질 가치가있는 것을 보자 마자 던져라. Null 포인터가 전달되면 일찍 포인터를 확인하고 NPE 던져 낮은 수준으로 떨어질 수 있습니다.

유효하지 않은 매개 변수가 전달 된 경우 매개 변수를 제공 한 계층이 결과를 처리해야하는 경우 오류를 해결하기 위해 수행 할 조치를 알게되면 즉시 포착하십시오 (일반적으로 if-else를 사용할 수있는 경우는 아닙니다). .


1
당신은 썼다 : 빨리 던져, ... 빨리 잡아 ...! 왜? "초기 던져, 늦게 잡아라"와는 대조적으로 그것은 완전히 반대되는 접근법입니다.
shylynx

1
@shylynx 나는 "얼마나 일찍 던지고, 늦게 붙잡는"곳에서 왔는지 모르겠지만 그 가치는 의심 스럽다. "늦게"붙잡는 것은 정확히 무엇을 의미합니까? 예외를 잡는 것이 합리적 일 경우 문제에 달려 있습니다. 분명한 것은 가능한 한 빨리 문제를 감지하고 던지기를 원한다는 것입니다.
Doval

2
"catch late"는 오류를 해결하기 위해해야 ​​할 일을 알기 전에 포착하는 방식과 대조되는 것으로 가정합니다. 예를 들어 오류 메시지를 인쇄 한 다음 예외를 다시 발생시킬 수 있도록 모든 것을 포착하는 함수가 표시되는 경우가 있습니다.

@Hurkyl : "catch late"의 문제점은 예외에 대해 아무것도 모르는 레이어를 통해 예외가 발생하면 상황에 대해 무언가를 수행하는 것이 어려울 수 있다는 것입니다. 예상했다. 간단한 예로, 사용자 문서 파일에 대한 파서가 디스크에서 CODEC를로드해야하고 디스크 오류를 읽는 동안 디스크 오류가 발생한다고 가정하면 파서를 호출하는 코드는 사용자를 읽는 동안 디스크 오류가 있다고 생각하면 부적절하게 작동 할 수 있습니다 문서.
supercat

4

유효한 비즈니스 규칙은 '하위 레벨 소프트웨어가 값을 계산하지 못하면 ...'입니다.

이것은 상위 레벨에서만 표현 될 수 있습니다. 그렇지 않으면 하위 레벨 소프트웨어는 자체 정확성을 기준으로 동작을 변경하려고합니다.


2

우선 예외적 인 상황은 예외입니다. 예를 들어, 원시 데이터를로드 할 수 없어서 존재하지 않으면 수치를 계산할 수 없습니다.

내 경험상 스택을 걷는 동안 예외를 추상화하는 것이 좋습니다. 일반적으로이 작업을 수행하려는 지점은 예외가 두 레이어 사이의 경계를 넘을 때마다 발생합니다.

데이터 영역에서 원시 데이터를 수집하는 중 오류가 발생하면 데이터를 요청한 사람에게 알리기 위해 예외를 발생시킵니다. 이 문제를 해결하려고 시도하지 마십시오. 처리 코드의 복잡성은 매우 높을 수 있습니다. 또한 데이터 계층은 데이터 요청에만 책임이 있으며이 작업을 수행하는 동안 발생하는 오류를 처리하지는 않습니다. 이것이 "초기 던져" 라는 의미 입니다.

귀하의 예에서 잡기 계층은 서비스 계층입니다. 서비스 자체는 데이터 액세스 계층 위에있는 새로운 계층입니다. 따라서 예외를 잡으려고합니다. 서비스에 장애 조치 인프라가 있고 다른 리포지토리에서 데이터를 요청하려고 할 수 있습니다. 이것도 실패하면, 서비스 호출자가 이해하는 것 (예 : 웹 서비스 인 경우 SOAP 결함 일 수 있음)으로 예외를 랩하십시오. 이후의 레이어가 정확히 무엇이 잘못되었는지 기록 할 수 있도록 원래 예외를 내부 예외로 설정하십시오.

서비스를 호출하는 계층 (예 : UI)이 서비스 오류를 포착 할 수 있습니다. 그리고 이것이 "catch late"를 의미합니다 . 하위 계층에서 예외를 처리 할 수 ​​없으면 다시 throw하십시오. 최상위 레이어가 예외를 처리 할 수 ​​없으면 처리하십시오! 로깅 또는 프리젠 테이션이 포함될 수 있습니다.

예를 들어 포인터가 잘못된 메모리를 가리 키기 때문에 오류가 있음을 사용자가 이해할 수 없기 때문에 (예 :보다 일반적인 예외로 래핑하여 위에서 설명한 것처럼) 예외를 다시 발생시켜야하는 이유는 다음과 같습니다. 그리고 그는 상관하지 않습니다. 그는 단지 서비스에 의해 수치를 계산할 수 없다는 점만 신경 쓰고 이것은 그에게 표시되어야하는 정보입니다.

학습과 당신은 (이상적으로) 완전히 남길 수 있습니다 간다 try/ catch는 UI에서 코드를. 대신 하위 계층에서 발생할 수있는 예외를 이해할 수있는 전역 예외 처리기를 사용하고 일부 예외를 로그에 기록한 다음 오류에 대한 의미있는 (및 지역화 된) 정보가 포함 된 오류 개체로 래핑합니다. 이러한 개체는 원하는 모든 형식 (메시지 상자, 알림, 메시지 토스트 등)으로 사용자에게 쉽게 제공 될 수 있습니다.


1

일반적으로 조기에 예외를 던지는 것은 깨진 계약이 필요 이상으로 코드를 통해 흐르지 않게하기위한 좋은 습관입니다. 예를 들어, 특정 함수 매개 변수가 양의 정수가 될 것으로 예상되는 경우 해당 변수가 코드 스택의 다른 곳에서 사용될 때까지 기다리지 않고 함수 호출 시점에서 해당 제한 조건을 적용해야합니다.

늦게 잡기 자신의 규칙이 있고 프로젝트마다 바뀌기 때문에 실제로 의견을 말할 수 없습니다. 그래도 한 가지 예외는 두 그룹으로 예외를 분리하는 것입니다. 하나는 내부 용이고 다른 하나는 외부 용입니다. 내부 예외는 내 자신의 코드에 의해 처리되고 처리되며 외부 예외는 코드를 호출하는 모든 코드에 의해 처리됩니다. 이것은 기본적으로 나중에 물건을 잡는 형태이지만 내부 코드에서 필요할 때 규칙을 벗어날 수있는 유연성을 제공하기 때문에별로 아닙니다.

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