본문 바로가기

try·catch·finally 흐름

예상하지 못한 예외가 disconnect를 건너뛰는 자원 누수를 재현하고 try/catch/finally의 실행 순서와 책임을 단계별로 고칩니다.

예외의 가장 큰 장점은 메서드마다 오류 코드를 확인하지 않아도 정상 경로를 연속해서 쓸 수 있다는 점입니다.

그러나 catch만 추가했다고 자원 정리가 자동 보장되지는 않습니다.

정상 종료나 예외 전파 때 필요한 정리는 정상 흐름이나 특정 catch 뒤가 아니라 finally에 놓아야 합니다. 다만 JVM 종료처럼 자바 실행 자체가 끝나는 상황까지 finally 실행을 보장하는 것은 아닙니다.


알 수 없는 예외와 정리 누락

서비스는 네트워크 전용 예외만 잡고 catch 다음에 disconnect를 호출합니다.

전송 중 IllegalStateException이 발생하면 해당 catch와 마지막 문장을 모두 건너뛰고 호출자에게 전파됩니다.

lab/DisconnectSkippedByUnexpectedFailure.java
public final class DisconnectSkippedByUnexpectedFailure {
    public static void main(String[] args) {
        NetworkClient client = new NetworkClient();
        try {
            client.connect();
            client.send();
        } catch (NetworkException error) {
            System.out.println("network=" + error.getMessage());
        }
        client.disconnect();
    }

    private static final class NetworkClient {
        void connect() throws NetworkException {
            System.out.println("connected");
        }

        void send() {
            throw new IllegalStateException("serializer crashed");
        }

        void disconnect() { System.out.println("disconnected"); }
    }

    private static final class NetworkException extends Exception {
        NetworkException(String message) { super(message); }
    }
}
관찰되는 표준 출력과 실패
connected
Exception in thread "main" java.lang.IllegalStateException: serializer crashed

disconnected가 없습니다.

catch가 없어서가 아니라 정리 호출의 위치가 특정 예외 목록에 의존하기 때문입니다.

앞으로 새 예외가 추가될 때마다 모든 catch 뒤 경로를 검토해야 한다면 구조적으로 안전하지 않습니다.


try·catch·finally의 역할

finally는 try 진입 뒤 정상 종료, 일치하는 catch 처리, 처리되지 않은 예외 전파 모두에서 실행됩니다.

연결이 실제로 성립한 뒤에만 닫아야 한다면 client가 내부 상태로 중복·미연결 close를 안전하게 처리하거나, 연결 여부를 서비스가 추적해야 합니다.

src/FinallyProtectedNetworkFlow.java
public final class FinallyProtectedNetworkFlow {
    public static void main(String[] args) {
        NetworkClient client = new NetworkClient();
        try {
            client.connect();
            client.send("error2");
        } catch (NetworkException error) {
            System.out.println("handled=" + error.getMessage());
        } finally {
            client.disconnect();
        }
        System.out.println("normal-end");
    }

    private static final class NetworkClient {
        private boolean connected;

        void connect() throws NetworkException {
            connected = true;
            System.out.println("connected");
        }

        void send(String data) throws NetworkException {
            if (data.contains("error2")) throw new NetworkException("send failed");
            System.out.println("sent=" + data);
        }

        void disconnect() {
            if (connected) {
                connected = false;
                System.out.println("disconnected");
            }
        }
    }

    private static final class NetworkException extends Exception {
        NetworkException(String message) { super(message); }
    }
}
connected
handled=send failed
disconnected
normal-end

이 예제는 catch와 finally가 모두 정상 완료하므로 뒤의 정상 문장까지 진행합니다.

만약 catch에서 처리하지 않은 RuntimeException이 생기면 finally가 정상 완료한 뒤 normal-end는 실행되지 않고 원래 예외가 상위로 전파됩니다.

정리를 보장하는 것과 오류를 복구하는 것은 서로 다른 책임입니다.


try·finally 구성

현재 계층에 복구 방법이 없고 자원만 정리해야 한다면 catch로 잡았다가 다시 던질 필요가 없습니다.

finally가 정상 완료하면 try/finally는 원래 예외를 그대로 전달하면서 정리만 수행합니다.

src/FinallyWithoutCatch.java
public final class FinallyWithoutCatch {
    public static void main(String[] args) {
        try {
            execute();
        } catch (BoardSendException error) {
            System.out.println("boundary=" + error.getMessage());
        }
    }

    private static void execute() throws BoardSendException {
        Session session = new Session();
        try {
            session.open();
            session.send();
        } finally {
            session.close();
        }
    }

    private static final class Session {
        void open() { System.out.println("open"); }
        void send() throws BoardSendException { throw new BoardSendException("timeout"); }
        void close() { System.out.println("close"); }
    }

    private static final class BoardSendException extends Exception {
        BoardSendException(String message) { super(message); }
    }
}
open
close
boundary=timeout

