11.1 예외와 예외 클래스
컴퓨터 하드웨어의 고장으로 인해 응용프로그램 실행 오류가 발생하는 것을 자바에서는 에러라고 한다. 아무리 견고하게 만들어도 개발자는 이런 에러에 대처할 방법이 전혀 없다.
자바에서는 에러 이외에 예외(Exception)라고 부르는 오류가 있다. 예외란 잘못된 사용 또는 코딩으로 인한 오류를 말한다. 예외가 발생되면 프로그램은 곧바로 종료된다는 점에서는 에러와 동일하지만, 예외 처리를 통해 계속 실행 상태를 유지할 수 있다. 예외에는 다음 두 가지가 있다.

자바는 예외가 발생하면 예외 클래스로부터 객체를 생성한다. 이 객체는 예외 처리 시 사용된다. 자바의 모든 에러와 예외 클래스는 Throwalbe을 상속받아 만들어지고, 추가적으로 예외 클래스는 java.lang.Exception 클래스를 상속받는다.

실행 예외는 RuntimeException과 그 자식 클래스에 해당한다. 그 밖의 예외 클래스는 모두 일반 예외이다. 자바는 자주 사용되는 예외 클래스를 표준 라이브러리로 제공한다. 앞의 그림에서 언급한 모든 예외 클래스는 표준 라이브러리에서 제공하는 것들이다.
11.2 예외 처리 코드
예외가 발생했을 때 프로그램의 갑작스러운 종료를 막고 정상 실행을 유지할 수 있도록 처리하는 코드를 예외 처리 코드라고 한다. 예외 처리 코드는 try-catch-finally 블록으로 구성된다. try-catch-finally 블록은 생성자 내부와 메소드 내부에 작성된다.

try 블록에서 작성한 코드가 예외 없이 정상 실행되면 catch 블록은 실행되지 않고 finally 블록이 실행된다. 그러나 try 블록에서 예외가 발생하면 catch 블록이 실행되고 연이어 finally 블록이 실행된다.
예외 발생 여부와 상관없이 finally 블록은 항상 실행된다. 심지어 try 블록과 catch 블록에서 return 문 (메소드 종료)을 사용하더라도 finally 블록은 항상 실행된다. finally 블록은 옵션으로 생략 가능하다.
다음 예제에서 printLength() 메소드는 문자열의 수를 리턴한다. 12라인에서 문자열 대신 null을 입력하면 5라인에서 NullPointerException이 발생한다. NullPointerException은 참조 변수가 null인 상태에서 필드나 메소드에 접근할 경우 발생한다. NullPointerException은 실행 예외이므로 컴파일할 때 예외 처리 코드가 없어도 되지만, 실행중에 발생하면 프로그램은 즉시 종료된다.
package ch11.sec02.exam01;
public class ExceptionHandlingExample1 {
public static void printLength(String data) {
try {
int result = data.length();
System.out.println("문자 수 " + result);
} catch (NullPointerException e) {
// TODO: handle exception
// System.out.println(e.getMessage()); // 예외가 발생한 이유를 리턴
// System.out.println(e.toString()); // 예외의 종류도 리턴 + 예외가 발생한 이유도 리턴
e.printStackTrace(); // 예외가 어디서 발생했는지 추적한 내용까지도 출력한다. 이거 많이 사용함.
} finally {
System.out.println("[마무리 실행]\n");
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("[프로그램 시작]\n");
printLength("ThisIsJava");
printLength(null);
System.out.println("[프로그램 종료]\n");
}
}

다음 예제에서 Class.forName("패키지 ... 클래스")은 ClassPath 위치에서 주어진 클래스를 찾는 코드이다. 찾지 못했을 경우, ClassNotFoundException이라는 일반 예외가 발생한다. 따라서 소스가 컴파일되려면 에외 처리 코드를 반드시 작성해야 한다.
package ch11.sec02.exam02;
public class ExceptionHandlingExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
Class.forName("java.lang.String"); // ClassNotFoundException 발생 가능 코드
System.out.println("java.lang.String 클래스가 존재합니다.");
} catch (ClassNotFoundException e) {
// TODO: handle exception
e.printStackTrace();
}
System.out.println();
try {
Class.forName("java.lang.String2"); // ClassNotFoundException 발생 가능 코드
System.out.println("java.lang.String 클래스가 존재합니다.");
} catch (ClassNotFoundException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}

