본문 바로가기

비검사 예외와 복구 경계

RuntimeException이 선언 없이 전파되어 프로그램을 종료하는 상황을 재현하고 체크 여부를 복구 가능성과 API 안정성에 맞춰 선택합니다.

언체크 예외는 처리하지 않아도 된다는 뜻이지 처리해서는 안 된다는 뜻이 아닙니다.

컴파일러가 catch나 throws를 강제하지 않으므로, 어떤 계층에서 복구하고 어떤 경계에서 공통 처리할지 설계가 더 중요합니다.

먼저 아무도 잡지 않은 예외가 호출 스택을 거슬러 올라가는 실제 결과를 봅니다.


처리되지 않은 런타임 예외

저장소가 처리할 수 없는 상태를 사용자 정의 언체크 예외로 던집니다.

서비스와 main 어느 곳에도 catch가 없습니다.

각 메서드에 throws를 적지 않아도 컴파일은 성공하지만 실행은 정상 종료 문장에 도달하지 못합니다.

lab/UncaughtUncheckedBoardException.java
public final class UncaughtUncheckedBoardException {
    public static void main(String[] args) {
        BoardService service = new BoardService();
        service.load("corrupt");
        System.out.println("normal-end");
    }

    private static final class BoardService {
        void load(String id) { repositoryCall(id); }

        private void repositoryCall(String id) {
            throw new PostDataException("invalid record: " + id);
        }
    }

    private static final class PostDataException extends RuntimeException {
        PostDataException(String message) { super(message); }
    }
}
실행 결과의 핵심
Exception in thread "main" UncaughtUncheckedBoardException$PostDataException:
invalid record: corrupt

이 단독 main 예제는 기본 미처리 예외 처리기가 타입·메시지·스택 추적을 표준 오류에 출력하고, main 스레드가 끝나면서 프로세스도 실패 코드로 종료됩니다. 일반적으로는 예외가 난 스레드가 종료되는 것이며, 다른 비데몬 스레드가 남은 프로그램 전체를 즉시 끝내는 것은 아닙니다.

normal-end는 출력되지 않습니다.

예외가 발생한 지점 이후의 정상 흐름은 중단되고, 일치하는 catch를 찾을 때까지 호출자가 차례로 빠져나갑니다.

잡히지 않은 예외 뒤 normal-end는 실행되지 않는다

main→load→repositoryCall 호출 뒤 동일 예외가 호출자를 빠져나오고 normal-end에 도달하지 않는 추적입니다. 호출 방향과 역방향 전파·스레드 종료를 단계로 따라가기 좋습니다.

잡히지 않은 예외 뒤 normal-end는 실행되지 않는다

UncaughtUncheckedBoardException은 repositoryCall에서 PostDataException을 던지고 load와 main에 catch가 없다. normal-end는 출력되지 않으며 해당 main 스레드가 미처리 예외로 끝난다.

미처리 예외 · 빠져나오는 순서

잡히지 않은 예외 뒤 normal-end는 실행되지 않는다

service.load 호출 뒤 repositoryCall에서 예외가 발생합니다. catch가 없어 load와 main을 빠져나오며 normal-end에 도달하지 않습니다.

호출 방향과 같은 예외가 빠져나오는 방향 실선은 main에서 load와 repositoryCall로 들어가는 호출 및 최종 기본 처리입니다. 점선은 repositoryCall에서 발생한 같은 PostDataException이 load와 main을 빠져나오는 전달 방향입니다. normal-end는 실행되지 않고 단독 예제의 main 스레드가 끝나며 기본 처리의 stderr 진단이 남습니다. 다른 모든 스레드 종료를 일반화하지 않습니다. main 호출 중 load("corrupt") normal-end에 못 감 load 호출 중 repositoryCall 호출 catch 없음 repositoryCall 실패 PostDataException invalid record: corrupt main 스레드 종료 기본 처리의 stderr 진단 타입·메시지·스택 실선: 호출과 최종 처리 · 점선: 같은 예외가 호출자를 빠져나오는 방향 · 모든 스레드 종료의 뜻 아님
호출 방향과 같은 예외가 빠져나오는 방향 실선은 main에서 load와 repositoryCall로 들어가는 호출 및 최종 기본 처리입니다. 점선은 repositoryCall에서 발생한 같은 PostDataException이 load와 main을 빠져나오는 전달 방향입니다. normal-end는 실행되지 않고 단독 예제의 main 스레드가 끝나며 기본 처리의 stderr 진단이 남습니다. 다른 모든 스레드 종료를 일반화하지 않습니다. main 호출 중 load("corrupt") normal-end에 못 감 load 호출 중 repositoryCall 호출 catch 없음 repositoryCall 실패 PostDataException invalid record: corrupt main 스레드 종료 기본 처리의 stderr 진단 타입·메시지·스택 실선: 호출과 최종 처리 점선: 같은 예외가 호출자를 빠져나오며 전파되는 방향
  1. repositoryCall

    발생·전달
    PostDataException 발생
    메시지: invalid record: corrupt
    정상 흐름의 결과
    정상 반환하지 않습니다.
  2. load

    발생·전달
    catch가 없어 같은 예외가 호출자로 전달됩니다.
    정상 흐름의 결과
    서비스 호출이 정상 완료하지 않습니다.
  3. main

    발생·전달
    catch가 없어 미처리 예외가 됩니다.
    정상 흐름의 결과
    normal-end 출력 문장에 도달하지 않습니다.