서비스는 로그를 남기거나 메시지를 바꾸지 않았습니다.

경계가 한 번만 처리하므로 중복 로그가 없습니다.

단, close 자체가 실패하면 원래 예외가 가려질 수 있습니다.

try-with-resources는 여러 close와 본문 실패를 다룰 때 억제 예외로 원인을 보존합니다.

정리 위치와 예외 처리 경계를 나눠 읽기

catch 불일치로 정리 생략, catch+finally 복구, finally 후 경계 전파라는 세 독립 실행 순서입니다. 각 프로그램의 도달·미도달·정상 완료를 단계별로 대조하며 정리와 복구의 차이를 확인할 수 있습니다.

정리 위치와 예외 처리 경계를 나눠 읽기

정리 호출을 catch 뒤에 둔 첫 예제는 disconnect를 건너뛴다. finally에서 정리하는 두 예제는 각각 지역 catch 후 normal-end로 이어지거나 상위 경계에 timeout을 전달한다.

정리 책임 · 세 구현 비교

정리 위치와 예외 처리 경계를 나눠 읽기

서로 다른 실패를 사용하는 세 프로그램입니다. 정리의 위치와 예외를 처리하는 경계를 나눠 비교합니다.

정리와 복구 경계를 구분하는 세 프로그램 영역마다 다른 프로그램과 실패입니다. 첫 예제의 IllegalStateException은 NetworkException catch 및 뒤 disconnect를 건너뜁니다. 둘째는 지역 catch 후 finally의 disconnect와 normal-end로 이어집니다. 셋째는 timeout을 처리하지 않고 finally의 close가 정상 완료한 뒤 main의 경계가 처리합니다. finally의 새 실패나 return, JVM 종료까지 포함한 절대 보장으로 일반화하지 않습니다. 원문 A · catch 뒤 정리 connect 뒤 실패 connected 출력 IllegalStateException catch가 안 맞음 NetworkException만 잡음 실제 예외는 통과 뒤 정리 호출 건너뜀 disconnect 호출 안 함 미처리 예외로 스레드 끝 원문 B · catch + finally connect 뒤 send 실패 connected 출력 NetworkException catch의 지역 복구 handled=send failed finally 뒤 정상 끝 disconnected 출력 normal-end 출력 원문 C · try + finally open 뒤 timeout open 출력 예외는 아직 처리 안 함 finally의 정리 close 출력 정리가 정상 완료 main의 경계 catch boundary=timeout 서로 다른 프로그램·실패 · 실선은 실제 전달·정리 순서 · finally가 정상 완료한 경로
정리와 복구 경계를 구분하는 세 프로그램 영역마다 다른 프로그램과 실패입니다. 첫 예제의 IllegalStateException은 NetworkException catch 및 뒤 disconnect를 건너뜁니다. 둘째는 지역 catch 후 finally의 disconnect와 normal-end로 이어집니다. 셋째는 timeout을 처리하지 않고 finally의 close가 정상 완료한 뒤 main의 경계가 처리합니다. finally의 새 실패나 return, JVM 종료까지 포함한 절대 보장으로 일반화하지 않습니다. 원문 A · catch 뒤 정리 connect 뒤 실패 connected 출력 IllegalStateException catch가 안 맞음 NetworkException만 잡음 실제 예외는 통과 뒤 정리 호출 건너뜀 disconnect 호출 안 함 미처리 예외로 스레드 끝 원문 B · catch + finally connect 뒤 send 실패 connected 출력 NetworkException catch의 지역 복구 handled=send failed finally 뒤 정상 끝 disconnected 출력 normal-end 출력 원문 C · try + finally open 뒤 timeout open 출력 예외는 아직 처리 안 함 finally의 정리 close 출력 정리가 정상 완료 main의 경계 catch boundary=timeout 서로 다른 프로그램·실패 실선은 실제 전달·정리 순서 finally가 정상 완료한 경로
정리 위치와 예외 처리 경계를 나눠 읽기
원문 구성실패·처리와 정리관찰 순서
catch 뒤 정리send()의 IllegalStateException은 NetworkException catch와 뒤의 disconnect()를 건너뜁니다.
connected
이후 미처리 예외로 종료
disconnected 출력 없음
catch + finallysend("error2")의 예외를 catch가 처리하고, finally의 disconnect()도 정상 완료합니다.
connected
handled=send failed
disconnected
normal-end
try + finallyexecute()는 timeout을 잡지 않고 close()만 수행합니다. main의 catch가 전파된 예외를 처리합니다.
open
close
boundary=timeout
catch 뒤 정리
실패·처리와 정리
send()의 IllegalStateException은 NetworkException catch와 뒤의 disconnect()를 건너뜁니다.
관찰 순서
connected
이후 미처리 예외로 종료
disconnected 출력 없음
catch + finally
실패·처리와 정리
send("error2")의 예외를 catch가 처리하고, finally의 disconnect()도 정상 완료합니다.
관찰 순서
connected
handled=send failed
disconnected
normal-end
try + finally
실패·처리와 정리
execute()는 timeout을 잡지 않고 close()만 수행합니다. main의 catch가 전파된 예외를 처리합니다.
관찰 순서
open
close
boundary=timeout