11.3 예외 종류에 따른 처리
try 블록에는 다양한 종류의 예외가 발생할 수 있다. 이 경우, 다중 catch를 사용하면 발생하는 예외에 따라 예외 처리 코드를 다르게 작성할 수 있다. catch 블록의 예외 클래스는 try 블록에서 발생된 예외의 종류를 말하는데, 해당 타입의 예외가 발생하면 catch 블록이 선택되어 실행된다.

catch 블록이 여러 개라 할지라도 catch 블록은 단 하나만 실행된다. 그 이유는 try 블록에서 동시 다발적으로 예외가 발생하지 않으며, 하나의 예외가 발생하면 즉시 실행을 멈추고 해당 catch 블록으로 이동하기 때문이다.
다음 예제는 배열의 인덱스가 초과되었을 경우 발생하는 ArrayIndexOutOfBoundsException과 숫자타입이 아닐 때 발생하는 NumberFormatException을 각각 다르게 예외 처리한다.
package ch11.sec03.exam01;
public class ExceptionHandlingExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
String[] array = { "100", "1oo" };
for (int i = 0; i <= array.length; i++) {
try {
int value = Integer.parseInt(array[i]);
System.out.println("array[" + i + "]: " + value);
} catch (ArrayIndexOutOfBoundsException e) {
// TODO: handle exception
System.out.println("배열 인덱스가 초과됨: " + e.getMessage());
} catch (NumberFormatException e) {
// TODO: handle exception
System.out.println("숫자로 변환될 수 없음. " + e.getMessage());
}
}
}
}

처리해야 할 예외 클래스들이 상속 관계에 있을 때는 하위 클래스 catch 블록을 먼저 작성하고 상위 클래스 catch 블록을 나중에 작성해야 한다. 예외가 발생하면 catch 블록은 위에서부터 차례대로 검사 대상이 되는데, 하위 예외도 상위 클래스 타입이므로 상위 클래스 catch 블록이 먼저 검사 대상이 되면 안 된다.

package ch11.sec03.exam02;
public class ExceptionHandlingExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
String[] array = { "100", "1oo" };
for (int i = 0; i <= array.length; i++) {
try {
int value = Integer.parseInt(array[i]);
System.out.println("array[" + i + "]: " + value);
} catch (ArrayIndexOutOfBoundsException e) {
// TODO: handle exception
System.out.println("배열 인덱스가 초과됨: " + e.getMessage());
} catch (NumberFormatException e) {
// TODO: handle exception
System.out.println("숫자로 변환될 수 없음. " + e.getMessage());
} catch (Exception e) {
// TODO: handle exception
System.out.println("실행에 문제가 있습니다.");
// 상위 예외 클래스는 아래쪽에 작성
}
}
}
}

두 개 이상의 예외를 하나의 catch 블록으로 동일하게 예외 처리하고 싶을 때가 있다. 이 경우에는 catch 블록에 예외 클래스를 기호 | 로 연결하면 된다.
package ch11.sec03.exam03;
public class ExceptionHandlingExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
String[] array = { "100", "1oo", null, "200" };
for (int i = 0; i < array.length; i++) {
try {
int value = Integer.parseInt(array[i]);
System.out.println("array[" + i + "]: " + value);
} catch (ArrayIndexOutOfBoundsException e) {
// TODO: handle exception
System.out.println("배열 인덱스가 초과됨: " + e.getMessage());
} catch (NumberFormatException | NullPointerException e) {
// TODO: handle exception
System.out.println("숫자로 변환될 수 없음. " + e.getMessage());
} catch (Exception e) {
// TODO: handle exception
System.out.println("실행에 문제가 있습니다.");
}
}
}
}

11.4 리소스 자동 닫기
11.5 예외 떠넘기기
메소드 내부에서 예외가 발생할 때 try-catch 블록으로 예외를 처리하는 것이 기본이지만, 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다. 이때 사용하는 키워드가 throws이다. throws는 메소드 선언부 끝에 작성하는데, 떠넘길 예외 클래스를 쉼표로 구분해서 나열해주면 된다.

throws 키워드가 붙어 있는 메소드에서 해당 예외를 처리하지 않고 떠넘겼기 때문에 이 메소드를 호출하는 곳에서 예외를 받아 처리해야 한다. 예를 들어 다음 코드는 ClassNotFoundException을 throws하는 method2()의 예외를 method1()에서 호출할 때 처리하고 있다.

package ch11.sec05;
public class ThrowsExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
findClass();
} catch (Exception e) {
// TODO: handle exception
System.out.println("예외 처리: " + e.toString());
}
}
public static void findClass() throws ClassNotFoundException {
// 메서드를 호출한 곳으로 예외를 던졌다. 이 메서드를 호출한 곳에서 이런 예외가 발생하면 그 곳에서 처리하라고.
Class.forName("java.lang.String2");
}
}

