본문 바로가기
Java/Java Basic

[Java Basic] 24. 예외 발생시키기와 사용자 지정 예외 생성

by Rosmary 2022. 8. 8.
728x90
반응형

 

 

 

지난 포스팅에 이어 Java 예외와 관련된 내용을 계속 진행한다. 이번 포스팅에서는 Java의 패키지에 정의된 예외를 일부러 발생시키는 예외 발생(throw exception)과 사용자 지정 예외를 생성하는 방법에 대해 알아보려 한다.

 

 

 

1. 예외 발생시키기 - throw 문

 

작성한 코드에 문제가 없음에도 예외를 일부러 일으킬 수 있는 방법이 있다. Java는 throw라는 이름의 키워드를 제공하는데, 해당 키워드는 Java의 예외 클래스 인스턴스를 catch 문으로 던지도록 유도하는 역할을 한다. 따라서 throw문은 반드시 try 문 블럭 내에서만 사용이 가능하다.

 

throw 문은 아래와 같이 사용한다.

 

try

{

    Exception_클래스명 참조변수명 = new Exception_클래스명 ("예외 설명 문구를 문자열로 입력");

    throw 참조변수명;

}

catch(Exception_클래스명 인스턴스매개변수명)

{...}

 

try 문 안의 내용을 보면 Exception 클래스의 인스턴스 생성 후, 인스턴스를 throw로 던지게 되어 있다. 이 두 줄의 코드를 한 줄로 아래와 같이 변환할 수 있다.

 

throw new Exception_클래스명("예외 설명 문구를 문자열로 입력");

 

실제로 코드를 작성하여 결과를 확인해보자.

 

 

위의 코드에서, 필자는 에러가 발생할만한 껀덕지를 주지 않았음에도 불구하고, throw 문에 의해 catch 문 내부의 코드가 실행되는 것을 확인할 수 있다. 코드 작성자가 일부러 예외를 일으켰다는 것만 제외한다면 일반적인 예외와 동일하다.

 

throw 문으로 예외를 일부러 발생시키면, try 문 내에서 throw 문 아래로 코드 작성 시 에러가 발생한다. Java는 throw로 인해 개발자가 일부러 예외를 일으켰다고 판단하기 때문에 throw 문 아래로 작성되는 코드는 불필요한 코드라 생각해서 에러를 출력하는 것이다.

 

 

 

예외 클래스의 인스턴스 생성 시, 인자로 예외에 대한 설명을 문자열로 작성하게 되어 있다. 이 문자열은 앞의 포스팅에서 보았던 getMessage() 매서드의 반환값이다. 

 

 

 

2. 매서드에서 예외 되던지기 - throws

 

Java에는 기본 패키지인 java.lang 내에 Thread라는 클래스가 존재한다. 이 클래스는 머지않은 미래에  멀티쓰레드라는 녀석에 대해 필자가 포스팅 할 때 다시 만나긴 할 것이나, 매서드에서의 예외 사용과 연관이 있어 잠깐 Thread로 예시를 들어보려 한다.

 

Thread 클래스 내에 정의된 매서드 중에 sleep() 이라는 매서드가 있다. 이 매서드는 인자로 정수값을 받는데, 프로그램 진행 시, 인자 시간만큼 프로그램을 지연시키는 역할을 한다. 이 sleep() 매서드를 사용하면 로켓 발사 카운트다운 프로그램을 작성할 수 있다. 필자는 현재도 우주에 발사되는 러시아의 소유즈(Союз) 우주선의 카운트다운을 프로그램으로 구현해보려 한다(조금 진지한 이야기를 하자면, 러시아는 로켓 발사할 때 카운트다운 따위 안한다. 그냥 쏜다...)

 

 

Thread.sleep()이 적용되지 않은 프로그램은 아래와 같이 카운트다운이 한번에 진행된다. 카운트다운 숫자가 출력되는 시간도 java.time.LocalDataTime 클래스로 함께 표시해보았다.

 

0.1초도 안되는 시간에 10부터 1까지 읊는다. MC 스나이퍼인가...

 

 

이제 Thread.sleep()을 코드에 적용해보려한다. 하지만 코드를 적용하면 예상치않게 에러가 발생함을 알 수 있다.

 

 

콘솔에서 발생한 에러의 내용을 보면 InterruptedException 에외가 처리되지 않았다는 문구가 보인다. 단순히 프로그램을 지연시키는데 왜 저 에러가 나타났을까? Thread.sleep() 매서드의 내용을 Java Documentation에서 확인해보자.

 

 

 

sleep() 매서드 내용을 보면, 매서드 선언부 뒤에 throws InterruptedException이라는 것이 보인다. 콘솔에서 만난 에러도 이와 동일한 에러였다. 앞서 보았던 throw 문이 특정 예외를 발생시키는 역할을 하는 것을 기억한다면, 매서드에 사용된 throws 문은 뒤에 명시된 예외를 매서드 내에서 반드시 처리하도록 유도한다는 것을 알 수 있다.

 