세 번째 예제의 close()는 정상 완료합니다. finally가 return이나 새 예외로 끝나면 기존 결과를 대체할 수 있으며, JVM 종료까지 정리를 보장하는 뜻은 아닙니다. 첫 예제의 실패 진단은 stderr이고 connected만 stdout입니다.

이 예의 조건

  • 서로 다른 세 프로그램이며 finally가 정상 완료하는 경로다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보정리 위치와 예외 처리 경계를 나눠 읽기 정리 위치와 예외 처리 경계를 나눠 읽기 정리 호출을 catch 뒤에 둔 첫 예제는 disconnect를 건너뛴다. finally에서 정리하는 두 예제는 각각 지역 catch 후 normal-end로 이어지거나 상위 경계에 timeout을 전달한다. 정리 책임 · 세 구현 비교 정리 위치와 예외 처리 경계를 나눠 읽기 서로 다른 실패를 사용하는 세 프로그램입니다. 정리의 위치와 예외를 처리하는 경계를 나눠 비교합니다. 정리 위치와 예외 처리 경계를 나눠 읽기 원문 구성 실패·처리와 정리 관찰 순서 catch 뒤 정리 send() 의 IllegalStateException 은 NetworkException catch와 뒤의 disconnect() 를 건너뜁니다. connected 이후 미처리 예외로 종료 disconnected 출력 없음 catch + finally send("error2") 의 예외를 catch가 처리하고, finally의 disconnect() 도 정상 완료합니다. connected handled=send failed disconnected normal-end try + finally execute() 는 timeout을 잡지 않고 close() 만 수행합니다. main의 catch가 전파된 예외를 처리합니다. open close boundary=timeout catch 뒤 정리 실패·처리와 정리 send() 의 IllegalStateException 은 NetworkException catch와 뒤의 disconnect() 를 건너뜁니다. 관찰 순서 connected 이후 미처리 예외로 종료 disconnected 출력 없음 catch + finally 실패·처리와 정리 send("error2") 의 예외를 catch가 처리하고, finally의 disconnect() 도 정상 완료합니다. 관찰 순서 connected handled=send failed disconnected normal-end try + finally 실패·처리와 정리 execute() 는 timeout을 잡지 않고 close() 만 수행합니다. main의 catch가 전파된 예외를 처리합니다. 관찰 순서 open close boundary=timeout 세 번째 예제의 close() 는 정상 완료합니다. finally가 return이나 새 예외로 끝나면 기존 결과를 대체할 수 있으며, JVM 종료까지 정리를 보장하는 뜻은 아닙니다. 첫 예제의 실패 진단은 stderr이고 connected 만 stdout입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
원 실패

각 프로그램의 작업에서 실패가 발생합니다.

  • A: IllegalStateException
  • B: NetworkException
  • C: timeout
  • 실제 예외 타입과 처리 경계를 비교한다.
지역 처리/정리

A는 catch 불일치, B는 catch 뒤 finally, C는 finally만 진행합니다.

  • A: disconnect 미호출
  • B: handled=send failed → disconnected
  • C: close
  • 정리가 예외 처리와 동일 동작은 아니다.
호출 종료

각 실패가 서로 다른 종료로 이어집니다.

  • A: 미처리 스레드 종료
  • B: normal-end
  • C: boundary=timeout
  • finally 정상 완료는 전달 중인 예외를 자동 복구하지 않는다.

정리 위치와 예외 처리 경계를 나눠 읽기

catch 불일치로 정리 생략, catch+finally 복구, finally 후 경계 전파라는 세 독립 실행 순서입니다. 각 프로그램의 도달·미도달·정상 완료를 단계별로 대조하며 정리와 복구의 차이를 확인할 수 있습니다.

정리 위치와 예외 처리 경계를 나눠 읽기

정리 호출을 catch 뒤에 둔 첫 예제는 disconnect를 건너뛴다. finally에서 정리하는 두 예제는 각각 지역 catch 후 normal-end로 이어지거나 상위 경계에 timeout을 전달한다.

정리 책임 · 세 구현 비교

정리 위치와 예외 처리 경계를 나눠 읽기

서로 다른 실패를 사용하는 세 프로그램입니다. 정리의 위치와 예외를 처리하는 경계를 나눠 비교합니다.

