- 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
- 자바가 제공하는 예외 계층 구조
- Exception과 Error의 차이는?
- RuntimeException과 RE가 아닌 것의 차이는?
- 커스텀한 예외 만드는 방법
- 예외활용하는 방법
예외처리 방법
예외처리
자바에서는 어떤 메서드를 호출할 때 개발자가 예측하지 못한 예외(Exception)가 발생할 수 있는데, 이 때 이 상황을 제어할 수 있도록 돕는 기능이 예외처리(Exception Handling)이라고 한다.
프로그램을 중단시키기 때문에 에러(Error)와 헷갈릴수 있는데, 예외와 에러의 차이는 아래에서 다시정리해보겠다.
아래의 코드를 보자.
1 | public class StudyHalle { |
10
을 0
으로 나누려고 하여 일부러 예외를 발생시켜보았다. 이 프로그램을 실행하면, 다음과 같이 ArithmeticException
이 발생한다.
divide()
메서드를 호출하는 코드에서 에러가 발생했다. 그래서 그 다음 콘솔에 출력하는 코드가 출력되지 않았다. 그런데 컴파일할 때는 발생하지 않았는데, 프로그램을 실행할때 발생했다. 예외(Exception)이 발생했다는 건 에러가 발생한것과 다른 경우인데, 이에 대해선 뒤에서 다루었다. (바로가기)Exception과 Error의 차이는?
컴파일 에러면 개발하는 과정에서 개발자가 디버깅하기 쉽겠지만, 이런 경우는 개발하는 과정에서 개발자가 미리 예측하여 예외처리를 해주어야 한다.
ArithmeticException
이 발생할 경우 예외처리를 하도록 try/catch를 적용해보았다.
1 | public class StudyHalle { |
다시 컴파일하고 프로그램을 실행한 결과는 아래와 같다.
ArithmeticException
이 발생할 경우, 콘솔에 메세지를 출력하도록 한 결과이다. 프로그램에서 에러처럼 발생된게 아니라 예외처리를 한 덕분에 프로그램이 중단되지 않고, 예외처리 코드 이후의 코드까지 정상적으로 출력되었다.
이처럼 예외상황이 발생했을때, 프로그램이 중단되지 않고 개발자가 의도한 대로 프로그램이 실행되도록 핸들링하는 것을 예외처리라 한다. 예외처리를 할 때 사용하는 try-catch를 더 자세히 알아보겠다.
try
try-catch문에서 try는 예외처리가 발생할 것으로 예상되는 코드를 포함시키는 영역이다.
try-catch의 흐름은 아래의 그림과 같다.
try문에서 Exception이 발생하지 않으면 try-catch문을 벗어나며, Exception이 발생하면 catch에서 핸들링하는 예외인지를 체크하여 예외처리가 있으면 그 로직으로 프로그램이 정상동작하며, 예외처리 코드가 없으면 예외로 인해 프로그램이 중단된다.
위의 예제코드에서 보면 divide()
를 호출하는 코드에서 분모가 0
으로 들어올경우 ArithmeticException
이 예상되므로 divide()
를 호출하는 코드를 try 문안에 넣었다.
try 문안에 try/catch문을 중첩해서 사용할수도 있다.
1 | public class StudyHalle { |
이렇게 해서 프로그램을 실행하면 다음과 같은 결과를 출력한다.
중첩 try-catch문에 의해서 예외처리가 되었음을 알 수 있다.
catch
try문안에서 발생할 수 있는 예외(Exception)를 예외처리하는 영역이다. 위의 코드를 다시 살펴보자.
1 | public class StudyHalle { |
아래 예외계층구조를 통해 더 알아보겠지만, 예외라고 하는 Exception은 클래스이다. ArithmeticException
같은 예외는 모두 Exception
의 하위 클래스들이다.
catch로 예외처리된 Exception은 하위 클래스까지 모두 예외처리가 되는데, 이 때문에 RuntimeException
이 예외처리 되었다면, RuntimeException
의 하위클래스인 ArithmeticException
도 예외처리가 된걸로 간주한다. 따라서 아래의 catch문이 실행되지 않은것이다.
멀티캐치(multi-catch)
예외처리는 복수로 할 수도 있으며, 비트논리합 연산자(참고)를 이용할수도 있다.
1 | public class StudyHalle { |
try문에서 throw
키워드로 NullPointerException
을 발생시켜서 예외처리가 어떻게 되는지 확인하였다. throw 키워드는 아래에서 설명한다.
비트논리 연산자를 이용한 catch를 멀티캐치라고 하는데, 이는 JDK 1.7부터 추가된 기능이다.
1 | catch(NullPointerException | IllegalArgumentException e) { |
출처 : Oracle Java Documentation - The catch Blocks
throw
try-catch가 발생할 수 있는 Exception을 예외처리하는 코드라면, throw
는 고의로 Exception을 발생하는 키워드이다.
Exception을 던지는 문법은 아래와 같다.
1 | RuntimeException e = new RuntimeException(); |
위에서 Exception도 객체라고 설명했다. 예외를 발생시키기 위해서는 Exception을 인스턴스로 선언해서 throw
키워드와 함께 사용하면 Exception을 발생시킬 수 있다.
일반적으로는 저렇게 2줄로 쓰기보다 아래처럼 한 줄로 사용한다.
1 | throw new RuntimeException(); |
예외를 던지는 예제 코드이다.
1 | public class StudyHalle { |
try문에서 NullPointerException
을 던져서 캐치가 작동하도록 유도했다.
throws
throw
가 예외를 고의로 발생하는 키워드였다면, throws
는 메서드 내부에서 발생한 예외를 메서드 외부로 던지는 키워드이다.
사실 throws
키워드를 사용하지 않더라도 어차피 메서드를 호출하는 영역에서 Exception이 발생한다. 따라서 throws
를 사용한다 하더라도 달라질것은 없다. 그렇다면 왜 사용하는걸까?
앞서 예외처리라는건 코드에서 발생할 수 있는 Exception을 개발자가 예외처리하는 것이라고 하였는데, 코드만 보고 발생할 수 있는 Exception을 바로 예상할 수 있는건 쉬운일이 아니다. 그런데 메서드에서 throws
키워드를 사용하면, 발생할 수 있는 Exception을 명시함으로써 메서드 호출부에서 실수로 Exception을 처리하지 못하는 실수를 방지할 수 있다.
예제 코드이다.
1 | public class StudyHalle { |
주의할 것
try-catch에서 catch가 예외처리를 하여 프로그램이 중단되지 않도록 한다면, throws
키워드를 사용한다는 것은 예외처리를 하는 것이 아니라 예외를 던짐으로써 책임을 넘기는 일이 된다.
따라서 throws
키워드가 있는 메서드를 호출하는 영역에서는 try-catch를 통해서 예외를 처리해주어야 한다.
finally
예외 발생 여부와 상관없이 무조건 실행되는 코드이다. 예제 코드를 확인해보자.
1 | public class StudyHalle { |
throwException()
는 리턴타입은 없고, RuntimeException
을 던지는 메서드이다. 메인 메서드에서 throwException()
를 호출함으로써 예외가 발생했지만, catch에서 처리하지 못하면서 RuntimeException
에 의해 프로그램이 중단되었다.
프로그램이 비정상적으로 중단되는 바람에 try-catch 바깥의 코드는 실행되지 못했지만, finally 문안의 코드는 예외 발생 여부와 상관없이 정상적으로 출력되었다.
예외와 상관없이 무조건 실행되어야할 코드는 finally안에 작성할 필요가 있다.
예외 계층 구조
Exception은 Error와 함께 Throwable을 상속받는 하위 클래스이다.
Error와 Exception의 차이는 아래에서 정리했다. 바로가기
그리고 위의 이미지처럼 많은 예외 클래스가 Exception 하위클래스로 상속관계가 구성된다.
그리고 Exception은 컴파일 단계에서 체크하는지 여부에 따라 다시 Checked Exception과 Unchecked Exception으로 구분한다.
Checked Exception과 Unchecked Exception은 다음과 같다.
Checked Exception
컴파일시 발생하는 예외가 Checked Exception이다. Checked Exception은 예외처리하지 않으면 컴파일 과정에서 예외가 발생하기 때문에 IDE에서 예외처리를 강제한다.
자주 쓰이는 Checked Exception은 다음과 같다.
IOException
java.io 패키지의 File System 또는 Data Stream을 사용하거나 java.net 패키지의 네트워크 애플리케이션을 사용할때 발생한다.
1 | try { |
ParseException
.parse()
을 사용하여 주어진 문자열을 기반으로 객체를 만드는데 파싱에서 에러가 발생할 경우 ParseException이 발생한다.
1 | try { |
InterruptedException
자바 스레드에서 join()
, sleep()
또는 wait()
을 호출할 때마다 대기(WAITING)상태 또는 TIMED_WAITING 상태로 전환된다.
게다가 자바 스레드는 interrupt()
를 이용하여 다른 스레드를 뺏어서 호출할 수 있는데, 이 때 이미 대기(WAITING, TIME_WAITING)상태인 스레드를 중단시킬경우 InterruptedException
이 발생한다.
1 | class ChildThread extends Thread { |
출처 : Baeldung - Common Java Exceptions
Unchecked Exception
런타임시 발생하는 예외가 Unchecked Exception이다. 컴파일 과정에서 체크되지 않기 때문에 IDE에서 예외처리를 강제하지는 않지만, 런타임시 발생할 수 있으므로 이 역시도 예외처리를 해주어야한다.
아래의 Unchecked Exception들은 RuntimeException
의 하위 클래스들이다.
NullPointerException
애플리케이션에서 null을 사용하려고 할 때, 메서드에서 던지는 Exception.
1 | String strObj = null; |
ArrayIndexOutOfBoundsException
배열의 인덱스를 잘못 참조할때 발생하는 Exception.
1 | int[] nums = {1,2,3,4,5}; |
StringIndexOutOfBoundsException
문자열의 인덱스를 잘못 참조할때 발생하는 Exception.
1 | String str = "Hello World."; |
NumberFormatException
문자열로부터 Integer 객체를 파싱할때, 문자열의 형식이 숫자형식이 아닐 때 발생하는 Exception.
1 | String rightNumberFormat = "10000"; |
ArithmeticException
0을 나누려고 할 때 발생하는 Exception.
1 | int result = 10/0; // throws ArithmeticException |
ClassCastException
인스턴스가 아닌 하위 클래스로 타입 캐스팅 하려고 할 때, 발생하는 Exception.
1 | public class Animal { |
1 | public class Dog { |
1 | public class Cat { |
1 | public class Human { |
dog
인스턴스를 선언하는 코드를 보면, Animal 타입의 인스턴스를 그 하위 클래스 타입으로 형변환을 하는 과정에서 ClassCastException
이 발생했다. 하위클래스로의 형변환은 불가능하기 때문이다.
출처 : Baeldung - Common Java Exceptions
Exception의 계층구조를 알아두어야 하는 이유가 있다.
catch문의 순서는 계층구조의 순서를 따라서 작성해야 한다. 계층구조에서 하위 계층을 먼저 catch하고, 그 다음 catch 순서부터 상위계층을 catch하는 순으로 작성해야한다.
1 | public class Example { |
출처 : 백기선님 유튜브 - [자바 라이브 스터디] 9주차 예외
위의 코드는 두번째 catch문에서 컴파일 에러가 발생한다. 왜 그럴까?
자바 API 문서를 통해 보면, IllegalArgumentException
은 RuntimeException
의 하위 클래스임을 알 수 있다.
다시 위의 코드로 돌아가면, catch 순서를 주목해야한다. RuntimeException
을 먼저 catch했기 때문에, 그럼 RuntimeException의 하위클래스까지 전부 catch가 된다. 그런 상황에서 이미 catch된 클래스의 하위 클래스를 예외처리하려고 하니, 컴파일러 입장에서는 무엇을 예외처리해야할지 알 수 없기 때문에 컴파일 에러가 발생하는 것이다.
Exception과 Error의 차이
Throwable 클래스를 상속받으며, 발생할 경우 프로그램이 중단된다는 공통점이 있음에도 Error와 Exception은 분명히 다른 클래스이다.
프로그램을 중단시키는 이벤트가 사전에 예측할 수 있었느냐, 없었느냐에 따라 Exception과 Error를 구분할 수 있다.
에러는 개발자가 예측할 수 없었던 이벤트로 인해 개발자가 복구할 수 없는 상황이다.
반면 예외는 개발자가 예측할 수 있으며, 예측을 하였다면 예외처리를 통해서 예외를 제어할수도 있다.
출처
RuntimeException과 RE가 아닌 것
RuntimeException
이란 말그대로 컴파일 시점이 아닌 런타임 시점에서 발생하는 예외를 뜻한다.
RuntimeException
은 컴파일 시점에 체크하지 않는 예외이기 때문에 Unchecked Exception에 해당하며, Unchecked Exception은 개발자가 유효성 검증을 통해서 회피할 수 있다.
Exception
은 크게 Checked Exception과 Unchecked Exception으로 구분할 수 있다. 어떤 블로그 글에서 Checked Exception과 Unchecked Exception을 클래스처럼 표현했는데, Exception을 상속받는 하위 클래스들의 분류라고 보는것이 올바른것 같다.
정리하면, RuntimeException
은 Unchecked Exception이며, RE가 아닌 예외는 Checked Exception이 된다.
Exception이 명확하지 않거나 Exception으로 인한 문제를 해결할 수 없다면 Unchecked Exception을, Exception이 발생하면 치명적이거나 Exception을 활용하여 의미있는 작업을 할 수 있다면 Checked Exception을 사용한다고 한다.
출처 : max9106 - [Java] Exception
커스텀한 예외 만드는 법
커스텀한 예외를 만드는 방법은 간단한다. Exception은 기본적으로 클래스기 때문에 Exception을 상속받는 클래스를 생성하면 된다.
계좌 잔액보다 큰 금액이 이체되려고 할 때 발생하는 BalanceInsuffientException
을 생성했다.
1 | public class BalanceInsuffientException extends Exception { |
1 | public class Account { |
예외활용하는 방법
단순히 try-catch로 예외를 처리하는 것을 넘어 이 예외의 메세지를 콘솔에 출력하는 방식이다. 위의 화면에서 커스텀 예외가 발생하도록 유도했는데, 사실 저렇게만 예외가 발생하면 안된다.
try-catch로 예외를 처리해주고, 이때 발생하는 예외 객체를 활용하는 3가지 메서드를 소개한다.
getMessage()
예외 메세지를 가져오는 메서드이다. 위의 예제코드를 활용했다.
1 | try { |
1 | 잔고 부족!! |
toString()
1 | try { |
1 | exceptionhandling.BalanceInsuffientException: 잔고 부족!! |
printStackTrace()
1 | try { |
1 | exceptionhandling.BalanceInsuffientException: 잔고 부족!! |
출처 : 생활코딩 - 예외1 문법