이 단독 main 예제의 기본 미처리 예외 처리에서는 타입·메시지·스택 추적이 표준 오류로 출력됩니다. 예외는 해당 스레드를 끝내는 것이며, 다른 비데몬 스레드가 있는 모든 프로그램의 즉시 종료를 뜻하지 않습니다.

이 예의 조건

  • 단독 예제 main 스레드의 종료이며 모든 스레드 종료로 일반화하지 않는다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보잡히지 않은 예외 뒤 normal-end는 실행되지 않는다 잡히지 않은 예외 뒤 normal-end는 실행되지 않는다 UncaughtUncheckedBoardException은 repositoryCall에서 PostDataException을 던지고 load와 main에 catch가 없다. normal-end는 출력되지 않으며 해당 main 스레드가 미처리 예외로 끝난다. 미처리 예외 · 빠져나오는 순서 잡히지 않은 예외 뒤 normal-end는 실행되지 않는다 service.load 호출 뒤 repositoryCall에서 예외가 발생합니다. catch가 없어 load와 main을 빠져나오며 normal-end에 도달하지 않습니다. repositoryCall 발생·전달 PostDataException 발생 메시지: invalid record: corrupt 정상 흐름의 결과 정상 반환하지 않습니다. load 발생·전달 catch 가 없어 같은 예외가 호출자로 전달됩니다. 정상 흐름의 결과 서비스 호출이 정상 완료하지 않습니다. main 발생·전달 catch 가 없어 미처리 예외가 됩니다. 정상 흐름의 결과 normal-end 출력 문장에 도달하지 않습니다. 이 단독 main 예제의 기본 미처리 예외 처리에서는 타입·메시지·스택 추적이 표준 오류로 출력됩니다. 예외는 해당 스레드를 끝내는 것이며, 다른 비데몬 스레드가 있는 모든 프로그램의 즉시 종료를 뜻하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
호출 체인

main에서 load(corrupt), repositoryCall로 들어갑니다.

  • 활성: repositoryCall
  • 대기 호출자: load,main
  • 안쪽 호출의 완료가 바깥 후속 문장의 조건이다.
동일 예외 전파

PostDataException이 catch 없는 두 호출자를 빠져나옵니다.

  • 예외: invalid record: corrupt
  • 처리 catch: 없음
  • 예외 객체를 새 결과로 복구하지 않는다.
미처리 종료

normal-end를 건너뛰고 기본 처리가 진단합니다.

  • normal-end: 미실행
  • 진단: stderr 타입·메시지·스택
  • 종료 대상: main 스레드
  • 정상 호출 체인의 후속 문장을 실행하지 못한다.

잡히지 않은 예외 뒤 normal-end는 실행되지 않는다

main→load→repositoryCall 호출 뒤 동일 예외가 호출자를 빠져나오고 normal-end에 도달하지 않는 추적입니다. 호출 방향과 역방향 전파·스레드 종료를 단계로 따라가기 좋습니다.

잡히지 않은 예외 뒤 normal-end는 실행되지 않는다

UncaughtUncheckedBoardException은 repositoryCall에서 PostDataException을 던지고 load와 main에 catch가 없다. normal-end는 출력되지 않으며 해당 main 스레드가 미처리 예외로 끝난다.

미처리 예외 · 빠져나오는 순서

잡히지 않은 예외 뒤 normal-end는 실행되지 않는다

service.load 호출 뒤 repositoryCall에서 예외가 발생합니다. catch가 없어 load와 main을 빠져나오며 normal-end에 도달하지 않습니다.