정리와 복구 경계를 구분하는 세 프로그램 영역마다 다른 프로그램과 실패입니다. 첫 예제의 IllegalStateException은 NetworkException catch 및 뒤 disconnect를 건너뜁니다. 둘째는 지역 catch 후 finally의 disconnect와 normal-end로 이어집니다. 셋째는 timeout을 처리하지 않고 finally의 close가 정상 완료한 뒤 main의 경계가 처리합니다. finally의 새 실패나 return, JVM 종료까지 포함한 절대 보장으로 일반화하지 않습니다. 원문 A · catch 뒤 정리 connect 뒤 실패 connected 출력 IllegalStateException catch가 안 맞음 NetworkException만 잡음 실제 예외는 통과 뒤 정리 호출 건너뜀 disconnect 호출 안 함 미처리 예외로 스레드 끝 원문 B · catch + finally connect 뒤 send 실패 connected 출력 NetworkException catch의 지역 복구 handled=send failed finally 뒤 정상 끝 disconnected 출력 normal-end 출력 원문 C · try + finally open 뒤 timeout open 출력 예외는 아직 처리 안 함 finally의 정리 close 출력 정리가 정상 완료 main의 경계 catch boundary=timeout 서로 다른 프로그램·실패 · 실선은 실제 전달·정리 순서 · finally가 정상 완료한 경로
정리와 복구 경계를 구분하는 세 프로그램 영역마다 다른 프로그램과 실패입니다. 첫 예제의 IllegalStateException은 NetworkException catch 및 뒤 disconnect를 건너뜁니다. 둘째는 지역 catch 후 finally의 disconnect와 normal-end로 이어집니다. 셋째는 timeout을 처리하지 않고 finally의 close가 정상 완료한 뒤 main의 경계가 처리합니다. finally의 새 실패나 return, JVM 종료까지 포함한 절대 보장으로 일반화하지 않습니다. 원문 A · catch 뒤 정리 connect 뒤 실패 connected 출력 IllegalStateException catch가 안 맞음 NetworkException만 잡음 실제 예외는 통과 뒤 정리 호출 건너뜀 disconnect 호출 안 함 미처리 예외로 스레드 끝 원문 B · catch + finally connect 뒤 send 실패 connected 출력 NetworkException catch의 지역 복구 handled=send failed finally 뒤 정상 끝 disconnected 출력 normal-end 출력 원문 C · try + finally open 뒤 timeout open 출력 예외는 아직 처리 안 함 finally의 정리 close 출력 정리가 정상 완료 main의 경계 catch boundary=timeout 서로 다른 프로그램·실패 실선은 실제 전달·정리 순서 finally가 정상 완료한 경로
정리 위치와 예외 처리 경계를 나눠 읽기
원문 구성실패·처리와 정리관찰 순서
catch 뒤 정리send()의 IllegalStateException은 NetworkException catch와 뒤의 disconnect()를 건너뜁니다.
connected
이후 미처리 예외로 종료
disconnected 출력 없음
catch + finallysend("error2")의 예외를 catch가 처리하고, finally의 disconnect()도 정상 완료합니다.
connected
handled=send failed
disconnected
normal-end
try + finallyexecute()는 timeout을 잡지 않고 close()만 수행합니다. main의 catch가 전파된 예외를 처리합니다.
open
close
boundary=timeout
catch 뒤 정리
실패·처리와 정리
send()의 IllegalStateException은 NetworkException catch와 뒤의 disconnect()를 건너뜁니다.
관찰 순서
connected
이후 미처리 예외로 종료
disconnected 출력 없음
catch + finally
실패·처리와 정리
send("error2")의 예외를 catch가 처리하고, finally의 disconnect()도 정상 완료합니다.
관찰 순서
connected
handled=send failed
disconnected
normal-end
try + finally
실패·처리와 정리
execute()는 timeout을 잡지 않고 close()만 수행합니다. main의 catch가 전파된 예외를 처리합니다.
관찰 순서
open
close
boundary=timeout

세 번째 예제의 close()는 정상 완료합니다. finally가 return이나 새 예외로 끝나면 기존 결과를 대체할 수 있으며, JVM 종료까지 정리를 보장하는 뜻은 아닙니다. 첫 예제의 실패 진단은 stderr이고 connected만 stdout입니다.

