예외와 스택 전파
메서드가 정상 결과를 만들 수 없을 때 예외는 호출자 쪽으로 실패 정보를 전달합니다.
예외 객체에는 타입, 메시지, 원인, 발생 위치의 스택 추적이 들어 있습니다.
현재 메서드가 처리하지 않으면 호출 스택을 따라 위로 전파되고 끝까지 처리되지 않으면 스레드가 종료됩니다.
모든 오류를 catch해서 계속 실행하는 것이 안전한 것은 아닙니다.
이 절에서는 복구할 수 있는 경계와 문맥만 추가해 다시 전달할 경계를 구분합니다.
예외가 전파될 때 JVM은 호출을 거슬러 올라가며 맞는 catch를 찾습니다. 이 과정을 스택 풀기라고 하며, 해당 지점의 지역 실행은 중단됩니다. 따라서 예외를 잡았다는 사실만으로 이전에 변경한 상태가 되돌아가지는 않습니다. 파일 기록이나 데이터 변경에는 별도의 원자성·롤백 설계가 필요합니다.
예외 타입이 실패 범주를 표현한다
IllegalArgumentException은 호출 계약 위반, IOException은 입출력 실패처럼 호출자가 대응 방식을 고를 정보를 줍니다.
스택 트레이스는 호출 경로를 역순으로 보여 준다
첫 원인 위치와 그 메서드를 부른 경로를 읽습니다.
메시지만 복사해 버리면 조사에 필요한 위치 정보가 사라집니다.
예제는 숫자 파싱 실패를 저장 데이터 오류로 번역하면서 원래 cause를 잃지 않는 한 계층을 보여 줍니다.
public class ExceptionPropagation {
static int parsePositive(String text) {
int value = Integer.parseInt(text);
if (value <= 0) throw new IllegalArgumentException("positive required");
return value;
}
static int load(String text) {
try {
return parsePositive(text);
} catch (NumberFormatException error) {
throw new IllegalStateException("invalid stored number: " + text, error);
}
}
public static void main(String[] args) {
System.out.println(load("25"));
try {
load("none");
} catch (IllegalStateException error) {
System.out.println(error.getMessage());
System.out.println(error.getCause().getClass().getSimpleName());
}
}
}정상 입력은 그대로 25를 반환합니다. none은 낮은 수준에서는 숫자 형식 오류지만 load 경계에서는 “저장된 값이 잘못됨”이라는 의미가 추가되고, 원래 NumberFormatException은 cause로 남습니다.
25
invalid stored number: none
NumberFormatException낮은 수준 파싱 원인을 cause로 유지하면서 저장 값이라는 높은 수준 문맥을 추가했습니다.
처리하지 않으면 호출자에게 전파된다
현재 계층이 복구 방법을 모르면 무리하게 기본값을 만들지 않고 throws나 래핑으로 더 적절한 경계에 전달합니다.
호출 스택 중간에서 무조건 잡는 코드와, 복구할 경계까지 원인을 보존해 전달하는 코드를 비교합니다.
static void middle() {
bottom(); // 처리하지 않으면 호출자에게 전파
}static void bottom() {
throw new IllegalArgumentException("bad input");
}// catch (Exception ignored) { } // 원인과 실패를 조용히 잃음
// 예외를 바꿀 때 new DomainException("context", cause)로 원인 보존원인 예외를 보존해 문맥을 추가한다
도메인 예외로 바꿀 때 원래 예외를 cause로 전달합니다.
낮은 수준 메시지와 높은 수준 작업 정보를 함께 추적할 수 있습니다.
load("0")도 실행해 보세요. 이번에는 숫자 파싱에는 성공하고 parsePositive의 IllegalArgumentException이 그대로 전파됩니다. 현재 catch가 형식 오류만 번역한다는 점을 확인한 뒤, 호출자가 두 실패에 같은 대응을 해야 하는지 다른 대응을 해야 하는지 판단해 보세요.
로그를 남기고 같은 예외를 다시 던지는 코드를 여러 계층에 반복하면 한 실패가 여러 번 기록됩니다. 복구하거나 의미를 바꾸는 경계에서는 cause를 보존하고, 최종 요청·작업 경계에서 한 번 기록하는 편이 원인 추적에 유리합니다.
예외 처리는 실패를 없애는 코드가 아니라 어느 계층이 어떤 정보로 복구하거나 종료할지 정하는 흐름입니다.