본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
12장 : 예외 처리

try-catch-finally의 실행 순서

예외가 날 수 있는 범위를 try로 감싸고 복구 가능한 타입을 catch에서 처리합니다.

여러 catch는 구체적인 하위 타입에서 넓은 상위 타입 순서로 둡니다.

finally는 성공, 실패, return 여부와 관계없이 정리 작업을 실행하려는 구문입니다.

하지만 finally에서 return하거나 새 예외를 던지면 원래 결과를 가릴 수 있습니다.

이 절에서는 흐름을 숨기지 않는 최소한의 try 범위를 만듭니다.

catch는 예외 타입만 보고 선택되며 같은 try에서 두 catch가 연달아 실행되지는 않습니다. 선택된 catch가 값을 반환하려 해도 메서드를 빠져나가기 전에 finally가 실행됩니다. 이 순서를 알아야 로그와 반환값이 섞인 출력을 정확히 읽을 수 있습니다.


try에는 함께 실패 처리할 코드만 둔다

너무 넓게 감싸면 어느 문장에서 난 예외인지와 어떤 상태가 변경됐는지 알기 어렵습니다.

복구 정책이 같은 작업만 묶습니다.


catch 순서는 구체 타입이 먼저다

Exception을 먼저 잡으면 그 아래 IOException 같은 catch는 도달할 수 없어 컴파일 오류가 납니다.

실제로 복구할 타입만 잡습니다.

세 호출은 각각 정상 나눗셈, 숫자 형식 오류, 0 나눗셈 경로를 지나지만 모두 메서드를 빠져나가기 전에 attempt finished를 출력합니다.

CatchOrder.java
public class CatchOrder {
    static int divide(String left, String right) {
        try {
            return Integer.parseInt(left) / Integer.parseInt(right);
        } catch (NumberFormatException error) {
            System.out.println("not a number");
            return 0;
        } catch (ArithmeticException error) {
            System.out.println("division by zero");
            return 0;
        } finally {
            System.out.println("attempt finished");
        }
    }

    public static void main(String[] args) {
        System.out.println(divide("10", "2"));
        System.out.println(divide("x", "2"));
        System.out.println(divide("10", "0"));
    }
}

첫 호출에서는 finally가 먼저 보이고 그 뒤 호출자가 반환값 5를 출력합니다. 실패 호출에서는 해당 catch의 메시지, finally 메시지, 호출자가 받은 기본값 0 순서입니다. 반환식을 평가한 뒤에도 실제 제어가 finally를 거친다는 증거입니다.

attempt finished
5
not a number
attempt finished
0
division by zero
attempt finished
0

성공과 두 실패 모두 finally를 거치지만 catch는 실제 예외 타입 하나만 선택합니다.

try의 좁은 작업 실행

너무 넓게 감싸면 어느 문장에서 난 예외인지와 어떤 상태가 변경됐는지 알기 어렵습니다.

가장 구체적인 catch 하나 선택

Exception을 먼저 잡으면 그 아래 IOException 같은 catch는 도달할 수 없어 컴파일 오류가 납니다.

finally 정리 뒤 결과 반환 또는 전파

관련 없는 여러 예외에 완전히 같은 복구가 필요하면 catch (A | B error)로 묶습니다.


다중 catch는 같은 처리를 공유한다

관련 없는 여러 예외에 완전히 같은 복구가 필요하면 catch (A | B error)로 묶습니다.

두 타입에 공통으로 안전한 정보만 사용합니다.

다중 catch의 공유 처리와 상하위 catch 순서, finally의 위험한 return을 각각 분리해 비교합니다.

비교 1
try {
    risky();
} catch (java.io.IOException | java.sql.SQLException error) {
    System.out.println(error.getMessage());
}
비교 2
// catch (Exception error) { }
// catch (IOException error) { } // 컴파일 오류: 이미 상위 타입에서 잡힘
비교 3
// finally { return 0; } // 원래 return이나 예외를 덮으므로 피한다

finally는 흐름 제어가 아니라 정리에 쓴다

락 해제나 임시 상태 복구처럼 반드시 수행할 작업을 둡니다.

AutoCloseable 자원은 try-with-resources가 더 안전합니다.


finally의 return은 원래 결과를 덮어쓴다

finally에서 반환하거나 새 예외를 던지면 try의 결과와 예외가 사라질 수 있습니다.

정리 작업만 두고 흐름 제어는 피합니다.

divide("10", "0")의 catch에서 예외를 다시 던지도록 바꿔도 finally가 출력되는지 확인하세요. 그런 다음 finally에 return을 넣어 보면 재던진 예외조차 사라집니다. 이 실습은 finally의 흐름 제어가 왜 원래 실패를 훼손하는지 보여 줍니다.

finally가 일반적으로 실행된다고 해서 프로세스 강제 종료나 JVM 중단까지 보장하는 것은 아닙니다. 반드시 외부에 반영돼야 하는 데이터는 finally 한 줄에만 의존하지 말고 저장소의 트랜잭션과 재시도 계약으로 보호합니다.


try-catch-finally는 예외를 많이 잡는 구조보다 복구할 범위와 원래 실패를 보존하는 구조가 중요합니다.