이 예의 조건

  • 서로 다른 세 프로그램이며 finally가 정상 완료하는 경로다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보정리 위치와 예외 처리 경계를 나눠 읽기 정리 위치와 예외 처리 경계를 나눠 읽기 정리 호출을 catch 뒤에 둔 첫 예제는 disconnect를 건너뛴다. finally에서 정리하는 두 예제는 각각 지역 catch 후 normal-end로 이어지거나 상위 경계에 timeout을 전달한다. 정리 책임 · 세 구현 비교 정리 위치와 예외 처리 경계를 나눠 읽기 서로 다른 실패를 사용하는 세 프로그램입니다. 정리의 위치와 예외를 처리하는 경계를 나눠 비교합니다. 정리 위치와 예외 처리 경계를 나눠 읽기 원문 구성 실패·처리와 정리 관찰 순서 catch 뒤 정리 send() 의 IllegalStateException 은 NetworkException catch와 뒤의 disconnect() 를 건너뜁니다. connected 이후 미처리 예외로 종료 disconnected 출력 없음 catch + finally send("error2") 의 예외를 catch가 처리하고, finally의 disconnect() 도 정상 완료합니다. connected handled=send failed disconnected normal-end try + finally execute() 는 timeout을 잡지 않고 close() 만 수행합니다. main의 catch가 전파된 예외를 처리합니다. open close boundary=timeout catch 뒤 정리 실패·처리와 정리 send() 의 IllegalStateException 은 NetworkException catch와 뒤의 disconnect() 를 건너뜁니다. 관찰 순서 connected 이후 미처리 예외로 종료 disconnected 출력 없음 catch + finally 실패·처리와 정리 send("error2") 의 예외를 catch가 처리하고, finally의 disconnect() 도 정상 완료합니다. 관찰 순서 connected handled=send failed disconnected normal-end try + finally 실패·처리와 정리 execute() 는 timeout을 잡지 않고 close() 만 수행합니다. main의 catch가 전파된 예외를 처리합니다. 관찰 순서 open close boundary=timeout 세 번째 예제의 close() 는 정상 완료합니다. finally가 return이나 새 예외로 끝나면 기존 결과를 대체할 수 있으며, JVM 종료까지 정리를 보장하는 뜻은 아닙니다. 첫 예제의 실패 진단은 stderr이고 connected 만 stdout입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
원 실패

각 프로그램의 작업에서 실패가 발생합니다.

  • A: IllegalStateException
  • B: NetworkException
  • C: timeout
  • 실제 예외 타입과 처리 경계를 비교한다.
지역 처리/정리

A는 catch 불일치, B는 catch 뒤 finally, C는 finally만 진행합니다.

  • A: disconnect 미호출
  • B: handled=send failed → disconnected
  • C: close
  • 정리가 예외 처리와 동일 동작은 아니다.
호출 종료

각 실패가 서로 다른 종료로 이어집니다.

  • A: 미처리 스레드 종료
  • B: normal-end
  • C: boundary=timeout
  • finally 정상 완료는 전달 중인 예외를 자동 복구하지 않는다.

예외 계층과 catch 범위

연결 실패는 다른 주소로 재시도할 수 있지만 전송 실패는 사용자에게 입력 보존 안내만 할 수 있다고 가정합니다.

하나의 오류 코드 필드보다 하위 예외 타입을 만들면 catch가 타입별 데이터와 복구 정책을 선택할 수 있습니다.

src/NetworkExceptionHierarchyFlow.java
public final class NetworkExceptionHierarchyFlow {
    public static void main(String[] args) {
        run("connect");
        run("send");
    }

    private static void run(String scenario) {
        NetworkClient client = new NetworkClient(scenario);
        try {
            client.connect();
            client.send("board-data");
        } catch (ConnectException error) {
            System.out.println("retry-address=" + error.address());
        } catch (SendException error) {
            System.out.println("preserve-data=" + error.data());
        } finally {
            client.disconnect();
        }
    }

    private record NetworkClient(String scenario) {
        void connect() throws ConnectException {
            if (scenario.equals("connect")) {
                throw new ConnectException("https://board.local");
            }
        }

        void send(String data) throws SendException {
            if (scenario.equals("send")) throw new SendException(data);
        }

        void disconnect() { System.out.println("close=" + scenario); }
    }

    private static class NetworkException extends Exception {
        NetworkException(String message) { super(message); }
    }

    private static final class ConnectException extends NetworkException {
        private final String address;
        ConnectException(String address) { super("connect " + address); this.address = address; }
        String address() { return address; }
    }

    private static final class SendException extends NetworkException {
        private final String data;
        SendException(String data) { super("send " + data); this.data = data; }
        String data() { return data; }
    }
}
retry-address=https://board.local
close=connect
preserve-data=board-data
close=send

이 예제의 NetworkException은 공통 부모이고, 실제 메서드는 각각 throws ConnectException, throws SendException을 선언합니다. 자식은 복구에 필요한 문맥을 제공합니다.

모든 자식을 같은 방식으로 처리하면 부모 하나를 잡아도 되지만, 구체 정책이 다르면 자식 catch를 먼저 둡니다.

서로 다른 catch와 공통 disconnect

같은 main의 connect/send 호출이 각각 catch 뒤 finally로 자원을 닫습니다. 실제 네 출력의 순서를 추적하면 오류 안내와 실제 재시도를 구분할 수 있습니다.

서로 다른 catch와 공통 disconnect

NetworkExceptionHierarchyFlow의 connect 시나리오는 ConnectException 주소를 출력한 뒤 close=connect로 끝나며 send 시나리오는 SendException 데이터를 출력한 뒤 close=send로 끝난다.