다시 코드를 작성해보자.

 

 

 

Thread.sleep()의 throws에 정의된 예외를 try-catch 문으로 처리하도록 코드를 수정하자, 필자가 의도한대로 1초마다 카운트다운이 되면서 로켓이 발사된다.

 

이제 throws 문으로 다시 되돌아와보자. 클래스 내에서 매서드를 작성하다보면, 여러 이유로 인해 반드시 예외처리를 해야 하는 순간이 오기 마련이다. 필자가 숫자를 입력받고 그 값을 되돌려주는 매서드를 하나 작성했다고 가정해보자.

 

 

 

코드에 대한 설명은 이제 불필요할 것 같으니 바로 본론으로 들어가보자. 필자가 작성한 매서드는 반드시 정수를 반환해야한다. 하지만 입력값이 정수가 아닌 경우 이 매서드에서 발생한 InputMismatchException 클래스의 인스턴스가 main 함수로 던져지게 되는데, main 함수는 이 예외에 대해 처리하는 try-catch 문이 작성되지 않았기 때문에 콘솔에 에러가 발생하게 되는 것이다. 

 

그런다고 returnInput() 매서드 내에 try-catch 문을 작성하기도 어려운 것이, 예외처리가 되더라도 매서드는 반드시 정수를 반환값으로 전달해주어야 하기 때문이다. 그런다고 예외처리 후 아무 값이나 전달하게되면 당연히 논리적인 에러가 발생할 가능성이 높아지게 된다.

 

 

 

throws는 매서드 내에서 처리하기가 어려운 예외를, main 함수나 자신을 호출한 다른 함수로 던지면서(throw) 이 예외를 반드시 처리하도록 강제하는 역할을 한다. 이제 위의 예시에서 returnInput() 매서드 사용 시 반드시 InputMismatchException에 대한 예외 처리를 강제한다고 해보자.

 

 

 

 

의도한대로 프로그램이 정상 동작함을 확인할 수 있다. 정리하자면, throws는 매서드에서 처리하기가 어려운, 그러나 반드시 발생할 수 밖에 없는 예외를 자신을 호출한 코드로 전달하여 예외를 대신 처리하도록 하는 기능을 한다고 보면 된다. 

 

throws 뒤에 명시되는 예외가 2개 이상인 경우 콤마로 구분하여 표시할 수 있다. 당연하게도 throws로 명시된 예외가 많아지면 catch()문 역시 비례해서 증가해야하만 한다.

 

 

 

[ 추가 자료 ]

예외 클래스는 크게 Exception 클래스와 그 자손 그리고 RuntimeException 클래스와 그 자손으로 구분한다. Exception 클래스 계열은 사용자의 실수에 의해 발생하는 예외가 정의되어 있고, RuntimeException 클래스 계열은 프로그래머의 실수로 인해 발생하는 예외가 정의되어 있다. 전자의 예로는 존재하지 않는 파일 이름을 입력하거나(FileNotFoundException), 입력 데이터 형태가 잘못된 경우(DataFormatException)가 있고, 후자는 배열의 범위를 벗어나거나(ArrayIndexOutOfBoundsException) 참조변수가 null인 멤버의 호출로 발생하는 예외(NullPointerException)를 예로 들 수 있다.

 

사용자에 의해 발생할 수 있는 에러의 경우 Java가 반드시 예외처리를 하도록 강제한다. 따라서 앞서 보았던 것처럼  Exception 클래스 계열은 예외 발생 코드가 반드시 try 문 내에 존재해야만 컴파일이 완료된다. 하지만 RuntimeException은 try 문 외부에서 선언이 되더라도 의외로 컴파일 에러는 발생하지 않는다. Java는 개발자 실수로 발생할 수 있는 RuntimeException 클래스 계열의 예외처리를 강제하지 않기 때문이다.

 

우측 RuntimeException은 예외처리가 되지 않아 에러가 발생한 것이다. 컴파일은 정상적으로 진행되었다.

 

이 때문에 반드시 예외처리가 강제되는 Exception 클래스 계열은 "checked 예외"로, RuntimeException 클래스 계열은 "unchecked 예외"라고 불린다.

 

 

 

3. 사용자 지정 예외 생성하기

 

Java에 정의된 예외들은, Java 사용 시 발생할 수 있는 대부분의 예외만 정의되어 있을 뿐이다. 따라서 개발자들이 프로그램을 제작하면서 발생할 수 있는 피치못할 에러와 예외도 반드시 존재할 수 밖에 없는데, 이 때문에 개발자들이 직접 자신들의 프로그램에서 발생할 수 있는 예외에 대해 정의해야하는 경우도 있다. 

 

바로 예외 클래스를 작성해보자. 우선 Java는 모든 하위 예외 클래스가 Exception을 상속받도록 지정되어 있다. 따라서 필자도 Exception을 상속받는 예외 클래스를 먼저 작성해보려 한다.

 

 

 

