checked, unchecked와 사용자 예외
Java 예외는 컴파일러가 처리 선언을 요구하는 checked 예외와 그렇지 않은 unchecked 예외로 나뉩니다.
checked 여부만으로 심각도나 복구 가능성이 자동 결정되지는 않습니다.
호출자가 합리적으로 대응할 수 있고 실패가 API의 중요한 결과라면 checked 예외가 선택지가 될 수 있습니다.
프로그래밍 계약 위반은 보통 RuntimeException 계열로 표현합니다.
이 절에서는 기술 예외를 호출자가 이해할 문맥으로 바꾸는 방법을 다룹니다.
checked와 unchecked는 “반드시 복구 가능”과 “치명적”의 구분이 아닙니다. 컴파일러가 catch 또는 throws를 강제하느냐의 차이입니다. 공개 API를 설계할 때는 호출자가 실패를 보고 실제로 다른 행동을 할 수 있는지, 모든 계층에 기계적인 전달 코드만 늘어나는지를 함께 봅니다.
checked 예외는 catch 또는 throws를 요구한다
IOException처럼 메서드 선언에 드러나 호출자가 처리 경로를 결정합니다.
무의미하게 연쇄 throws만 늘어나는지도 살펴봅니다.
unchecked 예외도 계약에 포함된다
잘못된 인자나 불가능한 상태를 즉시 알리지만 문서와 검증 없이 남발하면 호출자가 실패 조건을 알기 어렵습니다.
DomainExceptions.java는 지금 설명한 규칙을 실제 값의 변화로 보여 줍니다.
public class DomainExceptions {
static final class InsufficientBalanceException extends Exception {
InsufficientBalanceException(int balance, int requested) {
super("balance=" + balance + ", requested=" + requested);
}
}
static int withdraw(int balance, int amount)
throws InsufficientBalanceException {
if (amount <= 0) throw new IllegalArgumentException("amount");
if (amount > balance) throw new InsufficientBalanceException(balance, amount);
return balance - amount;
}
public static void main(String[] args) {
try {
System.out.println(withdraw(100, 30));
System.out.println(withdraw(20, 30));
} catch (InsufficientBalanceException error) {
System.out.println("retry: " + error.getMessage());
}
}
}첫 인출은 새 잔액 70을 돌려줍니다. 둘째 호출은 잔액 부족이라는 예상 가능한 업무 결과를 구체 정보와 함께 전달하고, 호출자는 다른 금액으로 재시도할 여지를 얻습니다. amount <= 0은 같은 복구 대상이 아니라 호출 계약 위반으로 분리됩니다.
70
retry: balance=20, requested=30금액 형식 자체가 잘못된 경우와 잔액 부족으로 재시도할 경우를 다른 예외 계약으로 나눴습니다.
사용자 예외는 업무 문맥을 담는다
낮은 수준 SQLException을 그대로 노출하기보다 어떤 주문 저장이 실패했는지 식별자와 원인을 함께 보존할 수 있습니다.
세 비교는 예외 타입을 기술 계층의 이름이 아니라 호출자가 취할 행동으로 나누는 기준을 보여 줍니다. 사용자 예외에 숫자 값을 필드로 보관하면 메시지를 다시 파싱하지 않고도 잔액과 요청 금액을 화면이나 응답으로 변환할 수 있습니다.
// 호출 계약 위반: unchecked
if (amount <= 0) throw new IllegalArgumentException("amount");// 호출자가 다른 금액으로 복구할 수 있는 업무 실패: checked 선택
throw new InsufficientBalanceException(balance, amount);// 기술 예외를 업무 예외로 바꿀 때 원인 생성자를 제공한다.
// SaveException(String orderId, Throwable cause) { super(orderId, cause); }예외 계층은 catch 전략만큼만 세분화한다
호출자가 다르게 대응하지 않는 여러 하위 예외를 만들면 복잡도만 늘어납니다.
재시도, 입력 수정, 중단처럼 행동이 달라질 때 타입을 나눕니다.
withdraw(100, 0)과 withdraw(20, 30)을 각각 별도 try에서 실행해 보세요. 하나는 호출 코드를 고쳐야 하고, 다른 하나는 금액을 낮춰 재시도할 수 있습니다. catch 이후 행동이 같다면 예외 계층을 나눈 실익이 적고, 행동이 다르면 타입 분리가 이유를 갖습니다.
사용자 예외 이름만 도메인 용어로 바꾸고 원인·식별자·실패 값을 버리면 진단성은 오히려 낮아집니다. 기술 예외를 번역할 때는 메시지 문자열에 모든 정보를 합치기보다 구조화된 필드와 cause를 함께 보존합니다.
예외 종류는 문법 편의가 아니라 호출자가 실패를 보고 어떤 행동을 선택할 수 있는지에 맞춰 설계합니다.