구체 catch · 공통 정리

서로 다른 catch와 공통 disconnect

연결과 전송 실패는 서로 다른 catch에서 문맥을 읽습니다. 두 호출 모두 finally에서 disconnect를 실행합니다.

실제 실패 문맥과 공통 finally 정리 같은 main이 connect와 send 시나리오를 차례로 호출합니다. 연결 실패의 ConnectException catch는 주소를 읽고 전송 실패의 SendException catch는 데이터를 읽습니다. 각 호출의 finally가 정상 완료하며 close=connect 또는 close=send를 출력합니다. 정책 문자열을 출력할 뿐 재시도나 저장을 실행하지 않습니다. 첫 호출 · connect 입력 connect에서 던짐 ConnectException send에는 도달 안 함 연결 실패 catch 주소 문맥을 읽음 retry-address= https://board.local finally의 disconnect close=connect 다음 호출 · send 입력 send에서 던짐 connect는 정상 완료 SendException 전송 실패 catch 데이터 문맥을 읽음 preserve-data=board-data finally의 disconnect close=send 한 main의 서로 다른 두 호출 · 실선은 각 호출의 실제 순서 · 재시도·데이터 저장 실행 아님
실제 실패 문맥과 공통 finally 정리 같은 main이 connect와 send 시나리오를 차례로 호출합니다. 연결 실패의 ConnectException catch는 주소를 읽고 전송 실패의 SendException catch는 데이터를 읽습니다. 각 호출의 finally가 정상 완료하며 close=connect 또는 close=send를 출력합니다. 정책 문자열을 출력할 뿐 재시도나 저장을 실행하지 않습니다. 첫 호출 · connect 입력 connect에서 던짐 ConnectException send에는 도달 안 함 연결 실패 catch 주소 문맥을 읽음 retry-address= https://board.local finally의 disconnect close=connect 다음 호출 · send 입력 send에서 던짐 connect는 정상 완료 SendException 전송 실패 catch 데이터 문맥을 읽음 preserve-data=board-data finally의 disconnect close=send 한 main의 서로 다른 두 호출 실선은 각 호출의 실제 순서 재시도·데이터 저장 실행 아님

run("connect")

도달한 실패와 catch
connect()가 ConnectException을 던져 주소를 받습니다. send()는 도달하지 않습니다.
원문 출력
retry-address=https://board.local
close=connect

run("send")

도달한 실패와 catch
connect()가 끝난 뒤 send()가 SendException을 던져 데이터를 받습니다.
원문 출력
preserve-data=board-data
close=send

출력 박스의 줄은 원문 stdout과 같습니다. 실제 재시도나 저장은 수행하지 않고 정책에 필요한 값만 출력합니다. 메서드는 각각 throws ConnectException, throws SendException을 선언하며 NetworkException은 공통 부모입니다.

이 예의 조건

  • 정책 출력만 수행하며 실제 재시도·데이터 저장은 없다.
  • 두 시나리오는 동일 main의 서로 다른 호출이다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보서로 다른 catch와 공통 disconnect 서로 다른 catch와 공통 disconnect NetworkExceptionHierarchyFlow의 connect 시나리오는 ConnectException 주소를 출력한 뒤 close=connect로 끝나며 send 시나리오는 SendException 데이터를 출력한 뒤 close=send로 끝난다. 구체 catch · 공통 정리 서로 다른 catch와 공통 disconnect 연결과 전송 실패는 서로 다른 catch에서 문맥을 읽습니다. 두 호출 모두 finally에서 disconnect를 실행합니다. run("connect") 도달한 실패와 catch connect() 가 ConnectException 을 던져 주소를 받습니다. send() 는 도달하지 않습니다. 원문 출력 retry-address=https://board.local close=connect run("send") 도달한 실패와 catch connect() 가 끝난 뒤 send() 가 SendException 을 던져 데이터를 받습니다. 원문 출력 preserve-data=board-data close=send 출력 박스의 줄은 원문 stdout과 같습니다. 실제 재시도나 저장은 수행하지 않고 정책에 필요한 값만 출력합니다. 메서드는 각각 throws ConnectException, throws SendException을 선언하며 NetworkException은 공통 부모입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
연결 시나리오

connect 실패로 send를 건너뜁니다.

  • 예외: ConnectException
  • send: 미도달
  • 연결 실패가 후속 전송 허가를 없앤다.
연결 안내와 정리

주소 문맥을 출력하고 finally가 닫습니다.

  • 정책: retry-address=https://board.local
  • 정리: close=connect
  • 실패를 안내한 뒤 이 호출의 자원을 정리한다.
전송 시나리오

다음 호출의 connect 성공 뒤 send가 실패합니다.

  • 예외: SendException
  • 문맥: board-data
  • 연결 성공과 전송 성공은 서로 다른 조건이다.
전송 안내와 정리

