JAVA/자바 코딩의 기술

5장 문제 발생에 대비하기

바디스 2023. 3. 6. 15:35

5-1 빠른 실패

class CruiseControl {
    static final double SPEED_OF_LIGHT_KMH = 1079252850;
    static final double SPEED_LIMIT = SPEED_OF_LIGHT_KMH;

    private double targetSpeedKmh;

    void setTargetSpeedKmh(double speedKmh) {
        if (speedKmh < 0) {
            throw new IllegalArgumentException();
        } else if (speedKmh <= SPEED_LIMIT) {
            targetSpeedKmh = speedKmh;
        } else {
            throw new IllegalArgumentException();
        }
    }
}

위의 코드는 앞뒤로 두개의 예외처리 분기에 둘러싸여있어 정상적인 제어 흐름 경로가 눈에 잘 들어오지 않습니다. 따라서 매개변수 검증 사이에서 정상적인 경로를 찾는데 시간이 낭비하게 됩니다.
이런식의 낭비를 줄이기 위해서 실패 상황을 빠르게 처리하여 메서드 전체를 읽고 이해하기 쉽게 하는 것이 좋습니다.

class CruiseControl {
    static final double SPEED_OF_LIGHT_KMH = 1079252850;
    static final double SPEED_LIMIT = SPEED_OF_LIGHT_KMH;

    private double targetSpeedKmh;

    void setTargetSpeedKmh(double speedKmh) {
        // 매개변수 검증
        if (speedKmh < 0 || speedKmh > SPEED_LIMIT) {
            throw new IllegalArgumentException();
        }

    // 일반적인 경로
        targetSpeedKmh = speedKmh;
    }
}

위의 코드 처럼 매개변수 검증과 일반적인 경로를 분리하여 예외 상황일 때 빠르게 실패 하도록 하는 것이 좋습니다.

5-2 항상 구체적인 예외 잡기

class TransmissionParser {
    static Transmission parse(String rawMessage) {
        if (rawMessage != null
                && rawMessage.length() != Transmission.MESSAGE_LENGTH) {
            throw new IllegalArgumentException("Bad message received!");
        }

        String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
        String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
        try {
            // rawId 문자열 안에 int형으로 바꿀 수 없는 문자가 있을 때 오류가 발생!!
            int id = Integer.parseInt(rawId);
            String content = rawContent.trim();
            return new Transmission(id, content);
        } catch (Exception e) {
            throw new IllegalArgumentException("Bad message received!");
        }
    }
}

자바의 예외는 복잡한 타입 계층 구조를 띄우고 있기에 예외를 잡으려면 항상 구체적인 예외 타입을 잡아야 합니다. 일반적으린 타입을 잡으면 잡아선 안 될 오류 까지 잡힐 위험이 있습니다.
위의 코드는 Integer의 parserInt에서 오류가 발생 할 때 예외를 처리하기 위해 기본적인 예외 Exception을 사용하였습니다. 하지만 이 경우 parserInt의 예외 뿐만아니라 OutofMemortError 같은 머신의 오류에서도 예외처리가 발생하게 됩니다.

class TransmissionParser {
    static Transmission parse(String rawMessage) {
        if (rawMessage != null &&
                rawMessage.length() != Transmission.MESSAGE_LENGTH) {
            throw new IllegalArgumentException("Bad message received!");
        }

        String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
        String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
        try {
            // 예외 발생!!
            int id = Integer.parseInt(rawId);
            String content = rawContent.trim();
            return new Transmission(id, content);
        } catch (NumberFormatException e) { // 예외 구체화
            throw new IllegalArgumentException("Bad message received!");
        }
    }
}

이를 방지하기 위해서 구체적인 예외를 잡도록 해야합니다. parseInt에서 발생하게 될 NumberFormatException으로 예외처리를 구체적을 잡는 편이 좋습니다.

5-3 메시지를 자세하게 적는다.

예외가 발생하면 메시지를 보고 처리할 수 있도록 어떤 상황에서 예외가 발생했는지 1~2문장 정도로 간결하게 잘 적어둬야 합니다. 단 너무 장황하게 쓰지는 말아야 합니다.

class TransmissionParser {
    static Transmission parse(String rawMessage) {
        if (rawMessage != null
                && rawMessage.length() != Transmission.MESSAGE_LENGTH) {
            throw new IllegalArgumentException(
                // 메시지 구체화
                String.format("Expected %d, but got %d characters in '%s'",
                    Transmission.MESSAGE_LENGTH, rawMessage.length(),
                    rawMessage));
        }

        String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
        String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
        try {
            int id = Integer.parseInt(rawId);
            String content = rawContent.trim();
            return new Transmission(id, content);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(
                // 메시지 구체화
                String.format("Expected number, but got '%s' in '%s'",
                        rawId, rawMessage));
        }
    }
}

위의 코드는 IllegalArgumentException를 2번 throw 하지만 각 예외처리 메시지를 다르게 해서 어떤 원인인지 자세하게 파악하도록 하였습니다.

5-4 원인 사슬 깨지 않기

예외는 또다른 예외를 발생 시킬수 있습니다. 이 경우 새로운 예외를 던져야 합니다.

try {
} catch (NumberFormatException e) {
    // BAD! Cause chain interrupted!
    throw new IllegalArgumentException(e.getCause());
}

언뜻 보면 괜찮아 보입니다. e.getCause()를 통해 원인을 넘겨 줍니다. 하지만 NumberFormatException이 throw에서 빠짐으로 원인만 연결되고 예외가 연결 되지는 않는 상황입니다. 잘못된 생략으로 오해를 부를 수 있는 상황입니다.

try {

} catch (NumberFormatException e) {
    throw new IllegalArgumentException("Message", e);
}

그래서 새로운 예외를 발생시키는 경우 예외를 통채로 원인으로 전달 하는 것이 원인 사슬을 끊지 않고 전달 할 수 있는 방법입니다.