요약

자바가 미리 만들어 둔 사고 종류에는 “잔액이 모자란다” 같은 내 프로그램만의 사고가 없다. 그럴 때 사고 종류를 직접 하나 만든다. 이름만 봐도 무슨 사고인지 알 수 있고, 받는 쪽에서 그 사고만 골라 처리할 수 있다. 만들 때 어느 부모 밑에 둘지를 골라야 한다. 그 선택에 따라, 쓰는 사람이 대비 코드를 꼭 써야 하는지가 갈린다.

예시로 보기

계좌에서 잔액보다 많은 돈을 찾으려 하면 막아야 한다. 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라서 잔액과 꼭 같은 돈은 찾을 수 있다.

검증: 위 표의 네 줄, 예외의 종류, 생성자 두 개의 메시지, 실행 예외로 만든 판 — 37_java-custom-exception_impl.java

정의

사용자 정의 예외는 표준 라이브러리에 없어서 직접 선언하는 예외 클래스다. 일반 예외로 만들려면 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의 순서 규칙).

연결

확인 문제

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으로 만들었다.

  1. 30000원, 10000원, 10001원의 경우와 getMessage()가 null인 경우는 검증 코드로 확인했다. ↩ ↩2

  2. 번역 오류는 JDK 21 javac로, 실행 예외로 만든 판(NegativeAmountException)은 검증 코드로 확인했다. ↩