데이터 문맥 안내 뒤 finally가 닫습니다.

  • 정책: preserve-data=board-data
  • 정리: close=send
  • 실패 문맥을 보존하는 정책 문자열과 실제 저장을 구분한다.

서로 다른 catch와 공통 disconnect

같은 main의 connect/send 호출이 각각 catch 뒤 finally로 자원을 닫습니다. 실제 네 출력의 순서를 추적하면 오류 안내와 실제 재시도를 구분할 수 있습니다.

서로 다른 catch와 공통 disconnect

NetworkExceptionHierarchyFlow의 connect 시나리오는 ConnectException 주소를 출력한 뒤 close=connect로 끝나며 send 시나리오는 SendException 데이터를 출력한 뒤 close=send로 끝난다.

구체 catch · 공통 정리

서로 다른 catch와 공통 disconnect

연결과 전송 실패는 서로 다른 catch에서 문맥을 읽습니다. 두 호출 모두 finally에서 disconnect를 실행합니다.

실제 실패 문맥과 공통 finally 정리 같은 main이 connect와 send 시나리오를 차례로 호출합니다. 연결 실패의 ConnectException catch는 주소를 읽고 전송 실패의 SendException catch는 데이터를 읽습니다. 각 호출의 finally가 정상 완료하며 close=connect 또는 close=send를 출력합니다. 정책 문자열을 출력할 뿐 재시도나 저장을 실행하지 않습니다. 첫 호출 · connect 입력 connect에서 던짐 ConnectException send에는 도달 안 함 연결 실패 catch 주소 문맥을 읽음 retry-address= https://board.local finally의 disconnect close=connect 다음 호출 · send 입력 send에서 던짐 connect는 정상 완료 SendException 전송 실패 catch 데이터 문맥을 읽음 preserve-data=board-data finally의 disconnect close=send 한 main의 서로 다른 두 호출 · 실선은 각 호출의 실제 순서 · 재시도·데이터 저장 실행 아님
실제 실패 문맥과 공통 finally 정리 같은 main이 connect와 send 시나리오를 차례로 호출합니다. 연결 실패의 ConnectException catch는 주소를 읽고 전송 실패의 SendException catch는 데이터를 읽습니다. 각 호출의 finally가 정상 완료하며 close=connect 또는 close=send를 출력합니다. 정책 문자열을 출력할 뿐 재시도나 저장을 실행하지 않습니다. 첫 호출 · connect 입력 connect에서 던짐 ConnectException send에는 도달 안 함 연결 실패 catch 주소 문맥을 읽음 retry-address= https://board.local finally의 disconnect close=connect 다음 호출 · send 입력 send에서 던짐 connect는 정상 완료 SendException 전송 실패 catch 데이터 문맥을 읽음 preserve-data=board-data finally의 disconnect close=send 한 main의 서로 다른 두 호출 실선은 각 호출의 실제 순서 재시도·데이터 저장 실행 아님

run("connect")

도달한 실패와 catch
connect()가 ConnectException을 던져 주소를 받습니다. send()는 도달하지 않습니다.
원문 출력
retry-address=https://board.local
close=connect

run("send")

도달한 실패와 catch
connect()가 끝난 뒤 send()가 SendException을 던져 데이터를 받습니다.
원문 출력
preserve-data=board-data
close=send

출력 박스의 줄은 원문 stdout과 같습니다. 실제 재시도나 저장은 수행하지 않고 정책에 필요한 값만 출력합니다. 메서드는 각각 throws ConnectException, throws SendException을 선언하며 NetworkException은 공통 부모입니다.

이 예의 조건

  • 정책 출력만 수행하며 실제 재시도·데이터 저장은 없다.
  • 두 시나리오는 동일 main의 서로 다른 호출이다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보서로 다른 catch와 공통 disconnect 서로 다른 catch와 공통 disconnect NetworkExceptionHierarchyFlow의 connect 시나리오는 ConnectException 주소를 출력한 뒤 close=connect로 끝나며 send 시나리오는 SendException 데이터를 출력한 뒤 close=send로 끝난다. 구체 catch · 공통 정리 서로 다른 catch와 공통 disconnect 연결과 전송 실패는 서로 다른 catch에서 문맥을 읽습니다. 두 호출 모두 finally에서 disconnect를 실행합니다. run("connect") 도달한 실패와 catch connect() 가 ConnectException 을 던져 주소를 받습니다. send() 는 도달하지 않습니다. 원문 출력 retry-address=https://board.local close=connect run("send") 도달한 실패와 catch connect() 가 끝난 뒤 send() 가 SendException 을 던져 데이터를 받습니다. 원문 출력 preserve-data=board-data close=send 출력 박스의 줄은 원문 stdout과 같습니다. 실제 재시도나 저장은 수행하지 않고 정책에 필요한 값만 출력합니다. 메서드는 각각 throws ConnectException, throws SendException을 선언하며 NetworkException은 공통 부모입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