호출 방향과 같은 예외가 빠져나오는 방향 실선은 main에서 load와 repositoryCall로 들어가는 호출 및 최종 기본 처리입니다. 점선은 repositoryCall에서 발생한 같은 PostDataException이 load와 main을 빠져나오는 전달 방향입니다. normal-end는 실행되지 않고 단독 예제의 main 스레드가 끝나며 기본 처리의 stderr 진단이 남습니다. 다른 모든 스레드 종료를 일반화하지 않습니다. main 호출 중 load("corrupt") normal-end에 못 감 load 호출 중 repositoryCall 호출 catch 없음 repositoryCall 실패 PostDataException invalid record: corrupt main 스레드 종료 기본 처리의 stderr 진단 타입·메시지·스택 실선: 호출과 최종 처리 · 점선: 같은 예외가 호출자를 빠져나오는 방향 · 모든 스레드 종료의 뜻 아님
호출 방향과 같은 예외가 빠져나오는 방향 실선은 main에서 load와 repositoryCall로 들어가는 호출 및 최종 기본 처리입니다. 점선은 repositoryCall에서 발생한 같은 PostDataException이 load와 main을 빠져나오는 전달 방향입니다. normal-end는 실행되지 않고 단독 예제의 main 스레드가 끝나며 기본 처리의 stderr 진단이 남습니다. 다른 모든 스레드 종료를 일반화하지 않습니다. main 호출 중 load("corrupt") normal-end에 못 감 load 호출 중 repositoryCall 호출 catch 없음 repositoryCall 실패 PostDataException invalid record: corrupt main 스레드 종료 기본 처리의 stderr 진단 타입·메시지·스택 실선: 호출과 최종 처리 점선: 같은 예외가 호출자를 빠져나오며 전파되는 방향
  1. repositoryCall

    발생·전달
    PostDataException 발생
    메시지: invalid record: corrupt
    정상 흐름의 결과
    정상 반환하지 않습니다.
  2. load

    발생·전달
    catch가 없어 같은 예외가 호출자로 전달됩니다.
    정상 흐름의 결과
    서비스 호출이 정상 완료하지 않습니다.
  3. main

    발생·전달
    catch가 없어 미처리 예외가 됩니다.
    정상 흐름의 결과
    normal-end 출력 문장에 도달하지 않습니다.

이 단독 main 예제의 기본 미처리 예외 처리에서는 타입·메시지·스택 추적이 표준 오류로 출력됩니다. 예외는 해당 스레드를 끝내는 것이며, 다른 비데몬 스레드가 있는 모든 프로그램의 즉시 종료를 뜻하지 않습니다.

이 예의 조건

  • 단독 예제 main 스레드의 종료이며 모든 스레드 종료로 일반화하지 않는다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보잡히지 않은 예외 뒤 normal-end는 실행되지 않는다 잡히지 않은 예외 뒤 normal-end는 실행되지 않는다 UncaughtUncheckedBoardException은 repositoryCall에서 PostDataException을 던지고 load와 main에 catch가 없다. normal-end는 출력되지 않으며 해당 main 스레드가 미처리 예외로 끝난다. 미처리 예외 · 빠져나오는 순서 잡히지 않은 예외 뒤 normal-end는 실행되지 않는다 service.load 호출 뒤 repositoryCall에서 예외가 발생합니다. catch가 없어 load와 main을 빠져나오며 normal-end에 도달하지 않습니다. repositoryCall 발생·전달 PostDataException 발생 메시지: invalid record: corrupt 정상 흐름의 결과 정상 반환하지 않습니다. load 발생·전달 catch 가 없어 같은 예외가 호출자로 전달됩니다. 정상 흐름의 결과 서비스 호출이 정상 완료하지 않습니다. main 발생·전달 catch 가 없어 미처리 예외가 됩니다. 정상 흐름의 결과 normal-end 출력 문장에 도달하지 않습니다. 이 단독 main 예제의 기본 미처리 예외 처리에서는 타입·메시지·스택 추적이 표준 오류로 출력됩니다. 예외는 해당 스레드를 끝내는 것이며, 다른 비데몬 스레드가 있는 모든 프로그램의 즉시 종료를 뜻하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
호출 체인

main에서 load(corrupt), repositoryCall로 들어갑니다.

  • 활성: repositoryCall
  • 대기 호출자: load,main
  • 안쪽 호출의 완료가 바깥 후속 문장의 조건이다.
동일 예외 전파

PostDataException이 catch 없는 두 호출자를 빠져나옵니다.

  • 예외: invalid record: corrupt
  • 처리 catch: 없음
  • 예외 객체를 새 결과로 복구하지 않는다.
미처리 종료

