예외 계층과 검사 예외
Throwable 계층을 복구 가능성으로 읽고 처리하지 않은 체크 예외의 컴파일 실패를 재현한 뒤 catch와 throws의 책임 차이를 설계합니다.
자바의 예외는 제어 키워드이기 전에 객체입니다.
메시지와 원인을 가진 객체가 타입 계층을 따라 전달되므로 catch가 어느 범위를 처리할지, throws가 어느 범위를 호출자에게 넘길지 다형성 규칙으로 결정됩니다.
먼저 아무 처리도 하지 않은 체크 예외가 왜 컴파일을 멈추는지 관찰합니다.
검사 예외의 처리 의무
Exception을 직접 상속한 BoardLoadException은 체크 예외입니다.
load()가 이 예외를 던질 수 있다고 선언했는데 main은 catch도 throws도 사용하지 않았습니다.
public final class UnhandledCheckedBoardException {
public static void main(String[] args) {
PostRepository repository = new PostRepository();
System.out.println(repository.load("missing"));
}
private static final class PostRepository {
String load(String id) throws BoardLoadException {
throw new BoardLoadException("not found: " + id);
}
}
private static final class BoardLoadException extends Exception {
BoardLoadException(String message) { super(message); }
}
}error: unreported exception BoardLoadException;
must be caught or declared to be thrown이 실패는 실행 중 우연히 발생한 것이 아니라 호출 계약을 지키지 않은 소스 오류입니다.
체크 예외를 발생시키는 메서드를 호출하면 현재 메서드가 복구하거나 자신의 계약에 throws를 추가해야 합니다.
컴파일러가 복구 방식을 정해 주지는 않지만, 책임 결정을 생략하는 것은 막습니다.
Throwable 예외 계층
Throwable 아래에는 크게 Error와 Exception이 있습니다.
Error는 JVM이나 실행 환경이 지속하기 어려운 심각한 문제를 표현하므로 일반 업무 코드가 광범위하게 잡아 계속 진행하는 대상이 아닙니다.
Exception 계열이 애플리케이션 흐름에서 주로 다루는 예외입니다.
Exception 가운데 RuntimeException과 그 하위는 언체크입니다.
그 밖의 Exception 하위는 체크입니다.
Error와 그 하위도 컴파일러의 catch/throws 의무가 없는 언체크 타입입니다. 따라서 "언체크"를 RuntimeException만의 이름으로 기억하지 않습니다. JLS의 예외 분류는 상속에 따른 분류를 정하며, catch(Exception)은 별도 가지인 Error를 포함하지 않습니다.
어느 쪽이 더 심각한지를 나타내는 서열은 아닙니다.
컴파일러가 catch/throws 결정을 강제하는지의 차이입니다.
부모 타입으로 잡으면 자식 예외까지 포함하므로 catch (Exception)은 생각보다 넓은 실패를 가립니다.
public final class ExceptionHierarchyObservation {
public static void main(String[] args) {
inspect(new BoardConnectionException("offline"));
inspect(new IllegalArgumentException("viewCount"));
}
private static void inspect(Exception exception) {
System.out.println(exception.getClass().getSimpleName()
+ ":checked=" + !(exception instanceof RuntimeException));
}
private static final class BoardConnectionException extends Exception {
BoardConnectionException(String message) { super(message); }
}
}BoardConnectionException:checked=true
IllegalArgumentException:checked=false분류는 상속 관계로 정해집니다.
이름에 Runtime을 넣거나 문서에서 “치명적”이라고 적는다고 바뀌지 않습니다.
사용자 정의 예외의 부모를 고르는 순간 호출자에게 부과할 컴파일 계약도 함께 선택합니다.
구체 예외의 지역 복구
파일에서 조회 수를 읽는 저장소가 일시적으로 실패했을 때 메모리 기본값을 쓸 수 있다고 가정합니다.
이 서비스에는 실제 대안이 있으므로 예외를 잡아 결과로 복구하는 것이 책임에 맞습니다.
public final class CheckedExceptionRecovery {
public static void main(String[] args) {
BoardService service = new BoardService(new FailingRepository());
System.out.println("viewCount=" + service.viewCountFor("java"));
}
private static final class BoardService {
private final PostRepository repository;
BoardService(PostRepository repository) { this.repository = repository; }
int viewCountFor(String title) {
try {
return repository.loadViewCount(title);
} catch (BoardReadException error) {
System.out.println("fallback=" + error.getMessage());
return 0;
}
}
}
private interface PostRepository {
int loadViewCount(String title) throws BoardReadException;
}
private static final class FailingRepository implements PostRepository {
public int loadViewCount(String title) throws BoardReadException {
throw new BoardReadException("store unavailable: " + title);
}
}
private static final class BoardReadException extends Exception {
BoardReadException(String message) { super(message); }
}
}fallback=store unavailable: java
viewCount=0이 예제의 catch는 0을 반환해 viewCountFor를 끝내고, main은 반환받은 값으로 viewCount=0을 출력합니다.
따라서 0이 정말 안전한 기본값인지가 중요합니다.
장애를 실제 조회 수 0과 혼동하면 조용한 데이터 오류가 됩니다.
기본값이 의미를 왜곡한다면 복구하지 말고 상위 경계로 전달해야 합니다.
결과 값으로 "조회 성공"과 "아직 알 수 없음"을 구별하는 방법도 있습니다. 다음 별도 예제는 실제 조회 수가 0인 경우를 실패와 분리합니다. 실패 때 count 필드는 읽지 않고 available부터 검사합니다.
예시
public final class CheckedLookupResult {
public static void main(String[] args) {
print("empty");
print("offline");
}
private static void print(String scenario) {
Lookup result = countFor(scenario);
System.out.println(scenario + "=" + (result.available()
? "count=" + result.count() : "unavailable"));
}
private static Lookup countFor(String scenario) {
try {
if (scenario.equals("offline")) {
throw new BoardReadException("store unavailable");
}
return new Lookup(true, 0);
} catch (BoardReadException error) {
return new Lookup(false, 0);
}
}
private record Lookup(boolean available, int count) { }
private static final class BoardReadException extends Exception {
BoardReadException(String message) { super(message); }
}
}empty=count=0
offline=unavailableavailable=false일 때 넣은 0은 실제 조회 수가 아닙니다. UI나 호출자는 이 경우를 확인 불가로 다뤄야 합니다. 이 선택 역시 업무상 가능한 대안을 값으로 만든 것이며, 원래 CheckedExceptionRecovery가 반환하는 int를 몰래 바꾼 코드는 아닙니다.
throws를 통한 책임 전달
이 예제의 서비스는 persist가 던진 입력 검증 오류에 저장 문맥을 더하고, CLI 경계는 안내 메시지를 출력합니다.
서비스는 IllegalArgumentException을 BoardWriteException으로 변환해 cause와 함께 전달하고, main이 변환된 예외를 잡습니다.
throw는 예외 객체를 발생시키는 문장이고, throws는 메서드가 밖으로 보낼 수 있는 타입을 선언합니다.
public final class CheckedExceptionPropagation {
public static void main(String[] args) {
BoardService service = new BoardService();
try {
service.save("exception", -10);
} catch (BoardWriteException error) {
System.out.println("retry-guide=" + error.getMessage());
System.out.println("cause=" + error.getCause().getClass().getSimpleName());
}
}
private static final class BoardService {
void save(String title, int viewCount) throws BoardWriteException {
try {
persist(title, viewCount);
} catch (IllegalArgumentException cause) {
throw new BoardWriteException("could not save " + title, cause);
}
}
private void persist(String title, int viewCount) {
if (viewCount < 0) throw new IllegalArgumentException("negative viewCount");
}
}
private static final class BoardWriteException extends Exception {
BoardWriteException(String message, Throwable cause) { super(message, cause); }
}
}retry-guide=could not save exception
cause=IllegalArgumentException원인을 cause로 연결하면 새 문맥을 추가하면서도 최초 실패를 잃지 않습니다.
문자열에 원인 메시지만 붙이면 스택 위치와 구체 타입이 사라집니다.
예외 변환은 다음 계층이 이해할 수 있는 추상화로 바꿀 때 사용하고, 단순히 throws를 감추려는 목적으로 모든 예외를 새로 포장하지 않습니다.
catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환
별도 복구 예제는 catch에서 0 반환, 변환 예제는 cause를 보존해 새 예외를 전달합니다. 각 독립 실행의 실패→처리→호출자 결과를 단계로 대조하며 반환과 예외 전달의 차이를 확인할 수 있습니다.
CheckedExceptionRecovery는 BoardReadException을 잡고 0을 반환한다. 별도 CheckedExceptionPropagation은 IllegalArgumentException을 원인으로 가진 BoardWriteException을 던져 main이 잡는다.
catch 책임 · 반환과 변환
catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환
복구와 변환은 별도 프로그램입니다. 한 서비스는 0을 반환하고, 다른 서비스는 원인을 연결한 체크 예외를 던집니다.
기본값으로 복구하는 원문
- 실패 발생
loadViewCount가BoardReadException을 던집니다.메시지: store unavailable: java- 서비스의 catch
- 실패 메시지를 출력하고
return 0으로 메서드에서 반환합니다. - main이 관찰
fallback=store unavailable: java viewCount=0
새 문맥으로 변환하는 원문
- 실패 발생
persist가 음수-10을 거부해IllegalArgumentException을 던집니다.- 서비스의 catch
BoardWriteException을 새로 던집니다.기존 예외는cause로 연결합니다.- main이 관찰
retry-guide=could not save exception cause=IllegalArgumentException
복구 원문은 catch 블록 뒤의 문장으로 진행하는 것이 아니라 호출자에게 0을 돌려줍니다. 변환 원문의 throws BoardWriteException은 전달 가능한 타입의 선언이며, 실제 새 예외는 throw가 발생시킵니다.
이 예의 조건
- 복구와 변환은 독립 프로그램이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환 catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환 CheckedExceptionRecovery는 BoardReadException을 잡고 0을 반환한다. 별도 CheckedExceptionPropagation은 IllegalArgumentException을 원인으로 가진 BoardWriteException을 던져 main이 잡는다. catch 책임 · 반환과 변환 catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환 복구와 변환은 별도 프로그램입니다. 한 서비스는 0을 반환하고, 다른 서비스는 원인을 연결한 체크 예외를 던집니다. 기본값으로 복구하는 원문 실패 발생 loadViewCount 가 BoardReadException 을 던집니다. 메시지: store unavailable: java 서비스의 catch 실패 메시지를 출력하고 return 0 으로 메서드에서 반환합니다. main이 관찰 fallback=store unavailable: java viewCount=0 새 문맥으로 변환하는 원문 실패 발생 persist 가 음수 -10 을 거부해 IllegalArgumentException 을 던집니다. 서비스의 catch BoardWriteException 을 새로 던집니다. 기존 예외는 cause 로 연결합니다. main이 관찰 retry-guide=could not save exception cause=IllegalArgumentException 복구 원문은 catch 블록 뒤의 문장으로 진행하는 것이 아니라 호출자에게 0 을 돌려줍니다. 변환 원문의 throws BoardWriteException 은 전달 가능한 타입의 선언이며, 실제 새 예외는 throw 가 발생시킵니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 원래 실패 읽기와 음수 저장이 각기 실패합니다. |
|
|
| 서비스 처리 읽기는 0으로 복구하고 저장은 원인을 새 예외에 보존합니다. |
|
|
| 호출자 결과 main이 값을 읽거나 경계 catch로 안내합니다. |
|
|
catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환
별도 복구 예제는 catch에서 0 반환, 변환 예제는 cause를 보존해 새 예외를 전달합니다. 각 독립 실행의 실패→처리→호출자 결과를 단계로 대조하며 반환과 예외 전달의 차이를 확인할 수 있습니다.
CheckedExceptionRecovery는 BoardReadException을 잡고 0을 반환한다. 별도 CheckedExceptionPropagation은 IllegalArgumentException을 원인으로 가진 BoardWriteException을 던져 main이 잡는다.
catch 책임 · 반환과 변환
catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환
복구와 변환은 별도 프로그램입니다. 한 서비스는 0을 반환하고, 다른 서비스는 원인을 연결한 체크 예외를 던집니다.
기본값으로 복구하는 원문
- 실패 발생
loadViewCount가BoardReadException을 던집니다.메시지: store unavailable: java- 서비스의 catch
- 실패 메시지를 출력하고
return 0으로 메서드에서 반환합니다. - main이 관찰
fallback=store unavailable: java viewCount=0
새 문맥으로 변환하는 원문
- 실패 발생
persist가 음수-10을 거부해IllegalArgumentException을 던집니다.- 서비스의 catch
BoardWriteException을 새로 던집니다.기존 예외는cause로 연결합니다.- main이 관찰
retry-guide=could not save exception cause=IllegalArgumentException
복구 원문은 catch 블록 뒤의 문장으로 진행하는 것이 아니라 호출자에게 0을 돌려줍니다. 변환 원문의 throws BoardWriteException은 전달 가능한 타입의 선언이며, 실제 새 예외는 throw가 발생시킵니다.
이 예의 조건
- 복구와 변환은 독립 프로그램이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환 catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환 CheckedExceptionRecovery는 BoardReadException을 잡고 0을 반환한다. 별도 CheckedExceptionPropagation은 IllegalArgumentException을 원인으로 가진 BoardWriteException을 던져 main이 잡는다. catch 책임 · 반환과 변환 catch에서 값을 반환하는 복구와 새 예외로 전달하는 변환 복구와 변환은 별도 프로그램입니다. 한 서비스는 0을 반환하고, 다른 서비스는 원인을 연결한 체크 예외를 던집니다. 기본값으로 복구하는 원문 실패 발생 loadViewCount 가 BoardReadException 을 던집니다. 메시지: store unavailable: java 서비스의 catch 실패 메시지를 출력하고 return 0 으로 메서드에서 반환합니다. main이 관찰 fallback=store unavailable: java viewCount=0 새 문맥으로 변환하는 원문 실패 발생 persist 가 음수 -10 을 거부해 IllegalArgumentException 을 던집니다. 서비스의 catch BoardWriteException 을 새로 던집니다. 기존 예외는 cause 로 연결합니다. main이 관찰 retry-guide=could not save exception cause=IllegalArgumentException 복구 원문은 catch 블록 뒤의 문장으로 진행하는 것이 아니라 호출자에게 0 을 돌려줍니다. 변환 원문의 throws BoardWriteException 은 전달 가능한 타입의 선언이며, 실제 새 예외는 throw 가 발생시킵니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 원래 실패 읽기와 음수 저장이 각기 실패합니다. |
|
|
| 서비스 처리 읽기는 0으로 복구하고 저장은 원인을 새 예외에 보존합니다. |
|
|
| 호출자 결과 main이 값을 읽거나 경계 catch로 안내합니다. |
|
|
catch와 throws의 선택
현재 메서드가 성공에 준하는 대안을 실제로 만들 수 있는지 묻습니다.
재시도 횟수와 지연 정책을 알고 있거나, 사용자가 수정 가능한 입력 오류로 변환하거나, 선택 기능만 포기해도 핵심 작업을 계속할 수 있으면 catch가 후보입니다.
그저 로그 한 줄을 남기고 같은 예외를 다시 던지는 중간 계층은 책임이 중복될 수 있습니다.
catch 순서는 구체 타입에서 넓은 타입으로 배치합니다.
부모를 먼저 잡으면 자식 catch에는 도달할 수 없어 컴파일되지 않습니다.
다중 catch는 처리 방식이 완전히 같은 형제 타입에 사용합니다.
Error까지 포함하는 Throwable catch는 종료 훅이나 프레임워크 경계처럼 매우 제한된 곳이 아니면 피합니다.
throws에는 실제 호출자가 알아야 할 안정적인 예외 계약을 적습니다.
구현 세부 예외가 공개 API 밖으로 그대로 새면 저장소 교체가 호출자 변경으로 이어집니다.
반대로 throws Exception처럼 넓게 선언하면 구체적인 체크 예외가 늘어도 이미 선언한 범위에 포함되어 세부 계약 변화가 잘 드러나지 않습니다. 호출자의 catch 또는 throws 의무 자체가 없어지는 것은 아닙니다.
연습 문제
BoardAccessException 아래의 BoardNotFoundException과 BoardPermissionException을 만들고, 서비스는 부모 타입만 throws 하세요.
CLI에서는 not-found는 새 기록 생성 안내, permission은 권한 안내로 구분해 출력합니다.
이번 서비스 입력은 null이 아닌 id입니다. missing이면 BoardNotFoundException, private이면 BoardPermissionException을 발생시키고, 그 밖의 id는 opened=id를 출력합니다. 두 자식 예외는 id를 보관하고 id()로 제공합니다. 부모 BoardAccessException은 Exception을 상속하는 체크 예외입니다.
CLI의 안내 형식은 각각 create-new=id와 request-access=id입니다. 부모 catch는 unexpected-access=메시지를 출력하는 안전망으로 두되, 이미 앞의 자식 catch에서 처리한 예외를 다시 처리하지 않습니다.
독립 입력 "public"의 출력과 선택되는 catch가 있는지 먼저 예상하세요.
해설 보기
public final class CheckedHierarchyExercise {
public static void main(String[] args) {
open("missing");
open("private");
}
private static void open(String id) {
BoardService service = new BoardService();
try {
service.open(id);
} catch (BoardNotFoundException error) {
System.out.println("create-new=" + error.id());
} catch (BoardPermissionException error) {
System.out.println("request-access=" + error.id());
} catch (BoardAccessException error) {
System.out.println("unexpected-access=" + error.getMessage());
}
}
private static final class BoardService {
void open(String id) throws BoardAccessException {
if (id.equals("missing")) throw new BoardNotFoundException(id);
if (id.equals("private")) throw new BoardPermissionException(id);
System.out.println("opened=" + id);
}
}
private static class BoardAccessException extends Exception {
BoardAccessException(String message) { super(message); }
}
private static final class BoardNotFoundException extends BoardAccessException {
private final String id;
BoardNotFoundException(String id) { super("missing " + id); this.id = id; }
String id() { return id; }
}
private static final class BoardPermissionException extends BoardAccessException {
private final String id;
BoardPermissionException(String id) { super("denied " + id); this.id = id; }
String id() { return id; }
}
}create-new=missing
request-access=private서비스 계약은 부모 타입으로 안정적으로 유지하면서 경계는 실제 자식 타입의 정보로 다른 복구를 수행합니다.
마지막 부모 catch는 향후 새 하위 예외가 추가됐을 때의 안전망입니다.
public 입력은 서비스가 opened=public을 출력하고 정상 반환하므로 어느 catch도 실행하지 않습니다. throws BoardAccessException을 선언했다는 사실이 매 호출에서 그 예외를 실제로 던진다는 뜻은 아닙니다.
부모 throws 계약에서도 실제 자식 타입으로 복구한다
서비스의 부모 throws 계약 아래 실제 자식 예외가 한 구체 catch와 출력으로 이어집니다. 발생→catch 선택→한 번의 결과 관찰을 강조하면 뒤 catch 미실행을 설명하기 좋습니다.
CheckedHierarchyExercise의 서비스는 BoardAccessException을 선언한다. missing과 private은 서로 다른 자식 예외가 발생해 CLI의 서로 다른 catch로 들어간다.
예외 계층 · 실제 catch 선택
부모 throws 계약에서도 실제 자식 타입으로 복구한다
서비스는 부모 BoardAccessException을 선언합니다. CLI는 실제 자식 타입과 일치하는 catch 하나를 선택합니다.
missing
- 발생 타입과 선택된 catch
BoardNotFoundException첫 번째 구체 catch- 원문 출력
create-new=missing
private
- 발생 타입과 선택된 catch
BoardPermissionException두 번째 구체 catch- 원문 출력
request-access=private
마지막 BoardAccessException catch에는 이 두 입력이 들어가지 않습니다. 앞에서 일치한 catch 하나만 실행합니다. 부모 catch를 먼저 두어 자식 catch를 가리는 순서는 컴파일되지 않습니다.
이 예의 조건
- 각 입력은 한 구체 catch만 실행한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 부모 throws 계약에서도 실제 자식 타입으로 복구한다 부모 throws 계약에서도 실제 자식 타입으로 복구한다 CheckedHierarchyExercise의 서비스는 BoardAccessException을 선언한다. missing과 private은 서로 다른 자식 예외가 발생해 CLI의 서로 다른 catch로 들어간다. 예외 계층 · 실제 catch 선택 부모 throws 계약에서도 실제 자식 타입으로 복구한다 서비스는 부모 BoardAccessException을 선언합니다. CLI는 실제 자식 타입과 일치하는 catch 하나를 선택합니다. missing 발생 타입과 선택된 catch BoardNotFoundException 첫 번째 구체 catch 원문 출력 create-new=missing private 발생 타입과 선택된 catch BoardPermissionException 두 번째 구체 catch 원문 출력 request-access=private 마지막 BoardAccessException catch에는 이 두 입력이 들어가지 않습니다. 앞에서 일치한 catch 하나만 실행합니다. 부모 catch를 먼저 두어 자식 catch를 가리는 순서는 컴파일되지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 서비스 계약 부모 BoardAccessException을 throws로 선언합니다. |
|
|
| 실제 예외 입력에 맞는 자식 타입이 발생합니다. |
|
|
| 단일 처리 해당 구체 catch가 한 줄을 출력합니다. |
|
|
부모 throws 계약에서도 실제 자식 타입으로 복구한다
서비스의 부모 throws 계약 아래 실제 자식 예외가 한 구체 catch와 출력으로 이어집니다. 발생→catch 선택→한 번의 결과 관찰을 강조하면 뒤 catch 미실행을 설명하기 좋습니다.
CheckedHierarchyExercise의 서비스는 BoardAccessException을 선언한다. missing과 private은 서로 다른 자식 예외가 발생해 CLI의 서로 다른 catch로 들어간다.
예외 계층 · 실제 catch 선택
부모 throws 계약에서도 실제 자식 타입으로 복구한다
서비스는 부모 BoardAccessException을 선언합니다. CLI는 실제 자식 타입과 일치하는 catch 하나를 선택합니다.
missing
- 발생 타입과 선택된 catch
BoardNotFoundException첫 번째 구체 catch- 원문 출력
create-new=missing
private
- 발생 타입과 선택된 catch
BoardPermissionException두 번째 구체 catch- 원문 출력
request-access=private
마지막 BoardAccessException catch에는 이 두 입력이 들어가지 않습니다. 앞에서 일치한 catch 하나만 실행합니다. 부모 catch를 먼저 두어 자식 catch를 가리는 순서는 컴파일되지 않습니다.
이 예의 조건
- 각 입력은 한 구체 catch만 실행한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 부모 throws 계약에서도 실제 자식 타입으로 복구한다 부모 throws 계약에서도 실제 자식 타입으로 복구한다 CheckedHierarchyExercise의 서비스는 BoardAccessException을 선언한다. missing과 private은 서로 다른 자식 예외가 발생해 CLI의 서로 다른 catch로 들어간다. 예외 계층 · 실제 catch 선택 부모 throws 계약에서도 실제 자식 타입으로 복구한다 서비스는 부모 BoardAccessException을 선언합니다. CLI는 실제 자식 타입과 일치하는 catch 하나를 선택합니다. missing 발생 타입과 선택된 catch BoardNotFoundException 첫 번째 구체 catch 원문 출력 create-new=missing private 발생 타입과 선택된 catch BoardPermissionException 두 번째 구체 catch 원문 출력 request-access=private 마지막 BoardAccessException catch에는 이 두 입력이 들어가지 않습니다. 앞에서 일치한 catch 하나만 실행합니다. 부모 catch를 먼저 두어 자식 catch를 가리는 순서는 컴파일되지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 서비스 계약 부모 BoardAccessException을 throws로 선언합니다. |
|
|
| 실제 예외 입력에 맞는 자식 타입이 발생합니다. |
|
|
| 단일 처리 해당 구체 catch가 한 줄을 출력합니다. |
|
|