자바가 미리 만들어 둔 사고 종류에는 “잔액이 모자란다” 같은 내 프로그램만의 사고가 없다. 그럴 때 사고 종류를 직접 하나 만든다. 이름만 봐도 무슨 사고인지 알 수 있고, 받는 쪽에서 그 사고만 골라 처리할 수 있다. 만들 때 어느 부모 밑에 둘지를 골라야 한다. 그 선택에 따라, 쓰는 사람이 대비 코드를 꼭 써야 하는지가 갈린다.
예시로 보기
계좌에서 잔액보다 많은 돈을 찾으려 하면 막아야 한다. withdraw가 아무것도 안 하고 조용히 끝나면 부른 쪽은 돈이 나간 줄 안다. false를 돌려주게 해도 부른 쪽이 그 값을 안 보면 그만이다. 예외를 일으키면 부른 쪽이 모르고 지나갈 수 없다. 표준 라이브러리에는 “잔액 부족” 예외가 없으니 직접 만든다.
public class InsufficientException extends Exception {
public InsufficientException() {}
public InsufficientException(String message) {
super(message);
}
}
public class Account {
private long balance;
public Account() {}
public long getBalance() {
return balance;
}
public void deposit(long money) {
balance += money;
}
public void withdraw(long money) throws InsufficientException {
if (balance < money) {
throw new InsufficientException("Insufficient balance: need more " + (money - balance) + " won.");
}
balance -= money;
}
}
withdraw는 잔액이 모자라면 throw로 예외를 일으키고, 선언에 throws InsufficientException을 붙여 부른 쪽에 넘긴다. 10000원을 넣고 돈을 찾아 본다.
Account account = new Account();
account.deposit(10000);
System.out.println("Deposit: " + account.getBalance());
try {
account.withdraw(5000);
} catch (InsufficientException e) {
String message = e.getMessage();
System.out.println(message);
} finally {
System.out.println("Deposit: " + account.getBalance());
}
찾는 돈을 바꿔 가며 돌린 결과다1.
withdraw에 준 돈 |
찍히는 줄 |
|---|---|
| 5000 (위 코드 그대로) | Deposit: 10000 → Deposit: 5000 |
| 30000 | Deposit: 10000 → Insufficient balance: need more 20000 won. → Deposit: 10000 |
| 10000 (잔액과 같다) | Deposit: 10000 → Deposit: 0 |
| 10001 (1원 모자란다) | Deposit: 10000 → Insufficient balance: need more 1 won. → Deposit: 10000 |
30000원을 찾으려 하면 20000원이 모자란다(30000 − 10000). throw에서 메서드가 끝나므로 그 아래의 balance -= money는 실행되지 않고, 잔액은 10000 그대로다. 조건이 balance < money라서 잔액과 꼭 같은 돈은 찾을 수 있다.
정의
사용자 정의 예외는 표준 라이브러리에 없어서 직접 선언하는 예외 클래스다. 일반 예외로 만들려면 Exception의 자식으로, 실행 예외로 만들려면 RuntimeException의 자식으로 선언한다.
public class XXXException extends Exception { // 또는 extends RuntimeException
public XXXException() {
}
public XXXException(String message) {
super(message);
}
}
생성자는 보통 둘을 둔다. 인자 없는 기본 생성자와, 사고 설명 글을 받는 생성자다. 글을 받는 생성자는 super(message)로 그 글을 부모에게 넘긴다. 부모(Exception)가 글을 보관하고 있다가 getMessage()를 부르면 돌려준다.
일으킬 때는 throw 뒤에 객체를 만들어 준다. 사고 원인을 알리는 글은 생성자 인자로 넣는다.
throw new InsufficientException();
throw new InsufficientException("예외메시지");
기본 생성자로 만든 예외의 getMessage()는 null을 돌려준다1.
어느 부모 밑에 둘까
| 부모 | 종류 | 일으키는 메서드 | 부르는 쪽 |
|---|---|---|---|
Exception |
일반 예외 | throws를 꼭 붙인다 |
try-catch나 throws가 없으면 번역되지 않는다 |
RuntimeException |
실행 예외 | throws 없이도 된다 |
대비 코드 없이도 번역된다. 안 받으면 실행 중에 끝난다 |
InsufficientException을 Exception의 자식으로 두었기 때문에, withdraw에서 throws를 빼면 unreported exception InsufficientException; must be caught or declared to be thrown 오류가 난다2. 부르는 사람이 “잔액 부족”에 꼭 대비하게 하고 싶으면 일반 예외로, 대비를 맡기고 싶으면 실행 예외로 만든다.
활용
- 예외 클래스 이름은
Exception으로 끝낸다. 위의 틀XXXException도, 표준 예외도 모두 그렇게 짓는다. - 메시지에는 수습에 필요한 숫자를 넣는다. 위 예는 “모자란다”만이 아니라 얼마가 모자라는지(
money - balance)를 넣었다. - 사고 검사는 상태를 바꾸기 전에 한다.
withdraw는balance -= money보다throw가 먼저라서, 예외가 난 뒤에도 잔액이 망가지지 않는다. - 사용자 정의 예외도
Exception의 자손이라catch (Exception e)에 걸린다. 따로 처리하려면 그 앞에catch (InsufficientException e)를 둔다(try-catch-finally의 순서 규칙).
연결
- 선수: 예외 던지기(
throw와throws), 예외와 예외 클래스(부모에 따라 종류가 정해진다) - 만드는 방법은 상속과 부모 생성자 호출 그대로다. 새로 배울 문법은 없다.
확인 문제
C1 나이에 음수가 들어오면 일으킬 InvalidAgeException을 실행 예외로 선언하라. 생성자는 기본 생성자와 메시지를 받는 생성자 둘이다.
답: public class InvalidAgeException extends RuntimeException { public InvalidAgeException() {} public InvalidAgeException(String message) { super(message); } }
이유: 실행 예외이므로 부모는 RuntimeException이다. Exception을 부모로 쓰면 일반 예외가 된다. super(message)를 빼먹으면 getMessage()가 null을 돌려준다.
C2 본문의 Account에 10000원을 넣은 뒤 try { account.withdraw(30000); } catch (InsufficientException e) { System.out.println(e.getMessage()); } finally { System.out.println("Deposit: " + account.getBalance()); }를 실행하면 무엇이 찍히는가?
답: Insufficient balance: need more 20000 won. 다음 줄에 Deposit: 10000.
이유: 10000 < 30000이라 throw가 실행되고, 메시지의 숫자는 30000 − 10000 = 20000이다. throw에서 withdraw가 끝나므로 balance -= money는 실행되지 않아 잔액은 10000 그대로다. Deposit: -20000이라고 답하면 throw 뒤의 줄이 실행된다고 본 것이다.
C3 InsufficientException의 부모를 Exception에서 RuntimeException으로 바꾸면 Account를 쓰는 사람에게 무엇이 달라지는가? 어느 쪽이 “잔액 부족”에 어울리는가?
답: withdraw를 try-catch 없이 불러도 번역된다. 대신 대비를 깜빡한 채 잔액이 모자라면 프로그램이 실행 중에 끝난다. 잔액 부족은 정상으로 쓰다가도 흔히 만나는 일이고 부른 쪽이 수습할 수 있으므로(다시 입력받기 등), 대비를 강제하는 일반 예외가 어울린다.
이유: 일반 예외는 번역기가 처리 코드를 검사하고, 실행 예외는 검사하지 않는다. 본문의 InsufficientException도 extends Exception으로 만들었다.