연결 시나리오

connect 실패로 send를 건너뜁니다.

  • 예외: ConnectException
  • send: 미도달
  • 연결 실패가 후속 전송 허가를 없앤다.
연결 안내와 정리

주소 문맥을 출력하고 finally가 닫습니다.

  • 정책: retry-address=https://board.local
  • 정리: close=connect
  • 실패를 안내한 뒤 이 호출의 자원을 정리한다.
전송 시나리오

다음 호출의 connect 성공 뒤 send가 실패합니다.

  • 예외: SendException
  • 문맥: board-data
  • 연결 성공과 전송 성공은 서로 다른 조건이다.
전송 안내와 정리

데이터 문맥 안내 뒤 finally가 닫습니다.

  • 정책: preserve-data=board-data
  • 정리: close=send
  • 실패 문맥을 보존하는 정책 문자열과 실제 저장을 구분한다.

finally에 부적합한 코드

finally에는 자원 해제, 잠금 해제, 임시 상태 복구처럼 성공 여부와 관계없이 필요한 최소 동작을 둡니다.

return을 넣으면 try의 반환값이나 진행 중인 예외를 덮을 수 있으므로 피합니다.

예시

다음은 피해야 하는 동작을 따로 관찰하는 실패 설계 예제입니다. returnCase는 원래 10을 반환하려 했고 exceptionCase는 원래 예외를 전달하려 했지만, finally의 return이 두 결과를 모두 대체합니다.

lab/FinallyOutcomeOverride.java
public final class FinallyOutcomeOverride {
    public static void main(String[] args) {
        System.out.println("return=" + returnCase());
        System.out.println("exception=" + exceptionCase());
    }

    private static int returnCase() {
        try {
            return 10;
        } finally {
            return 20;
        }
    }

    private static int exceptionCase() {
        try {
            throw new IllegalStateException("work failed");
        } finally {
            return 20;
        }
    }
}
return=20
exception=20

예외를 정상적으로 복구한 것이 아니라 실패 전달을 없애 버린 결과입니다. JLS의 finally 규칙은 finally 자체가 return이나 throw로 끝나면 앞서 진행 중이던 결과를 대체하도록 정합니다. 정리 후 원래 결과를 유지하려면 finally가 정상 완료해야 합니다.

정리 중 발생한 예외를 무조건 삼키는 것도 장애 증거를 없앱니다.

긴 업무 로직을 finally에 넣으면 원래 실패와 정리 실패의 우선순위가 모호해집니다.

닫기 작업은 가능하면 멱등적으로 설계하고, 미연결 상태 close가 안전한지 계약을 정합니다.

여러 자원을 역순으로 닫아야 하거나 close 실패까지 보존해야 하면 AutoCloseable과 try-with-resources를 선택합니다.


연습 문제

Connection을 먼저 열고 Channel을 나중에 열었습니다.

전송 중 예외가 발생해도 Channel, Connection 순서로 닫히고 경계에서 원래 메시지를 확인하도록 중첩 try/finally를 작성하세요.

이 연습에서는 Resource.open과 close는 각각 open=이름, close=이름을 출력하고 정상 완료합니다. 두 자원을 순서대로 연 뒤 전송 단계가 IllegalStateException("send failed")를 던집니다. 경계는 boundary=메시지를 출력합니다.

독립 연습에서는 전송을 sent 출력 후 정상 반환으로 바꿉니다. 두 close의 순서와 경계 catch가 실행되는지 예상하세요. open이나 close도 실패하는 경우는 다음 절의 주 예외·억제 예외 조건과 구별합니다.

해설 보기
src/NestedFinallyExercise.java
public final class NestedFinallyExercise {
    public static void main(String[] args) {
        try {
            useResources();
        } catch (IllegalStateException error) {
            System.out.println("boundary=" + error.getMessage());
        }
    }

    private static void useResources() {
        Resource connection = new Resource("connection");
        connection.open();
        try {
            Resource channel = new Resource("channel");
            channel.open();
            try {
                throw new IllegalStateException("send failed");
            } finally {
                channel.close();
            }
        } finally {
            connection.close();
        }
    }

    private record Resource(String name) {
        void open() { System.out.println("open=" + name); }
        void close() { System.out.println("close=" + name); }
    }
}
open=connection
open=channel
close=channel
close=connection
boundary=send failed

나중에 확보한 자원을 먼저 반환해야 의존 관계가 깨지지 않습니다.

독립 정상 입력에서는 open=connection → open=channel → sent → close=channel → close=connection입니다. 예외가 없어 boundary 로그는 없습니다. 정리는 정상·실패 두 경우에 필요하지만 catch는 일치하는 실패가 있을 때만 실행합니다.

중첩 구조가 복잡해지는 순간 try-with-resources가 같은 순서를 더 안전하고 간결하게 표현합니다.