공통 예외 처리와 자원 정리
본문과 close가 함께 실패할 때 원인이 가려지는 문제를 관찰하고 AutoCloseable, 억제 예외, 역순 정리, 경계 로깅으로 자원 수명을 완성합니다.
처리할 수 없는 언체크 예외는 여러 중간 계층이 각각 잡기보다 애플리케이션 경계에서 한 번 처리하는 편이 일관됩니다.
자원 정리는 그 경계까지 기다릴 수 없으므로 자원을 확보한 가장 가까운 코드에서 구조적으로 보장해야 합니다.
try-with-resources는 이 두 책임을 분리하면서 close 실패 정보도 보존합니다.
본문 예외와 자원 정리 예외
본문의 전송 오류가 주된 실패인데 close도 예외를 던집니다.
try-with-resources는 본문 예외를 밖으로 보내고 close 예외를 suppressed 목록에 붙입니다.
아무도 잡지 않으므로 프로세스는 실패하지만 두 원인을 모두 관찰할 수 있습니다.
public final class TryWithResourcesPrimaryFailure {
public static void main(String[] args) {
try (BoardChannel channel = new BoardChannel()) {
channel.send();
}
}
private static final class BoardChannel implements AutoCloseable {
void send() {
throw new IllegalStateException("primary send failure");
}
@Override
public void close() {
throw new IllegalArgumentException("secondary close failure");
}
}
}java.lang.IllegalStateException: primary send failure
Suppressed: java.lang.IllegalArgumentException: secondary close failure수동 finally에서 close 예외가 그대로 던져지면 전송 오류를 덮을 수 있습니다.
try-with-resources는 최초 실패를 primary로 유지하고 정리 실패를 잃지 않습니다.
여기서 최초 실패는 항상 본문 예외라는 뜻이 아닙니다. 본문이 정상 완료했다면 역순 정리에서 먼저 발생한 close 실패가 주 예외가 됩니다. 두 번째 자원의 초기화가 실패했다면 그 초기화 예외가 주 예외이고, 이미 초기화된 첫 번째 자원만 자동으로 닫습니다. JLS의 자원 정리 규칙은 이 경우들을 구별합니다.
운영 로그에는 예외 객체 전체를 전달해야 suppressed와 cause가 함께 기록됩니다.
AutoCloseable 자원 계약
try 괄호에서 선언한 객체는 AutoCloseable을 구현해야 합니다.
블록을 벗어날 때 컴파일러가 close 호출을 구성하고, 변수 범위도 try 안으로 제한합니다.
정상, catch 처리, return, 처리되지 않은 예외 모두 같은 정리 규칙을 사용합니다.
public final class AutoCloseableBoardClient {
public static void main(String[] args) {
send("publish-post");
}
private static void send(String data) {
try (BoardClient client = new BoardClient("board.local")) {
client.connect();
client.send(data);
} catch (BoardGatewayException error) {
System.out.println("handled=" + error.getMessage());
}
}
private static final class BoardClient implements AutoCloseable {
private final String address;
private boolean connected;
BoardClient(String address) { this.address = address; }
void connect() {
connected = true;
System.out.println("connect=" + address);
}
void send(String data) {
if (!connected) throw new BoardGatewayException("not connected");
System.out.println("send=" + data);
}
@Override
public void close() {
if (connected) {
connected = false;
System.out.println("close=" + address);
}
}
}
private static final class BoardGatewayException extends RuntimeException {
BoardGatewayException(String message) { super(message); }
}
}connect=board.local
send=publish-post
close=board.local이 try-with-resources는 해당 자원을 한 번 자동으로 닫습니다. 직접 중복 호출될 가능성까지 고려해 close를 멱등적으로 만들면 방어력이 높아집니다.
닫힌 객체를 다시 쓰면 명확한 IllegalStateException을 던지도록 상태 계약을 정할 수도 있습니다.
여러 자원의 역순 정리
두 번째 자원이 첫 번째 자원에 의존할 수 있으므로 나중에 연 자원을 먼저 닫습니다.
생성 도중 두 번째 객체가 실패해도 이미 생성된 첫 번째 객체는 자동으로 닫힙니다.
public final class MultiResourceCloseOrder {
public static void main(String[] args) {
try (Resource connection = new Resource("connection");
Resource channel = new Resource("channel")) {
System.out.println("work");
}
}
private static final class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
System.out.println("open=" + name);
}
@Override
public void close() { System.out.println("close=" + name); }
}
}open=connection
open=channel
work
close=channel
close=connection이미 할당된 final 필드도 AutoCloseable 하위 타입이면 try-with-resources의 기존 자원으로 사용할 수 있습니다. 다만 오래 보관하거나 공유하는 자원은 필드 여부보다 현재 블록에서 닫아도 되는 수명·소유권인지 먼저 판단해야 합니다.
긴 수명이 필요하면 수명 소유 객체 자체가 AutoCloseable을 구현하고 상위 조립 코드가 닫거나, 자원 풀처럼 프레임워크가 수명을 관리하도록 할 수 있습니다.
소유권이 불분명한 객체를 임의로 닫으면 다른 소비자의 작업을 깨뜨립니다.
예시
다음 독립 파일은 본문 실패, close만의 실패, 두 번째 자원 초기화 실패를 비교합니다. 실제 파일·네트워크를 열지 않고 출력과 예외로 순서만 관찰합니다. close가 하나 실패해도 앞서 초기화한 다른 자원의 close를 계속 시도합니다.
public final class ResourceFailurePriority {
public static void main(String[] args) {
run("body", true, false);
run("close", false, false);
run("init", false, true);
}
private static void run(String scenario, boolean failBody, boolean failSecondInit) {
System.out.println("case=" + scenario);
try (Resource first = new Resource("first", false);
Resource second = new Resource("second", failSecondInit)) {
if (failBody) throw new IllegalStateException("work");
System.out.println("work=done");
} catch (RuntimeException primary) {
System.out.println("primary=" + primary.getClass().getSimpleName()
+ ":" + primary.getMessage());
for (Throwable suppressed : primary.getSuppressed()) {
System.out.println("suppressed=" + suppressed.getMessage());
}
}
}
private static final class Resource implements AutoCloseable {
private final String name;
Resource(String name, boolean failInit) {
this.name = name;
if (failInit) throw new IllegalStateException("init-" + name);
System.out.println("open=" + name);
}
public void close() {
System.out.println("close=" + name);
throw new IllegalArgumentException("close-" + name);
}
}
}case=body
open=first
open=second
close=second
close=first
primary=IllegalStateException:work
suppressed=close-second
suppressed=close-first
case=close
open=first
open=second
work=done
close=second
close=first
primary=IllegalArgumentException:close-second
suppressed=close-first
case=init
open=first
close=first
primary=IllegalStateException:init-second
suppressed=close-firstinit에서는 두 번째 초기화가 값을 반환하지 못했으므로 open=second, close=second, work=done이 없습니다. 첫 자원의 close 실패는 초기화 실패에 억제 예외로 붙습니다. 예제의 catch는 서로 다른 두 주 예외 타입을 관찰하기 위해 RuntimeException을 사용합니다. 초기화 도중 실제 자원을 확보하는 생성자는 자신이 아직 반환하지 못한 자원의 정리도 책임져야 하며, 이 순서 예제는 그런 실제 자원을 만들지 않습니다.
확보한 순서와 닫는 순서를 구분하기
자원을 획득하고 작업한 뒤 선언 역순으로 close합니다. 단일/이중 자원 사례를 독립으로 유지하며 획득과 반환 수명을 추적할 수 있습니다.
단일 BoardClient는 작업 후 한 번 닫힌다. MultiResourceCloseOrder는 connection 다음 channel을 생성하고 work 뒤 channel 다음 connection을 닫는다.
자원 수명 · 초기화와 정리
확보한 순서와 닫는 순서를 구분하기
단일 client와 다중 자원은 별도 예제입니다. connection 다음 channel을 확보하면 channel부터 역순으로 닫습니다.
| 원문 자원 | 초기화·사용 | close 순서와 출력 |
|---|---|---|
| 단일 client | 첫 번째 예제 connect=board.localsend=publish-post | 그 자원의 정리 close=board.local |
connection | 다중 예제에서 먼저 초기화 open=connection | 다중 예제에서 두 번째 정리 close=connection |
channel | 다중 예제에서 나중 초기화 open=channel | 다중 예제에서 첫 번째 정리 close=channel |
- 단일 client
- 초기화·사용
- 첫 번째 예제
connect=board.localsend=publish-post - close 순서와 출력
- 그 자원의 정리
close=board.local
connection- 초기화·사용
- 다중 예제에서 먼저 초기화
open=connection - close 순서와 출력
- 다중 예제에서 두 번째 정리
close=connection
channel- 초기화·사용
- 다중 예제에서 나중 초기화
open=channel - close 순서와 출력
- 다중 예제에서 첫 번째 정리
close=channel
다중 예제의 work는 두 open 뒤, 두 close 앞에 출력됩니다. 뒤 자원의 초기화가 예외로 끝나면 앞서 초기화한 자원만 닫는 것은 언어 규칙이며, 이 원문 main에서는 초기화가 모두 성공합니다. 자동 close 호출과 객체의 물리적 소멸은 같은 뜻이 아닙니다.
이 예의 조건
- 단일/다중 자원은 독립 원문이며 다중 초기화는 둘 다 성공한다.
- close는 물리 객체 소멸과 다르다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 확보한 순서와 닫는 순서를 구분하기 확보한 순서와 닫는 순서를 구분하기 단일 BoardClient는 작업 후 한 번 닫힌다. MultiResourceCloseOrder는 connection 다음 channel을 생성하고 work 뒤 channel 다음 connection을 닫는다. 자원 수명 · 초기화와 정리 확보한 순서와 닫는 순서를 구분하기 단일 client와 다중 자원은 별도 예제입니다. connection 다음 channel을 확보하면 channel부터 역순으로 닫습니다. 확보한 순서와 닫는 순서를 구분하기 원문 자원 초기화·사용 close 순서와 출력 단일 client 첫 번째 예제 connect=board.local send=publish-post 그 자원의 정리 close=board.local connection 다중 예제에서 먼저 초기화 open=connection 다중 예제에서 두 번째 정리 close=connection channel 다중 예제에서 나중 초기화 open=channel 다중 예제에서 첫 번째 정리 close=channel 단일 client 초기화·사용 첫 번째 예제 connect=board.local send=publish-post close 순서와 출력 그 자원의 정리 close=board.local connection 초기화·사용 다중 예제에서 먼저 초기화 open=connection close 순서와 출력 다중 예제에서 두 번째 정리 close=connection channel 초기화·사용 다중 예제에서 나중 초기화 open=channel close 순서와 출력 다중 예제에서 첫 번째 정리 close=channel 다중 예제의 work 는 두 open 뒤, 두 close 앞에 출력됩니다. 뒤 자원의 초기화가 예외로 끝나면 앞서 초기화한 자원만 닫는 것은 언어 규칙이며, 이 원문 main에서는 초기화가 모두 성공합니다. 자동 close 호출과 객체의 물리적 소멸은 같은 뜻이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 확보 단일 client 또는 connection 다음 channel을 엽니다. |
|
|
| 작업 확보된 자원으로 실행합니다. |
|
|
| 자동 정리 단일을 닫고 다중은 역순으로 닫습니다. |
|
|
확보한 순서와 닫는 순서를 구분하기
자원을 획득하고 작업한 뒤 선언 역순으로 close합니다. 단일/이중 자원 사례를 독립으로 유지하며 획득과 반환 수명을 추적할 수 있습니다.
단일 BoardClient는 작업 후 한 번 닫힌다. MultiResourceCloseOrder는 connection 다음 channel을 생성하고 work 뒤 channel 다음 connection을 닫는다.
자원 수명 · 초기화와 정리
확보한 순서와 닫는 순서를 구분하기
단일 client와 다중 자원은 별도 예제입니다. connection 다음 channel을 확보하면 channel부터 역순으로 닫습니다.
| 원문 자원 | 초기화·사용 | close 순서와 출력 |
|---|---|---|
| 단일 client | 첫 번째 예제 connect=board.localsend=publish-post | 그 자원의 정리 close=board.local |
connection | 다중 예제에서 먼저 초기화 open=connection | 다중 예제에서 두 번째 정리 close=connection |
channel | 다중 예제에서 나중 초기화 open=channel | 다중 예제에서 첫 번째 정리 close=channel |
- 단일 client
- 초기화·사용
- 첫 번째 예제
connect=board.localsend=publish-post - close 순서와 출력
- 그 자원의 정리
close=board.local
connection- 초기화·사용
- 다중 예제에서 먼저 초기화
open=connection - close 순서와 출력
- 다중 예제에서 두 번째 정리
close=connection
channel- 초기화·사용
- 다중 예제에서 나중 초기화
open=channel - close 순서와 출력
- 다중 예제에서 첫 번째 정리
close=channel
다중 예제의 work는 두 open 뒤, 두 close 앞에 출력됩니다. 뒤 자원의 초기화가 예외로 끝나면 앞서 초기화한 자원만 닫는 것은 언어 규칙이며, 이 원문 main에서는 초기화가 모두 성공합니다. 자동 close 호출과 객체의 물리적 소멸은 같은 뜻이 아닙니다.
이 예의 조건
- 단일/다중 자원은 독립 원문이며 다중 초기화는 둘 다 성공한다.
- close는 물리 객체 소멸과 다르다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 확보한 순서와 닫는 순서를 구분하기 확보한 순서와 닫는 순서를 구분하기 단일 BoardClient는 작업 후 한 번 닫힌다. MultiResourceCloseOrder는 connection 다음 channel을 생성하고 work 뒤 channel 다음 connection을 닫는다. 자원 수명 · 초기화와 정리 확보한 순서와 닫는 순서를 구분하기 단일 client와 다중 자원은 별도 예제입니다. connection 다음 channel을 확보하면 channel부터 역순으로 닫습니다. 확보한 순서와 닫는 순서를 구분하기 원문 자원 초기화·사용 close 순서와 출력 단일 client 첫 번째 예제 connect=board.local send=publish-post 그 자원의 정리 close=board.local connection 다중 예제에서 먼저 초기화 open=connection 다중 예제에서 두 번째 정리 close=connection channel 다중 예제에서 나중 초기화 open=channel 다중 예제에서 첫 번째 정리 close=channel 단일 client 초기화·사용 첫 번째 예제 connect=board.local send=publish-post close 순서와 출력 그 자원의 정리 close=board.local connection 초기화·사용 다중 예제에서 먼저 초기화 open=connection close 순서와 출력 다중 예제에서 두 번째 정리 close=connection channel 초기화·사용 다중 예제에서 나중 초기화 open=channel close 순서와 출력 다중 예제에서 첫 번째 정리 close=channel 다중 예제의 work 는 두 open 뒤, 두 close 앞에 출력됩니다. 뒤 자원의 초기화가 예외로 끝나면 앞서 초기화한 자원만 닫는 것은 언어 규칙이며, 이 원문 main에서는 초기화가 모두 성공합니다. 자동 close 호출과 객체의 물리적 소멸은 같은 뜻이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 확보 단일 client 또는 connection 다음 channel을 엽니다. |
|
|
| 작업 확보된 자원으로 실행합니다. |
|
|
| 자동 정리 단일을 닫고 다중은 역순으로 닫습니다. |
|
|
경계 처리기의 응답과 진단
공통 처리기는 모든 예상치 못한 예외를 잡아 사용자에게 안전한 메시지를 제공하고, 운영자에게는 원인·스택·요청 문맥을 남깁니다.
예외 메시지를 그대로 사용자에게 노출하면 경로, 쿼리, 내부 주소가 유출될 수 있습니다.
public final class CommonExceptionBoundary {
public static void main(String[] args) {
handle("REQ-42", () -> new BoardService().publish("예외 처리 질문"));
}
private static void handle(String requestId, Command command) {
try {
command.run();
System.out.println("status=ok");
} catch (BoardValidationException error) {
System.out.println("status=bad-request,field=" + error.field());
} catch (Exception error) {
System.out.println("status=system-error,request=" + requestId);
logForOperator(requestId, error);
}
}
private static void logForOperator(String requestId, Exception error) {
System.out.println("board=" + requestId + ":"
+ error.getClass().getSimpleName() + ":" + error.getMessage());
}
private interface Command { void run(); }
private static final class BoardService {
void publish(String title) {
throw new BoardSystemException("database offline for " + title);
}
}
private static final class BoardValidationException extends RuntimeException {
private final String field;
BoardValidationException(String field) { this.field = field; }
String field() { return field; }
}
private static final class BoardSystemException extends RuntimeException {
BoardSystemException(String message) { super(message); }
}
}status=system-error,request=REQ-42
board=REQ-42:BoardSystemException:database offline for 예외 처리 질문실무 로거에는 logger.error("request={}", requestId, error)처럼 예외 객체 자체를 마지막 인수로 전달해 스택 추적을 보존합니다.
예제를 단순화한 문자열 출력은 운영 로깅의 대체물이 아닙니다.
경계의 두 출력과 진단 정보의 한계
시스템 실패를 경계 catch가 잡아 사용자 문자열 뒤 운영 문자열을 출력합니다. 실제 stdout 순서를 나누되 HTTP나 완전한 로그 시스템으로 확대하지 않습니다.
CommonExceptionBoundary의 현재 main은 BoardSystemException을 공통 catch에서 처리하여 안전한 system-error/request 줄과 운영용 클래스명·메시지 줄을 출력한다. 문자열 출력에는 전체 스택·cause·suppressed가 포함되지 않는다.
처리 경계 · 응답과 진단
경계의 두 출력과 진단 정보의 한계
현재 입력은 시스템 실패입니다. 사용자용 안내와 운영용 문자열은 정보를 다르게 담지만 둘 다 예제의 stdout입니다.
사용자용 안내
- 들어 있는 정보
status=system-error,request=REQ-42- 이 출력이 의미하지 않는 것
- 내부 오류 메시지를 포함하지 않습니다. 실제 HTTP 응답이나 5xx 코드를 전송한 예제는 아닙니다.
운영용 문자열
- 들어 있는 정보
board=REQ-42:BoardSystemException:database offline for예외 처리 질문- 이 출력이 의미하지 않는 것
- 요청 식별자·클래스명·메시지만 있습니다. 스택 추적이나 cause·suppressed 전체 출력이 아닙니다.
각 셀의 조각은 원문 stdout에서는 한 줄로 연결됩니다. 이 main은 status=ok나 검증 실패 catch를 실행하지 않습니다. 일반적인 예외 객체 전체 진단은 로거에 객체를 전달하는 별도 책임이며, 현재 오류에 cause나 suppressed가 실제로 있다고 주장하지 않습니다.
이 예의 조건
- 원 main은 시스템 실패만 실행한다.
- 두 문자열은 stdout이며 HTTP나 전체 스택 출력이 아니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 경계의 두 출력과 진단 정보의 한계 경계의 두 출력과 진단 정보의 한계 CommonExceptionBoundary의 현재 main은 BoardSystemException을 공통 catch에서 처리하여 안전한 system-error/request 줄과 운영용 클래스명·메시지 줄을 출력한다. 문자열 출력에는 전체 스택·cause·suppressed가 포함되지 않는다. 처리 경계 · 응답과 진단 경계의 두 출력과 진단 정보의 한계 현재 입력은 시스템 실패입니다. 사용자용 안내와 운영용 문자열은 정보를 다르게 담지만 둘 다 예제의 stdout입니다. 사용자용 안내 들어 있는 정보 status=system-error, request=REQ-42 이 출력이 의미하지 않는 것 내부 오류 메시지를 포함하지 않습니다. 실제 HTTP 응답이나 5xx 코드를 전송한 예제는 아닙니다. 운영용 문자열 들어 있는 정보 board=REQ-42: BoardSystemException: database offline for 예외 처리 질문 이 출력이 의미하지 않는 것 요청 식별자·클래스명·메시지만 있습니다. 스택 추적이나 cause·suppressed 전체 출력이 아닙니다. 각 셀의 조각은 원문 stdout에서는 한 줄로 연결됩니다. 이 main은 status=ok 나 검증 실패 catch를 실행하지 않습니다. 일반적인 예외 객체 전체 진단은 로거에 객체를 전달하는 별도 책임이며, 현재 오류에 cause나 suppressed가 실제로 있다고 주장하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 실패 전달 REQ-42에서 시스템 예외가 경계로 옵니다. |
|
|
| 첫 안내 사용자용 최소 상태를 먼저 출력합니다. |
|
|
| 운영 문맥 뒤에 클래스명·메시지 문자열을 출력합니다. |
|
|
경계의 두 출력과 진단 정보의 한계
시스템 실패를 경계 catch가 잡아 사용자 문자열 뒤 운영 문자열을 출력합니다. 실제 stdout 순서를 나누되 HTTP나 완전한 로그 시스템으로 확대하지 않습니다.
CommonExceptionBoundary의 현재 main은 BoardSystemException을 공통 catch에서 처리하여 안전한 system-error/request 줄과 운영용 클래스명·메시지 줄을 출력한다. 문자열 출력에는 전체 스택·cause·suppressed가 포함되지 않는다.
처리 경계 · 응답과 진단
경계의 두 출력과 진단 정보의 한계
현재 입력은 시스템 실패입니다. 사용자용 안내와 운영용 문자열은 정보를 다르게 담지만 둘 다 예제의 stdout입니다.
사용자용 안내
- 들어 있는 정보
status=system-error,request=REQ-42- 이 출력이 의미하지 않는 것
- 내부 오류 메시지를 포함하지 않습니다. 실제 HTTP 응답이나 5xx 코드를 전송한 예제는 아닙니다.
운영용 문자열
- 들어 있는 정보
board=REQ-42:BoardSystemException:database offline for예외 처리 질문- 이 출력이 의미하지 않는 것
- 요청 식별자·클래스명·메시지만 있습니다. 스택 추적이나 cause·suppressed 전체 출력이 아닙니다.
각 셀의 조각은 원문 stdout에서는 한 줄로 연결됩니다. 이 main은 status=ok나 검증 실패 catch를 실행하지 않습니다. 일반적인 예외 객체 전체 진단은 로거에 객체를 전달하는 별도 책임이며, 현재 오류에 cause나 suppressed가 실제로 있다고 주장하지 않습니다.
이 예의 조건
- 원 main은 시스템 실패만 실행한다.
- 두 문자열은 stdout이며 HTTP나 전체 스택 출력이 아니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 경계의 두 출력과 진단 정보의 한계 경계의 두 출력과 진단 정보의 한계 CommonExceptionBoundary의 현재 main은 BoardSystemException을 공통 catch에서 처리하여 안전한 system-error/request 줄과 운영용 클래스명·메시지 줄을 출력한다. 문자열 출력에는 전체 스택·cause·suppressed가 포함되지 않는다. 처리 경계 · 응답과 진단 경계의 두 출력과 진단 정보의 한계 현재 입력은 시스템 실패입니다. 사용자용 안내와 운영용 문자열은 정보를 다르게 담지만 둘 다 예제의 stdout입니다. 사용자용 안내 들어 있는 정보 status=system-error, request=REQ-42 이 출력이 의미하지 않는 것 내부 오류 메시지를 포함하지 않습니다. 실제 HTTP 응답이나 5xx 코드를 전송한 예제는 아닙니다. 운영용 문자열 들어 있는 정보 board=REQ-42: BoardSystemException: database offline for 예외 처리 질문 이 출력이 의미하지 않는 것 요청 식별자·클래스명·메시지만 있습니다. 스택 추적이나 cause·suppressed 전체 출력이 아닙니다. 각 셀의 조각은 원문 stdout에서는 한 줄로 연결됩니다. 이 main은 status=ok 나 검증 실패 catch를 실행하지 않습니다. 일반적인 예외 객체 전체 진단은 로거에 객체를 전달하는 별도 책임이며, 현재 오류에 cause나 suppressed가 실제로 있다고 주장하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 실패 전달 REQ-42에서 시스템 예외가 경계로 옵니다. |
|
|
| 첫 안내 사용자용 최소 상태를 먼저 출력합니다. |
|
|
| 운영 문맥 뒤에 클래스명·메시지 문자열을 출력합니다. |
|
|
같은 예외를 중간과 경계에서 반복 기록하지 않고, 지역 복구를 실제로 수행한 계층은 필요한 수준의 짧은 이벤트만 남깁니다.
자원 정리 방식의 선택
AutoCloseable로 표현되는 파일, 스트림, 소켓, JDBC 연결·문장·결과 집합에는 try-with-resources를 우선합니다.
다중 자원의 역순 정리와 suppressed 처리를 언어가 보장합니다.
잠금 해제, 임시 플래그 복원, 트랜잭션 문맥처럼 close 객체가 아닌 정리는 finally가 더 직접적일 수 있습니다.
자원을 직접 생성하지 않았다면 누가 소유하고 누가 닫는지 먼저 확인합니다.
메서드 인수로 받은 공유 스트림을 무조건 닫지 않습니다.
성공 시 반환해야 하는 자원은 호출자에게 소유권이 넘어간다는 계약과 AutoCloseable 타입을 명확히 합니다.
close 실패 정책도 로깅만 할지 작업 전체를 실패로 볼지 정해야 합니다.
예외 처리의 완성 기준은 catch 수가 아닙니다.
실패 뒤에도 불변식과 자원 상태가 안전하고, 복구 가능한 오류는 적절한 계층이 해결하며, 복구 불가능한 오류는 원인을 보존한 채 한 경계에서 사용자 응답과 운영 진단으로 나뉘어야 합니다.
연습 문제
두 자원을 선언하고 본문과 두 close가 모두 실패하게 만드세요.
경계에서 주 예외 메시지와 suppressed 배열의 순서를 출력해 어떤 정리 실패가 먼저 보존되는지 확인합니다.
자원 선언 이름과 순서는 first, second입니다. 초기화는 성공하며 본문은 IllegalStateException("work"), 각 close는 IllegalArgumentException("close-" + name)을 던집니다. 경계는 IllegalStateException을 잡아 primary=메시지와 suppressed=메시지를 배열 순서대로 출력합니다.
독립 연습에서는 본문과 second.close가 정상 완료하고 first.close만 실패한다고 바꿔 생각합니다. 주 예외 타입·메시지, suppressed 개수, 원래 경계 catch가 실행되는지를 예상하세요.
해설 보기
public final class SuppressedExceptionExercise {
public static void main(String[] args) {
try {
execute();
} catch (IllegalStateException primary) {
System.out.println("primary=" + primary.getMessage());
for (Throwable suppressed : primary.getSuppressed()) {
System.out.println("suppressed=" + suppressed.getMessage());
}
}
}
private static void execute() {
try (FailingResource first = new FailingResource("first");
FailingResource second = new FailingResource("second")) {
throw new IllegalStateException("work");
}
}
private record FailingResource(String name) implements AutoCloseable {
@Override
public void close() { throw new IllegalArgumentException("close-" + name); }
}
}primary=work
suppressed=close-second
suppressed=close-first정리 순서와 같은 순서로 suppressed가 추가됩니다.
첫 번째 주 예외는 그대로 유지되므로 장애의 시작점과 정리 문제를 동시에 진단할 수 있습니다.
독립 조건의 주 예외는 IllegalArgumentException("close-first")이고 suppressed는 비어 있습니다. 원 해설의 catch는 IllegalStateException만 잡으므로 이 예외를 처리하지 못합니다. 따라서 그 코드를 그대로 두면 primary= 로그가 아니라 미처리 예외 진단을 보게 됩니다. 별도 관찰 코드의 catch 타입을 바꾸는 것과 원래 본문 실패 예제의 출력은 구별해야 합니다.
본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기
본문 주 예외 뒤 second.close와 first.close 실패가 suppressed에 차례로 추가됩니다. 주 예외를 고정하고 역순 정리의 누적 상태를 보여 줄 수 있습니다.
SuppressedExceptionExercise의 본문 IllegalStateException work는 primary로 유지된다. second.close의 close-second가 suppressed 첫 번째, first.close의 close-first가 두 번째로 보존된다.
정리 실패 · 주 예외와 추가 실패
본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기
본문 work 실패를 주 예외로 유지합니다. second, first 순서의 close 실패를 suppressed[0], [1]에 추가합니다.
본문
- 전달되는 예외 객체의 위치
IllegalStateException메시지:work경계가 받는 주 예외- 경계 stdout 순서
primary=work
second.close()· 먼저 정리- 전달되는 예외 객체의 위치
IllegalArgumentExceptiongetSuppressed()[0]에 보존- 경계 stdout 순서
suppressed=close-second
first.close()· 나중 정리- 전달되는 예외 객체의 위치
IllegalArgumentExceptiongetSuppressed()[1]에 보존- 경계 stdout 순서
suppressed=close-first
자원 선언은 first 다음 second입니다. 두 자원은 초기화에 성공하며 second.close()가 실패해도 first.close()를 시도합니다. suppressed는 정리 중의 추가 실패이고, 앞 절의 원인 변환에 쓰는 cause와 다릅니다. 이 프로그램은 스택 전체가 아니라 메시지 세 줄을 출력합니다.
이 예의 조건
- 두 초기화가 성공하며 suppressed와 cause는 다른 관계다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기 본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기 SuppressedExceptionExercise의 본문 IllegalStateException work는 primary로 유지된다. second.close의 close-second가 suppressed 첫 번째, first.close의 close-first가 두 번째로 보존된다. 정리 실패 · 주 예외와 추가 실패 본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기 본문 work 실패를 주 예외로 유지합니다. second, first 순서의 close 실패를 suppressed[0], [1]에 추가합니다. 본문 전달되는 예외 객체의 위치 IllegalStateException 메시지: work 경계가 받는 주 예외 경계 stdout 순서 primary=work second.close() · 먼저 정리 전달되는 예외 객체의 위치 IllegalArgumentException getSuppressed()[0] 에 보존 경계 stdout 순서 suppressed=close-second first.close() · 나중 정리 전달되는 예외 객체의 위치 IllegalArgumentException getSuppressed()[1] 에 보존 경계 stdout 순서 suppressed=close-first 자원 선언은 first 다음 second 입니다. 두 자원은 초기화에 성공하며 second.close() 가 실패해도 first.close() 를 시도합니다. suppressed는 정리 중의 추가 실패이고, 앞 절의 원인 변환에 쓰는 cause와 다릅니다. 이 프로그램은 스택 전체가 아니라 메시지 세 줄을 출력합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 본문 실패 work를 주 예외로 보관합니다. |
|
|
| 역순 닫기 second 뒤 first의 실패를 붙입니다. |
|
|
| 경계 관찰 같은 primary와 두 추가 실패를 출력합니다. |
|
|
본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기
본문 주 예외 뒤 second.close와 first.close 실패가 suppressed에 차례로 추가됩니다. 주 예외를 고정하고 역순 정리의 누적 상태를 보여 줄 수 있습니다.
SuppressedExceptionExercise의 본문 IllegalStateException work는 primary로 유지된다. second.close의 close-second가 suppressed 첫 번째, first.close의 close-first가 두 번째로 보존된다.
정리 실패 · 주 예외와 추가 실패
본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기
본문 work 실패를 주 예외로 유지합니다. second, first 순서의 close 실패를 suppressed[0], [1]에 추가합니다.
본문
- 전달되는 예외 객체의 위치
IllegalStateException메시지:work경계가 받는 주 예외- 경계 stdout 순서
primary=work
second.close()· 먼저 정리- 전달되는 예외 객체의 위치
IllegalArgumentExceptiongetSuppressed()[0]에 보존- 경계 stdout 순서
suppressed=close-second
first.close()· 나중 정리- 전달되는 예외 객체의 위치
IllegalArgumentExceptiongetSuppressed()[1]에 보존- 경계 stdout 순서
suppressed=close-first
자원 선언은 first 다음 second입니다. 두 자원은 초기화에 성공하며 second.close()가 실패해도 first.close()를 시도합니다. suppressed는 정리 중의 추가 실패이고, 앞 절의 원인 변환에 쓰는 cause와 다릅니다. 이 프로그램은 스택 전체가 아니라 메시지 세 줄을 출력합니다.
이 예의 조건
- 두 초기화가 성공하며 suppressed와 cause는 다른 관계다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기 본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기 SuppressedExceptionExercise의 본문 IllegalStateException work는 primary로 유지된다. second.close의 close-second가 suppressed 첫 번째, first.close의 close-first가 두 번째로 보존된다. 정리 실패 · 주 예외와 추가 실패 본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기 본문 work 실패를 주 예외로 유지합니다. second, first 순서의 close 실패를 suppressed[0], [1]에 추가합니다. 본문 전달되는 예외 객체의 위치 IllegalStateException 메시지: work 경계가 받는 주 예외 경계 stdout 순서 primary=work second.close() · 먼저 정리 전달되는 예외 객체의 위치 IllegalArgumentException getSuppressed()[0] 에 보존 경계 stdout 순서 suppressed=close-second first.close() · 나중 정리 전달되는 예외 객체의 위치 IllegalArgumentException getSuppressed()[1] 에 보존 경계 stdout 순서 suppressed=close-first 자원 선언은 first 다음 second 입니다. 두 자원은 초기화에 성공하며 second.close() 가 실패해도 first.close() 를 시도합니다. suppressed는 정리 중의 추가 실패이고, 앞 절의 원인 변환에 쓰는 cause와 다릅니다. 이 프로그램은 스택 전체가 아니라 메시지 세 줄을 출력합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 본문 실패 work를 주 예외로 보관합니다. |
|
|
| 역순 닫기 second 뒤 first의 실패를 붙입니다. |
|
|
| 경계 관찰 같은 primary와 두 추가 실패를 출력합니다. |
|
|