매서드에서도 일부러 예외를 발생시켰고, 매서드 예외는 Main 함수에서 처리할 수 있게 Throw로 던졌다. 매서드의 예외를 보면 필자가 Exception 클래스를 상속하여 만든 MyError 클래스의 인스턴스가 던져지도록 설정한 것이 보인다. 이 클래스의 생성자 인자로 작성된 messages는 인스턴스 생성 시 super() 문에 의해 new Exception(message)로 새 Checked 예외  클래스의 인스턴스를 생성하게 된다. 앞서도 언급한 내용이지만 Checked 예외 클래스 계열은 예외를 발생시키면 무조건 try-catch문으로 처리하도록 되어 있다.  

 

이번에는 RuntimeException 계열로 코드를 변경해보자.

 

 

동일한 결과지만 RuntimeException의 경우 try-catch문을 생략하더라도 컴파일은 진행된다는 것이 Exception과의 차이점이다. 

 

 

최근 Java 개발은 예외 클래스를 직접 상속해야하는 경우, 주로 RuntimeException 클래스를 상속받는 Unchecked 예외로 거의 생성한다. Unchecked 예외들은 try-catch 문이 강제되지 않기 때문에 Checked 예외로 인해 try-catch 문이 비례하여 증가함으로 인해 코드 관리가 복잡해지기 때문이다.

 

예외 생성의 마지막 예제로 나이를 입력받고 출생년도를 계산해주는 프로그램을 작성해보자.

 

 

 

 

Java에 정의된 기본 Exception 클래스들 중 일부는 getMessage() 매서드가 정의되어 있지 않다. InputMismatchException의 경우도 getMessage()로 예외 문구를 출력하면 null 값이 화면에 나타난다. 따라서 위와 같이 새로운 예외 클래스를 형성하고 상위 클래스의 getMessage() 매서드를 오버라이딩함으로써 추가 예외 안내 문구를 작성하는 것도 가능하다.

 

 

 

4. Chained Exception(연결된 예외)

 

위의 프로그램을 조금 고쳐보자. 필자는 Main 매서드 내에 위치한 모든 에러를 ProgramError 클래스가 호출되도록 할 예정이다. 2개의 catch() 구문을 ProgramError 만 처리할 수 있도록 1개로 줄인 뒤, 동일하게 프로그을 실행시켜보았다.

 

 

 

위의 두 화면은 서로 다른 예외가 발생하였으나, 동일한 결과가 콘솔 화면에 출력된 것을 나타낸 것이다. 왼쪽은 정수형이 아닌 값이 입력되어 InputMismatchException이 발생한 것이고, 우측은 필자가 직접 만든 ProgramError 클래스의 Exception이 발생한 것이다. 하지만 콘솔에 나타난 결과를 보면 단지 ProgramError와 관련된 내용만 출력된 것을 알 수 있다.

 

프로그램을 만들다 보면, 어느 코드에서 무슨 이유로 예외가 발생했는지 명확하게 알아야 할 필요성이 있다. 하지만 지금과 같이 모든 에러를 하나의 예외 클래스 인스턴스로 호출하게 되면 예외 발생의 원인을 파악하기가 매우 어려워진다. 

 

이 때문에 Java는 발생하는 예외에 다른 원인 예외가 존재할 때, 이 예외 클래스의 인스턴스를 연결할 수 있는 예외 매서드를 제공한다. 매서드의 이름은 initCause()이며, 인자는 원인이 되는 예외 인스턴스를 명시해주면 된다. initCause()는 아래의 형태로 사용한다.

 

try { 예외 발생 코드; }

catch( 원인예외클래스명 원인예외클래스인스턴스 )

{

    예외클래스명  참조변수명 = new 원인예외클래스명;

    예외클래스명.initCause(원인예외클래스인스턴스);

    throw 예외클래스명;

}

 

 

initCase() 매서드가 적용된 예외 클래스는 원인예외클래스 인스턴스와 연결되는데, 이렇게 원인예외클래스와 연결된 예외클래스가 에러로 인해 발생하는 경우, 원인예외클래스가 caused by: 라는 문구로 콘솔에 표시된다.

 

위의 코드에서 InputMismatchException을 원인 예외로 처리해보자. 

 

 

 

마찬가지로 나이가 양수가 아닌 값이 입력되는 경우도 연결된 예외를 통해 어느 부분에서 오류가 발생했는지 확인할 수 있도록 코드 수정이 가능하다. 단, InputMismatchexception과는 달리, 양수가 아닌 값이 입력되어 나타나는 에러는 논리적 에러이기 때문에 개발자가 직접 예외를 만들어 연결해주어야 한다. 

 

 


 

 

다음 포스팅에서는 Java의 기본 패키지인 java.lang 에 대해 알아보려 한다.

 

 

 

 

Fin.

반응형

댓글