메서드 내부(findClass 메서드)에서 예외를 잡아도 된다. 그치만 내부에서 예외를 잡지않고 예외가 발생하면 나를 호출한 쪽(main메서드)에서 예외를 처리하겠다고 만들었음.
나열해야 할 예외 클래스가 많을 경우에는 throws Exception 또는 throws Throwalbe 만으로 모든 예외를 간단히 떠넘길 수도 있다.

main() 메소드에서도 throws 키워드를 사용해서 예외를 떠넘길 수 있는데, 결국 JVM이 최종적으로 예외 처리를 하게 된다. JVM은 예외의 내용을 콘솔에 출력하는 것으로 예외 처리를 한다.

11.6 사용자 정의 예외
예외도 필요하다면 만들어서 써야한다. 특정 상황에서 내가 원하는 예외는 표준 라이브러리에 정의되있지 않기 때문이다.
은행의 뱅킹 프로그램에서 잔고보다 더 많은 출금 요청이 들어온 경우에는 잔고 부족 예외를 발생시킬 필요가 없다. 그러나 잔고 부족 예외는 표준 라이브러리에는 존재하지 않기 때문에 직접 예외 클래스를 정의해서 사용해야 한다. 이것을 사용자 정의 예외라고 한다.
사용자 정의 예외
사용자 정의 예외는 컴파일러가 체크하는 일반 예외(실행 하기 전에 예외확인)로 선언할 수도 있고, 컴파일러가 체크하지 않는 실행 예외(실행할 때 예외 확인)로 선언할 수도 있다. 통상적으로 일반 예외는 Exception의 자식 클래스로 선언하고, 실행 예외는 RuntimeException의 자식 클래스로 선언한다.


사용자 정의 예외 클래스에는 기본 생성자와 예외 메시지를 입력받는 생성자를 선언해준다. 예외 메시지는 부모 생성자 매개값으로 넘겨주는데, 그 이유는 예외 객체의 공통 메소드인 getMessage()의 리턴값으로 사용하기 위해서이다.
다음은 잔고 부족예외를 사용자 정의 예외 클래스로 선언한 것이다.
package ch11.sec06;
public class InsufficientException extends Exception {
public InsufficientException() {
}
public InsufficientException(String message) {
super(message);
}
}
예외 발생 시키기
자바에서 제공하는 표준 예외뿐만 아니라 사용자 정의 예외를 직접 코드에서 발생시키려면 throw 키워드와 함께 예외 객체를 제공하면 된다. 예외의 원인에 해당하는 메시지를 제공하고 싶다면 생성자 매개값으로 전달한다.

throw된 예외는 직접 try-catch 블록으로 예외를 처리할 수도 있지만 (아래 왼쪽), 대부분은 메소드를 호출한 곳에서 예외를 처리하도록 throws 키워드로 예외를 떠넘긴다. (아래 오른쪽)

다음 예제는 은행 계좌(Account) 클래스의 출금(withdraw) 메소드에서 잔고(balance) 필드와 출금액(매개값)을 비교해 잔고가 부족하면 InsufficientException을 발생시키고 thorws한다.
그리고 AccountExample은 withdraw() 메소드를 호출할 때 예외 처리를 한다.
package ch11.sec06;
public class Account {
private long balance;
public long getBalance() {
return balance;
}
public void deposit(int money) {
balance += money;
}
public void withdraw(int money) throws InsufficientException {
if (balance < money) {
// 예외를 throw로 발생시킴.
throw new InsufficientException("잔고 부족: " + (money - balance) + "원이 모자름.");
}
balance -= money;
}
}
package ch11.sec06;
public class AccountExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
Account account = new Account();
account.deposit(10000);
System.out.println("예금액은 " + account.getBalance() + "원 입니다.");
try {
account.withdraw(30000);
} catch (InsufficientException e) {
// TODO: handle exception
String message = e.getMessage();
System.out.println(message);
}
}
}

'자바 > 이것이 자바다' 카테고리의 다른 글
| 13. 제네릭 (1) | 2023.11.17 |
|---|---|
| 12. java.base 모듈 (1) | 2023.11.17 |
| 10. 라이브러리와 모듈 (0) | 2023.11.15 |
| 9. 중첩 선언과 익명 객체 (0) | 2023.11.15 |
| 8. 인터페이스 (1) | 2023.11.14 |