normal-end를 건너뛰고 기본 처리가 진단합니다.

  • normal-end: 미실행
  • 진단: stderr 타입·메시지·스택
  • 종료 대상: main 스레드
  • 정상 호출 체인의 후속 문장을 실행하지 못한다.

비검사 예외의 문서화

RuntimeException 하위 타입은 throws를 생략할 수 있지만 중요한 실패 계약을 IDE와 API 문서에 드러내려면 선언할 수 있습니다.

선언하더라도 호출자에게 컴파일 강제가 생기지는 않습니다.

src/DocumentedUncheckedContract.java
public final class DocumentedUncheckedContract {
    public static void main(String[] args) {
        Post entry = Post.create("exception", 45);
        System.out.println(entry);

        try {
            Post.create("exception", -1);
        } catch (InvalidPostException error) {
            System.out.println("rejected=" + error.getMessage());
        }
    }

    private record Post(String title, int viewCount) {
        static Post create(String title, int viewCount)
                throws InvalidPostException {
            if (title == null || title.isBlank()) {
                throw new InvalidPostException("blank title");
            }
            if (viewCount <= 0) {
                throw new InvalidPostException("viewCount=" + viewCount);
            }
            return new Post(title, viewCount);
        }
    }

    private static final class InvalidPostException extends RuntimeException {
        InvalidPostException(String message) { super(message); }
    }
}
Post[title=exception, viewCount=45]
rejected=viewCount=-1

이 오류는 같은 인수로 다시 호출해도 성공하지 않습니다.

현재 값이 생성자 계약을 어겼으므로 호출 지점 또는 입력 경계가 값을 고쳐야 합니다.

깊은 계층이 무의미하게 재시도하는 대신 실패를 전달하는 것이 맞습니다.


검사 예외와 비검사 예외

체크 예외는 호출자가 놓치면 안 되는 복구 가능한 조건에 유용합니다.

예를 들어 사용자가 다른 파일을 고를 수 있는 파일 선택 API나, 업무상 반드시 승인/거절을 구분해야 하는 연동은 호출자 결정을 강제할 가치가 있습니다.

하지만 네트워크·DB 장애처럼 대부분의 중간 계층이 해결할 수 없는 예외를 모두 체크로 만들면 throws가 계층마다 반복됩니다.

언체크 예외는 복구 불가능한 시스템 장애, 프로그래밍 계약 위반, 중간 계층이 모르는 인프라 실패를 상위 경계로 보내기 좋습니다.

대신 API 문서, 명확한 예외 계층, 공통 처리기가 필요합니다.

“귀찮아서 RuntimeException”은 선택 기준이 아니며, 실제 호출자가 무엇을 할 수 있는지가 기준입니다.

src/UncheckedExceptionTranslation.java
public final class UncheckedExceptionTranslation {
    public static void main(String[] args) {
        BoardService service = new BoardService(new BrokenStore());
        try {
            service.summary("java");
        } catch (BoardSystemException error) {
            System.out.println("public=" + error.getMessage());
            System.out.println("root=" + error.getCause().getClass().getSimpleName());
        }
    }

    private static final class BoardService {
        private final BoardStore store;
        BoardService(BoardStore store) { this.store = store; }

        String summary(String title) {
            try {
                return title + "=" + store.viewCount(title);
            } catch (StoreAccessException cause) {
                throw new BoardSystemException("board data unavailable", cause);
            }
        }
    }

    private interface BoardStore { int viewCount(String title); }

    private static final class BrokenStore implements BoardStore {
        public int viewCount(String title) {
            throw new StoreAccessException("disk read failed: " + title);
        }
    }

    private static final class StoreAccessException extends RuntimeException {
        StoreAccessException(String message) { super(message); }
    }

    private static final class BoardSystemException extends RuntimeException {
        BoardSystemException(String message, Throwable cause) { super(message, cause); }
    }
}
public=board data unavailable
root=StoreAccessException

서비스는 저장 장치 예외를 업무 경계의 예외로 변환합니다.

이런 예외 변환 덕분에 호출자는 디스크 구현을 몰라도 됩니다.

현재 main은 service.summary가 반환하는 문자열을 출력하지 않고, 실패했을 때만 catch의 두 줄을 출력합니다. 별도 실험에서 저장소 구현을 return 40으로 바꾸면 서비스는 java=40을 반환하지만 현재 main의 표준 출력은 없습니다. 성공 값을 관찰하려면 System.out.println(service.summary("java"))처럼 반환값을 출력하는 호출이 따로 필요합니다. 값을 반환하는 것과 값을 화면에 출력하는 것을 구별합니다.

