[Java] 예외의 계층구조 - 엉뚱한 예외가 잡힐 때

도비·2025년 2월 5일

Java

목록 보기
1/6

문제 상황

JUnit으로 테스트코드를 짜던 중 다른 예외를 잡는 문제가 발생했다. 문제가 된 코드는 다음과 같다.

@Test
void Level3_emptyValueTest() {
	assertSimpleTest(() -> {
    	try {
        	run(테스트케이스 입력부);
            assertThat(output()).contains(결과);
        } catch (IllegalArgumentException e) {
        	assertThat(e).isInstanceOf(IllegalArgumentException.class);
        }
    });
}

코드 내용은 테스트케이스 입력부를 넣으면 결과가 나오거나 IllegalArgumentException을 던지는지 확인하는 것이다. 일반적으로 잘 실행되었으나 오버플로우가 발생했을 때, NumberFormatException을 예외로 던졌는데 테스트가 성공했다. 문제 상황을 더 가시적으로 보기 위해 테스트를 해봤다.

public class Application {
    public static void main(String[] args) {
        exceptionTest();
    }

    static void exceptionTest() {
        try {
            throwNumberFormatException();
        } catch (IllegalArgumentException exception) {
            System.out.println("IllegalArgumentException");
        } catch (Exception exception) {
            System.out.println("Else");
        }
    }

    static void throwNumberFormatException() {
        throw new NumberFormatException();
    }
}

실행 결과는 IllegalArgumentException 이었다. 결론부터 말하자면 NumberFormatExceptionIllegalArgumentException를 상속해 구현되었기 때문에, 다형성을 지니는 자바의 클래스 특성상 하위 타입이 상위 타입으로 치환될 수 있기에 상위 타입을 잡아내는 코드에 걸린 것이었다.

자바의 예외

자바는 객체지향 언어인 만큼 예외까지도 객체지향적으로 설계되었다. 예외 역시 class로 구현되어있다. Exception 파일을 뜯어보면 다음과 같이 구현되어있는 것을 확인할 수 있다.

public class Exception extends Throwable {
...

Exception이 상속받고있는 Throwable 역시 class로 구현되어있다.

public class Throwable implements Serializable {
...

그리고 대망의 NumberFormatExceptionIllegalArguementException을 상속받아 구현되어있었다.

public class NumberFormatException extends IllegalArgumentException {
...

그리고 위의 Exception 클래스를 제외한 자바의 모든 예외들은 다른 예외를 상속받아 구현되어있다. 다음은 예외의 계층구조를 보여주는 간단한 도식이다.

즉 자바에서 예외는 계층구조를 가지고 있었다.

문제의 원인

API에서 말하기를 IllegalArguementException은 메서드에 부적절한 인수가 전달되었다는 것을 드러내기 위해 발생시키는 예외이고, NumberFormatException은 문자열을 숫자로 바꾸려고 시도했으나 인수인 문자열이 숫자 포멧이 아니라는 것을 말하기 위해 발생시키는 예외이다. 즉 변환 메서드의 인수인 문자열을 메서드에 넣었을 때 발생시키는 예외가 NumberFormatException이기에 이는 부적절한 인수를 발생할 때 던지는 IllegalArguementException를 상속한 것으로 생각된다.
결과적으로 NumberFormatExceptionIllegalArguementException를 상속받았기에 후자를 걸러낼 때 함께 걸러진다는 것이다.

해결 방법

거창한 해결방법을 찾지는 못했다. ExceptionThrowable을 상속하고, 이것 역시 class를 통해 구현되기에 class의 공통 조상인 Object의 메서드를 통해 해결하고자 했다.

@Test
void Level3_emptyValueTest() {
	assertSimpleTest(() -> {
    	try {
        	run(테스트케이스 입력부);
            assertThat(output()).contains(결과);
        } catch (IllegalArgumentException e) {
        	assertThat(e.getClass()).isEqualTo(IllegalArgumentException.class);
        }
    });
}

그렇다. 그냥 getClass()로 직접 비교했다. 결과적으로는 NumberFormatException 예외가 발생하면 테스트에 통과하지 못하고 IllegalArgumentException 만 테스트를 통과할 수 있게 되었다.

이 글의 주제에 대한 결론은 "엉뚱한 예외가 잡힐 때"라는 전제가 잘못되었다는 것이다. 다른 예외가 잡히긴 했으나 이는 적절하게 잡아낸 것이었다.

profile
문과 였던 것...

4개의 댓글

comment-user-thumbnail
2025년 2월 5일

우와아아 코드를 보니까 정말 토가 나와요 효과최고^_^

답글 달기
comment-user-thumbnail
2025년 2월 18일

아니 지금보니까 마지막 결론 무엇.. 웬만한 반전영화 뺨치네 식스센스급 벨로그 재밌게 읽었습니다^^

답글 달기
comment-user-thumbnail
2025년 2월 18일

assertThrows로 특정예외를 정확히 확인하는 것도 고려한건가용?? 이거쓰면 더 직관적으로 될 것 같은데
++ 예외테스트랑 출력테스트 분리해서

1개의 답글