catch 후 같은 추상화의 예외를 메시지만 바꿔 다시 던지는 것은 가치가 적지만, 계층 경계를 넘으며 안정된 타입과 의미를 제공한다면 변환이 결합을 줄입니다.

서비스 예외의 메시지와 보존한 원인은 별개다

UncheckedExceptionTranslation은 StoreAccessException을 잡아 BoardSystemException으로 바꾸고 cause를 보존한다. main은 새 메시지와 원인의 타입을 각각 출력한다.

예외 객체 · 메시지와 cause

서비스 예외의 메시지와 보존한 원인은 별개다

서비스가 저장소 실패를 한 번 변환합니다. main이 잡는 서비스 예외와 cause로 연결된 원인은 서로 다른 객체입니다.

새 서비스 예외와 원래 cause 객체의 관계 새 BoardSystemException과 원래 StoreAccessException은 서로 다른 객체입니다. 점선은 새 객체에서 원래 객체로 이어지는 cause 참조입니다. 실선은 main이 읽어서 출력한 대응이며 public 메시지 뒤 root 타입을 출력합니다. 모바일에서 출력 필드가 나뉘어도 원문에서는 public 한 줄과 root 한 줄이며 원인의 전체 메시지나 스택을 출력하지 않습니다. main이 잡은 새 객체 BoardSystemException board data unavailable cause의 원래 객체 StoreAccessException disk read failed: java 첫 출력의 메시지 public=board data unavailable 둘째 출력의 타입 root=StoreAccessException 점선: 새 객체의 cause 참조 · 실선: main이 읽어 출력한 값 · 원인의 메시지·스택 전체 출력 아님
새 서비스 예외와 원래 cause 객체의 관계 새 BoardSystemException과 원래 StoreAccessException은 서로 다른 객체입니다. 점선은 새 객체에서 원래 객체로 이어지는 cause 참조입니다. 실선은 main이 읽어서 출력한 대응이며 public 메시지 뒤 root 타입을 출력합니다. 모바일에서 출력 필드가 나뉘어도 원문에서는 public 한 줄과 root 한 줄이며 원인의 전체 메시지나 스택을 출력하지 않습니다. main이 잡은 새 객체 BoardSystemException board data unavailable cause의 원래 객체 StoreAccessException disk read failed: java 첫 출력의 메시지 public= board data unavailable 둘째 출력의 타입 root= StoreAccessException 점선: 새 객체의 cause 참조 실선: main이 출력한 값 원인 메시지 전체 출력은 아님

main이 잡은 서비스 예외

타입
BoardSystemException
각 객체의 메시지
board data unavailable
main의 출력
public=board data unavailable

cause로 보존한 저장소 예외

타입
StoreAccessException
각 객체의 메시지
disk read failed: java
main의 출력
root=StoreAccessException

서비스 예외 생성자가 super(message, cause)로 원인을 전달합니다. getMessage()는 새 문맥을, getCause()는 보존한 원인 객체를 줍니다. 원문은 원인의 전체 메시지나 스택을 출력한 예제가 아닙니다.


예외 전파의 명시적 결정

중간 서비스가 오류를 복구할 방법이 없다면 catch하지 않고 전달합니다.

로그는 최종 처리 경계에서 한 번 남기는 것이 보통 낫습니다.

모든 계층이 같은 예외를 기록하면 한 장애가 여러 건처럼 보이고, 스택 추적이 반복되어 원인 파악이 어려워집니다.

다만 선택 기능만 포기하면 핵심 흐름을 계속할 수 있는 경우에는 지역 복구가 가능합니다.

추천 게시글 제목을 불러오지 못해도 사용자가 직접 입력할 수 있다면 추천 예외만 잡고 빈 추천 목록을 보여 줄 수 있습니다.

DB 저장 실패를 잡아 성공으로 위장해서는 안 됩니다.

복구 뒤 시스템 불변식이 유지되는지 확인해야 합니다.


예외 타입 선택 기준

  1. 호출자가 이 실패를 발견하면 즉시 다른 선택으로 성공을 만들 수 있는가?
  2. 거의 모든 중간 계층이 같은 throws를 전달만 하게 되는가?
  3. 이 실패가 외부 환경 문제인가, 호출자의 계약 위반인가?
  4. 공개 API가 구현체의 구체 예외에 종속되는가?
  5. 경계 처리기가 사용자 응답과 운영 로그를 만들 수 있는가?

첫 질문의 답이 명확히 '예'이고 호출자별 복구가 다르면 체크 예외나 결과 타입을 검토합니다.

두 번째 질문의 답이 '예'라면 언체크 변환과 공통 경계 처리가 더 단순할 수 있습니다.

입력 계약 위반은 IllegalArgumentException 계열, 객체 상태 위반은 IllegalStateException 계열이 기본 후보입니다.

구체 도메인 의미가 필요할 때만 사용자 정의 타입을 추가합니다.


연습 문제

추천 저장소는 RecommendationUnavailableException을 던질 수 있습니다.

추천 실패는 빈 목록으로 복구하되 게시글 저장 실패는 상위로 계속 전달하도록 서비스 코드를 작성하세요.

loadRecommendations는 RecommendationUnavailableException("model offline")을 던지고, recommendations의 반환 타입은 List<String>입니다. save(title)는 BoardWriteFailure("store offline: " + title)를 던집니다. 두 예외는 RuntimeException의 자식이며 title은 null이 아닌 것으로 정합니다.

main은 recommendations 결과를 먼저 recommendations=목록 형식으로 출력한 뒤 save("exception")을 호출합니다. 저장 실패는 main의 BoardWriteFailure catch에서 save-failed=메시지로 출력합니다. 서비스 안에서는 추천 예외만 잡고 저장 예외를 잡아 성공으로 바꾸지 않습니다.

독립 입력으로 저장 제목만 "draft"로 바꾸면 추천 결과와 저장 실패 메시지가 각각 어떻게 되는지 예상하세요.

해설 보기
src/SelectiveUncheckedRecoveryExercise.java
import java.util.List;

public final class SelectiveUncheckedRecoveryExercise {
    public static void main(String[] args) {
        BoardService service = new BoardService();
        System.out.println("recommendations=" + service.recommendations());
        try {
            service.save("exception");
        } catch (BoardWriteFailure error) {
            System.out.println("save-failed=" + error.getMessage());
        }
    }

    private static final class BoardService {
        List<String> recommendations() {
            try {
                return loadRecommendations();
            } catch (RecommendationUnavailableException error) {
                return List.of();
            }
        }

        void save(String title) {
            throw new BoardWriteFailure("store offline: " + title);
        }

        private List<String> loadRecommendations() {
            throw new RecommendationUnavailableException("model offline");
        }
    }

    private static final class RecommendationUnavailableException extends RuntimeException {
        RecommendationUnavailableException(String message) { super(message); }
    }

    private static final class BoardWriteFailure extends RuntimeException {
        BoardWriteFailure(String message) { super(message); }
    }
}
recommendations=[]
save-failed=store offline: exception

추천은 부가 기능이라 빈 값으로도 핵심 사용이 가능하지만 저장은 성공을 만들 수 없어 경계로 전달합니다.

체크 여부보다 복구 후 의미가 보존되는지가 더 중요한 판단입니다.

독립 입력에서는 recommendations=[]는 그대로이고 save-failed=store offline: draft가 됩니다. save는 어떤 title에도 실패를 던지는 이 테스트 구현이므로 제목을 바꿨다는 이유로 저장 성공을 예상하지 않습니다.

추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다

같은 main에서 추천 실패를 빈 목록으로 복구한 뒤 저장 실패를 경계로 전달합니다. 실제 recommendations 출력→save 시도→catch 출력의 시간 순서가 핵심입니다.

추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다

SelectiveUncheckedRecoveryExercise는 추천 예외를 서비스에서 빈 목록으로 바꾼다. 저장 실패는 서비스에서 복구하지 않고 main이 잡아 실패를 출력한다.

지역 복구 · 기능별 책임

추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다

추천 실패는 빈 목록으로 복구합니다. 저장 실패는 성공 값을 만들 수 없어 main의 경계로 전달합니다.

지역 복구 뒤 필수 저장을 시도하는 main 같은 main의 실제 순서입니다. 추천 서비스는 RecommendationUnavailableException을 잡아 빈 목록을 반환하고 main이 먼저 recommendations=[]를 출력합니다. 이어 저장은 BoardWriteFailure를 던져 main의 경계 catch로 전달됩니다. 실선은 이 순서이며 save-failed=store offline: exception은 원문 한 줄의 출력입니다. 실제 재시도나 저장 성공을 뜻하지 않습니다. 추천 서비스의 지역 복구 저장 서비스의 전달 추천 읽기 실패 RecommendationUnavailableException 서비스 catch List.of()로 반환 저장 성공의 뜻 아님 다음 save 호출 title="exception" 저장 실패 던짐 BoardWriteFailure 서비스에서 복구 안 함 main의 첫 출력 recommendations=[] main의 경계 catch save-failed=store offline: exception 실선은 같은 main의 순서 · 추천 빈 목록 뒤 저장 시도 · 실제 재시도·저장 성공 없음
지역 복구 뒤 필수 저장을 시도하는 main 같은 main의 실제 순서입니다. 추천 서비스는 RecommendationUnavailableException을 잡아 빈 목록을 반환하고 main이 먼저 recommendations=[]를 출력합니다. 이어 저장은 BoardWriteFailure를 던져 main의 경계 catch로 전달됩니다. 실선은 이 순서이며 save-failed=store offline: exception은 원문 한 줄의 출력입니다. 실제 재시도나 저장 성공을 뜻하지 않습니다. 추천 서비스의 지역 복구 추천 읽기 실패 Recommendation UnavailableException 서비스 catch List.of()로 반환 저장 성공의 뜻 아님 main의 첫 출력 recommendations=[] 저장 서비스의 전달 다음 save 호출 title="exception" 저장 실패 던짐 BoardWriteFailure 서비스에서 복구 안 함 main의 경계 catch save-failed= store offline: exception 실선은 같은 main의 순서 추천 빈 목록 뒤 저장을 시도 실제 재시도·저장 성공은 없음

recommendations()

서비스의 실제 처리
RecommendationUnavailableException을 잡아 List.of()를 반환합니다.
main이 관찰하는 결과
recommendations=[]

save("exception")

서비스의 실제 처리
BoardWriteFailure를 던집니다.
서비스에는 이를 복구하는 catch가 없습니다.
main이 관찰하는 결과
save-failed=store offline: exception

main은 추천 결과를 먼저 출력한 뒤 저장을 시도합니다. 두 번째 줄은 저장 성공이 아니라 경계에서 잡은 실패입니다. 이 예제에는 실제 재시도나 저장 성공 경로가 실행되지 않습니다.

이 예의 조건

  • 동일 main의 실제 순서이며 저장 성공/재시도를 수행하지 않는다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다 추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다 SelectiveUncheckedRecoveryExercise는 추천 예외를 서비스에서 빈 목록으로 바꾼다. 저장 실패는 서비스에서 복구하지 않고 main이 잡아 실패를 출력한다. 지역 복구 · 기능별 책임 추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다 추천 실패는 빈 목록으로 복구합니다. 저장 실패는 성공 값을 만들 수 없어 main의 경계로 전달합니다. recommendations() 서비스의 실제 처리 RecommendationUnavailableException 을 잡아 List.of() 를 반환합니다. main이 관찰하는 결과 recommendations=[] save("exception") 서비스의 실제 처리 BoardWriteFailure 를 던집니다. 서비스에는 이를 복구하는 catch가 없습니다. main이 관찰하는 결과 save-failed=store offline: exception main 은 추천 결과를 먼저 출력한 뒤 저장을 시도합니다. 두 번째 줄은 저장 성공이 아니라 경계에서 잡은 실패입니다. 이 예제에는 실제 재시도나 저장 성공 경로가 실행되지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
추천 실패 복구

추천 서비스가 오류를 빈 목록으로 반환합니다.

  • 예외: RecommendationUnavailableException
  • 반환: List.of()
  • 선택 기능의 실패를 서비스 내부에서 복구한다.
main 다음 호출

recommendations=[] 출력 뒤 필수 save를 시도합니다.

  • 첫 출력: recommendations=[]
  • 저장 제목: exception
  • 추천 복구가 저장 성공을 보장하지 않는다.
저장 실패 경계

BoardWriteFailure를 main이 잡습니다.

  • 예외: BoardWriteFailure
  • 출력: save-failed=store offline: exception
  • 필수 작업 실패는 빈 추천값으로 감추지 않고 전달한다.

추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다

같은 main에서 추천 실패를 빈 목록으로 복구한 뒤 저장 실패를 경계로 전달합니다. 실제 recommendations 출력→save 시도→catch 출력의 시간 순서가 핵심입니다.

추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다

SelectiveUncheckedRecoveryExercise는 추천 예외를 서비스에서 빈 목록으로 바꾼다. 저장 실패는 서비스에서 복구하지 않고 main이 잡아 실패를 출력한다.

지역 복구 · 기능별 책임

추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다

추천 실패는 빈 목록으로 복구합니다. 저장 실패는 성공 값을 만들 수 없어 main의 경계로 전달합니다.

지역 복구 뒤 필수 저장을 시도하는 main 같은 main의 실제 순서입니다. 추천 서비스는 RecommendationUnavailableException을 잡아 빈 목록을 반환하고 main이 먼저 recommendations=[]를 출력합니다. 이어 저장은 BoardWriteFailure를 던져 main의 경계 catch로 전달됩니다. 실선은 이 순서이며 save-failed=store offline: exception은 원문 한 줄의 출력입니다. 실제 재시도나 저장 성공을 뜻하지 않습니다. 추천 서비스의 지역 복구 저장 서비스의 전달 추천 읽기 실패 RecommendationUnavailableException 서비스 catch List.of()로 반환 저장 성공의 뜻 아님 다음 save 호출 title="exception" 저장 실패 던짐 BoardWriteFailure 서비스에서 복구 안 함 main의 첫 출력 recommendations=[] main의 경계 catch save-failed=store offline: exception 실선은 같은 main의 순서 · 추천 빈 목록 뒤 저장 시도 · 실제 재시도·저장 성공 없음
지역 복구 뒤 필수 저장을 시도하는 main 같은 main의 실제 순서입니다. 추천 서비스는 RecommendationUnavailableException을 잡아 빈 목록을 반환하고 main이 먼저 recommendations=[]를 출력합니다. 이어 저장은 BoardWriteFailure를 던져 main의 경계 catch로 전달됩니다. 실선은 이 순서이며 save-failed=store offline: exception은 원문 한 줄의 출력입니다. 실제 재시도나 저장 성공을 뜻하지 않습니다. 추천 서비스의 지역 복구 추천 읽기 실패 Recommendation UnavailableException 서비스 catch List.of()로 반환 저장 성공의 뜻 아님 main의 첫 출력 recommendations=[] 저장 서비스의 전달 다음 save 호출 title="exception" 저장 실패 던짐 BoardWriteFailure 서비스에서 복구 안 함 main의 경계 catch save-failed= store offline: exception 실선은 같은 main의 순서 추천 빈 목록 뒤 저장을 시도 실제 재시도·저장 성공은 없음

recommendations()

서비스의 실제 처리
RecommendationUnavailableException을 잡아 List.of()를 반환합니다.
main이 관찰하는 결과
recommendations=[]

save("exception")

서비스의 실제 처리
BoardWriteFailure를 던집니다.
서비스에는 이를 복구하는 catch가 없습니다.
main이 관찰하는 결과
save-failed=store offline: exception

main은 추천 결과를 먼저 출력한 뒤 저장을 시도합니다. 두 번째 줄은 저장 성공이 아니라 경계에서 잡은 실패입니다. 이 예제에는 실제 재시도나 저장 성공 경로가 실행되지 않습니다.

이 예의 조건

  • 동일 main의 실제 순서이며 저장 성공/재시도를 수행하지 않는다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다 추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다 SelectiveUncheckedRecoveryExercise는 추천 예외를 서비스에서 빈 목록으로 바꾼다. 저장 실패는 서비스에서 복구하지 않고 main이 잡아 실패를 출력한다. 지역 복구 · 기능별 책임 추천의 빈 결과와 저장 실패를 같은 성공으로 보지 않는다 추천 실패는 빈 목록으로 복구합니다. 저장 실패는 성공 값을 만들 수 없어 main의 경계로 전달합니다. recommendations() 서비스의 실제 처리 RecommendationUnavailableException 을 잡아 List.of() 를 반환합니다. main이 관찰하는 결과 recommendations=[] save("exception") 서비스의 실제 처리 BoardWriteFailure 를 던집니다. 서비스에는 이를 복구하는 catch가 없습니다. main이 관찰하는 결과 save-failed=store offline: exception main 은 추천 결과를 먼저 출력한 뒤 저장을 시도합니다. 두 번째 줄은 저장 성공이 아니라 경계에서 잡은 실패입니다. 이 예제에는 실제 재시도나 저장 성공 경로가 실행되지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
추천 실패 복구

추천 서비스가 오류를 빈 목록으로 반환합니다.

  • 예외: RecommendationUnavailableException
  • 반환: List.of()
  • 선택 기능의 실패를 서비스 내부에서 복구한다.
main 다음 호출

recommendations=[] 출력 뒤 필수 save를 시도합니다.

  • 첫 출력: recommendations=[]
  • 저장 제목: exception
  • 추천 복구가 저장 성공을 보장하지 않는다.
저장 실패 경계

BoardWriteFailure를 main이 잡습니다.

  • 예외: BoardWriteFailure
  • 출력: save-failed=store offline: exception
  • 필수 작업 실패는 빈 추천값으로 감추지 않고 전달한다.