try-with-resources와 공통 예외 처리
AutoCloseable과 try-with-resources로 자원을 자동으로 닫고 정리 중에 난 예외까지 억제된 예외로 남기는 방법, 그리고 처리하지 못한 예외를 맨 바깥의 공통 처리기에서 한 번에 다루는 방법을 배웁니다.
finally를 다룬 글에는 풀지 못한 문제가 두 가지 남아 있었습니다. 하나는 자원이 여러 개이면 try-finally를 자원마다 하나씩 겹쳐 써야 해서 코드가 금방 길어진다는 것입니다. 다른 하나는 finally 안의 close()가 예외를 던지면 그 예외가 원래 예외를 덮어써, 처음에 무엇이 잘못됐는지 알 수 없게 된다는 것입니다. 자바 7에서 들어온 try-with-resources 문은 이 두 문제를 언어 차원에서 해결합니다.
이 글의 뒷부분에서는 비검사 예외를 다룬 글에서 예고한 공통 처리기를 만듭니다. 자원 정리와 예외 처리는 일어나는 곳이 다릅니다. 자원은 연 곳에서 바로 닫아야 하지만, 대응할 수 없는 예외는 프로그램 맨 바깥까지 올려 보내 한곳에서 처리하는 편이 낫습니다.
try-with-resources로 자원 자동으로 닫기
try 키워드 뒤의 괄호 안에서 변수를 선언하고 객체를 대입하면, 그 객체는 try-with-resources가 관리하는 자원(resource)이 됩니다. try 블록이 끝나면 자바가 그 자원의 close()를 자동으로 호출합니다. 블록이 정상적으로 끝나든, 예외로 끝나든, return으로 빠져나가든 마찬가지입니다. 대신 괄호 안에 둘 수 있는 객체는 java.lang.AutoCloseable 인터페이스를 구현해야 합니다. 이 인터페이스에는 close() 메서드 하나만 있고, 컴파일러는 이 타입을 보고 그 객체에 close()가 있다는 것을 압니다. 구현하지 않은 클래스를 괄호 안에 쓰면 try-with-resources not applicable to variable type이라는 컴파일 오류가 납니다.
다음 예제의 BoardClient는 AutoCloseable을 구현해 게시판 서버와의 연결을 닫습니다.
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.localtry (BoardClient client = new BoardClient("board.local"))가 자원을 선언하는 부분입니다. 블록 안에서 연결하고(connect=board.local) 데이터를 보낸(send=publish-post) 뒤 블록이 끝나자, 코드 어디에도 client.close()를 쓰지 않았는데 close=board.local이 출력되었습니다. 예외가 나지 않았으므로 catch 블록은 실행되지 않았습니다. 자원 변수 client는 try 블록 안에서만 쓸 수 있고, catch 블록에서 쓰면 cannot find symbol 오류가 납니다. 블록을 벗어나면 이미 닫힌 객체이므로, 닫힌 자원을 실수로 다시 쓰는 일을 문법으로 막는 셈입니다.
close()가 connected 필드를 확인하는 이유는 finally를 다룬 글의 disconnect()와 같습니다. try-with-resources는 자원 하나에 close()를 한 번만 호출하지만, 누군가 직접 close()를 한 번 더 부를 수도 있습니다. AutoCloseable의 API 문서도 close()를 여러 번 호출해도 문제가 없게, 즉 멱등적으로 만들라고 강하게 권합니다. 파일과 스트림이 구현하는 java.io.Closeable은 AutoCloseable의 하위 인터페이스인데, 아예 '이미 닫혔다면 아무 효과가 없다'고 정해 두었습니다. 한발 더 나아가 닫힌 뒤에 send()를 호출하면 IllegalStateException을 던지게 만들어, 잘못된 사용을 바로 드러낼 수도 있습니다.
AutoCloseable의 close()는 throws Exception으로 선언되어 있습니다. 구현 클래스가 이 선언을 그대로 따라 쓰면, 그 자원을 쓰는 try-with-resources 문은 자동으로 호출되는 close()가 던질 수 있는 검사 예외 Exception을 catch하거나 throws로 넘겨야 합니다. 그러지 않으면 exception thrown from implicit call to close() on resource variable이라는 설명이 붙은 컴파일 오류가 납니다. 그래서 API 문서는 구현 클래스의 close()에는 더 구체적인 예외를 선언하고, 실패할 일이 없다면 throws를 아예 빼라고 권합니다. 위의 BoardClient.close()에 throws가 없는 것도 그 때문입니다.
자원은 catch와 finally보다 먼저 닫힌다
try-with-resources 문에도 catch와 finally를 붙일 수 있는데, 이때 실행 순서를 정확히 알아 두어야 합니다. 다음 프로그램은 본문에서 예외를 던지고, 자원의 close()와 catch, finally가 각각 언제 실행되는지 출력합니다.
public final class CloseBeforeCatch {
public static void main(String[] args) {
try (Resource resource = new Resource()) {
System.out.println("body");
throw new IllegalStateException("fail");
} catch (IllegalStateException error) {
System.out.println("catch=" + error.getMessage());
} finally {
System.out.println("finally");
}
}
private static final class Resource implements AutoCloseable {
@Override
public void close() { System.out.println("close"); }
}
}body
close
catch=fail
finally본문에서 예외가 났는데 catch보다 close가 먼저 출력되었습니다. try-with-resources 문에 붙은 catch와 finally는 자원을 모두 닫은 다음에 실행되기 때문입니다. 그래서 catch 블록에 들어왔을 때 자원은 이미 닫혀 있고, 앞에서 본 것처럼 자원 변수도 쓸 수 없습니다. 실패한 뒤에 자원으로 무언가를 더 해야 한다면 그 일은 try 블록 안에서 처리해야 합니다.
여러 자원은 연 순서의 반대로 닫힌다
괄호 안에는 자원을 세미콜론으로 구분해 여러 개 선언할 수 있습니다. 다음 프로그램은 connection을 먼저, channel을 나중에 엽니다. 각 자원은 생성자에서 open=이름을, close()에서 close=이름을 출력합니다.
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자원은 선언한 순서대로 만들어지고, 본문이 끝나면 그 반대 순서로 닫힙니다. 나중에 연 자원이 먼저 연 자원에 기대고 있을 수 있기 때문입니다. 채널은 연결 위에서 동작하므로, 연결을 먼저 닫으면 채널이 정리할 기회를 잃습니다. finally를 다룬 글의 연습 문제 1에서 try-finally를 두 겹으로 겹쳐 만든 순서를 여기서는 try 문 하나로 얻었습니다.
선언한 자원 가운데 하나를 만들다가 실패하면, 그 앞에서 이미 만들어진 자원만 닫힙니다. 예를 들어 channel의 생성자가 예외를 던지면 channel에는 아무것도 대입되지 않았으니 닫을 것이 없고, 먼저 만들어진 connection만 닫힙니다. 이 경우는 잠시 뒤 다른 실패와 함께 실행해 봅니다. 다음 그림은 자원 하나를 쓰는 AutoCloseableBoardClient와 자원 두 개를 쓰는 MultiResourceCloseOrder에서 자원을 확보하는 순서와 닫는 순서를 나란히 보여 줍니다.
확보한 순서와 닫는 순서를 구분하기
자원을 획득하고 작업한 뒤 선언 역순으로 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을 엽니다. |
|
|
| 작업 확보된 자원으로 실행합니다. |
|
|
| 자동 정리 단일을 닫고 다중은 역순으로 닫습니다. |
|
|
정리 중 실패는 억제된 예외로 남는다
이제 두 번째 문제를 보겠습니다. 다음 프로그램은 본문의 send()가 실패하고, 이어서 자동으로 호출되는 close()도 실패합니다. finally를 다룬 글에서 try-finally로 같은 상황을 만들었을 때는 close()의 예외만 남고 원래 예외는 사라졌습니다.
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");
}
}
}Exception in thread "main" java.lang.IllegalStateException: primary send failure
at TryWithResourcesPrimaryFailure$BoardChannel.send(TryWithResourcesPrimaryFailure.java:10)
at TryWithResourcesPrimaryFailure.main(TryWithResourcesPrimaryFailure.java:4)
Suppressed: java.lang.IllegalArgumentException: secondary close failure
at TryWithResourcesPrimaryFailure$BoardChannel.close(TryWithResourcesPrimaryFailure.java:15)
at TryWithResourcesPrimaryFailure.main(TryWithResourcesPrimaryFailure.java:3)이번에는 프로그램을 끝낸 예외가 본문의 primary send failure입니다. close()의 secondary close failure도 사라지지 않고, 그 아래 들여쓴 Suppressed: 줄에 붙어 있습니다. try-with-resources는 본문에서 예외가 나면 그것을 주 예외(primary exception)로 정해 두고 자원을 닫습니다. 닫는 도중 예외가 나도 주 예외를 바꾸지 않고, 새 예외를 주 예외의 억제된 예외(suppressed exception) 목록에 추가합니다. 억제된 예외에서 close를 호출한 위치가 try 줄인 3행으로 나오는 것은, 그 호출이 소스에 직접 쓴 코드가 아니라 컴파일러가 넣은 코드이기 때문입니다.
억제된 예외 목록은 Throwable의 getSuppressed()로 꺼낼 수 있으며, 추가된 순서대로 담긴 배열이 돌아옵니다. 원인(cause)과 헷갈리기 쉽지만 둘은 관계가 다릅니다. 원인은 '이 예외가 저 예외 때문에 생겼다'는 관계이고, 예외를 바꿔 던질 때 생성자에 넘겨서 연결합니다. 억제된 예외는 '주 예외가 전달되는 도중에 이런 실패도 함께 일어났다'는 기록입니다. close()의 실패는 send()의 실패 때문에 생긴 것이 아니므로, 원인이 아니라 억제된 예외로 붙습니다.
어느 예외가 주 예외가 되는가
주 예외가 언제나 본문의 예외인 것은 아닙니다. 다음 프로그램은 자원 두 개를 쓰면서 세 가지 경우를 차례로 실행합니다. body는 본문이 실패하는 경우, close는 본문은 성공하고 닫기만 실패하는 경우, init은 두 번째 자원의 생성자가 실패하는 경우입니다. 모든 자원의 close()는 close=이름을 출력한 뒤 예외를 던집니다. catch를 RuntimeException으로 넓게 쓴 것은 경우마다 주 예외의 타입이 다르기 때문입니다.
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-first출력이 길지만, 경우별로 주 예외와 억제된 예외만 뽑아 보면 다음 표와 같습니다.
| 경우 | 가장 먼저 일어난 실패 | 주 예외 | 억제된 예외 |
|---|---|---|---|
body | 본문의 work | IllegalStateException: work | close-second, close-first |
close | second의 close() | IllegalArgumentException: close-second | close-first |
init | second의 생성자 | IllegalStateException: init-second | close-first |
규칙은 하나입니다. 가장 먼저 일어난 실패가 주 예외가 되고, 그 뒤 자원을 닫다가 생긴 실패는 일어난 순서대로 억제된 예외에 쌓입니다. body에서는 본문의 예외가 가장 먼저 일어났으므로 주 예외이고, second와 first를 닫은 순서 그대로 두 close 실패가 붙었습니다. close에서는 본문이 성공했으므로, 역순으로 닫다가 처음 실패한 close-second가 주 예외가 됩니다. 이때도 first를 닫는 일은 멈추지 않고 이어지며, 그 실패는 close-second에 억제된 예외로 붙습니다.
init의 출력에는 open=second, work=done, close=second가 없습니다. second의 생성자가 예외를 던져 객체가 만들어지지 않았으므로 본문은 시작되지 않았고, 닫을 second도 없기 때문입니다. 먼저 만들어진 first만 닫히고, 그 close 실패가 생성자의 예외에 억제된 예외로 붙었습니다. 이 규칙들은 Java 언어 명세(JLS) 14.20.3.1절에 정해져 있습니다.
init의 경우에서 주의할 점이 하나 나옵니다. 생성자 안에서 파일 같은 실제 자원을 연 다음에 예외가 나면, 그 객체는 변수에 대입되지 않으므로 try-with-resources가 닫아 주지 못합니다. 그런 생성자는 예외를 던지기 전에 자기가 이미 연 자원을 직접 닫아야 합니다. 이 예제의 생성자는 출력만 할 뿐 실제 자원을 열지 않으므로 그런 정리가 필요 없습니다.
더 깊이: 컴파일러가 만드는 코드
try-with-resources는 컴파일러가 try, catch, finally로 풀어 쓰는 문법입니다. JLS 14.20.3.1절은 자원 하나짜리 기본 형태를 어떤 코드로 바꾸는지 정해 두었는데, 앞의 TryWithResourcesPrimaryFailure에 적용하면 대략 다음과 같습니다.
public final class ExpandedTryWithResources {
public static void main(String[] args) {
final BoardChannel channel = new BoardChannel();
Throwable primary = null;
try {
channel.send();
} catch (Throwable t) {
primary = t;
throw t;
} finally {
if (channel != null) {
if (primary != null) {
try {
channel.close();
} catch (Throwable suppressed) {
primary.addSuppressed(suppressed);
}
} else {
channel.close();
}
}
}
}
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");
}
}
}실행하면 앞 프로그램과 같은 모양으로 primary send failure가 주 예외로, secondary close failure가 Suppressed: 줄로 출력됩니다. 본문에서 예외가 나면 catch (Throwable t)가 그것을 primary에 기억해 두고 다시 던집니다. 그러면 finally가 close()를 호출할 때 주 예외가 있는지에 따라 다르게 처리합니다. 주 예외가 있으면 close()의 예외를 잡아 addSuppressed()로 주 예외에 붙이고, 없으면 close()의 예외가 그대로 밖으로 나가 주 예외가 됩니다. 자원이 null이면 close()를 호출하지 않는다는 것도 이 코드에서 알 수 있습니다.
main에 throws Throwable이 없는데도 throw t;가 컴파일되는 것은, 다시 대입하지 않은 catch 변수를 그대로 다시 던지면 컴파일러가 try 블록에서 실제로 나올 수 있는 예외만 던진다고 판단하기 때문입니다. 이 예제의 try 블록에서는 비검사 예외만 나올 수 있습니다. 자원이 여러 개면 이 구조가 자원마다 한 겹씩 겹쳐지므로, 안쪽에 있는 나중 자원이 먼저 닫힙니다. catch나 finally가 붙은 try-with-resources는 이렇게 만든 코드 전체를 다시 바깥 try 문으로 감싼 것이어서, 앞에서 본 것처럼 자원이 먼저 닫히고 나서 catch가 실행됩니다.
내가 연 자원만 닫는다
자원을 닫는 일은 그 자원을 연 쪽이 맡습니다. 메서드 인수로 받은 스트림처럼 다른 곳에서 열어 건네준 자원을 받은 쪽이 닫아 버리면, 그 자원을 계속 쓰려던 쪽이 곤란해집니다. 자바 9부터는 이미 있는 변수도 final이거나 사실상 final이라면 try (out)처럼 괄호 안에 그대로 쓸 수 있는데, 이 변수가 남에게서 받은 자원이라면 문제가 생깁니다. 다음 프로그램은 보고서를 출력하는 메서드가 인수로 받은 PrintStream을 try-with-resources로 닫습니다. 그런데 main이 건넨 것은 표준 출력인 System.out입니다.
import java.io.PrintStream;
public final class ClosingSharedStream {
public static void main(String[] args) {
printReport(System.out);
System.out.println("after-report");
}
private static void printReport(PrintStream out) {
try (out) {
out.println("report");
}
}
}reportmain의 after-report는 출력되지 않았는데 예외도 나지 않았습니다. printReport()가 끝나면서 System.out을 닫아 버렸기 때문입니다. PrintStream은 쓰기에 실패해도 예외를 던지지 않고 내부에 오류 표시만 남기므로, 그 뒤의 출력은 아무 소리 없이 사라집니다. 이 메서드는 System.out을 연 적이 없으니 닫아서도 안 됩니다. try (out)을 지우고 out.println("report");만 남기면 두 줄이 모두 출력됩니다.
반대로 자원을 열어서 돌려주는 메서드는 그 자원을 닫지 않고 그대로 돌려주며, 닫는 일은 받은 쪽으로 넘어갑니다. 표준 라이브러리의 Files.newBufferedReader(path)가 그런 예로, 이 메서드가 돌려준 BufferedReader는 호출한 쪽이 try-with-resources로 닫아야 합니다. 필드에 담아 오래 쓰는 자원도 조심해야 합니다. final 필드라면 try (this.client)처럼 괄호 안에 쓸 수는 있지만, 다른 메서드가 함께 쓰는 자원을 한 메서드가 닫아 버리면 안 됩니다. 이럴 때는 그 자원을 가진 객체가 AutoCloseable을 구현하게 하고, 그 객체를 만든 쪽이 다 쓴 뒤에 닫게 하거나, 프레임워크가 자원의 수명을 관리하게 합니다.
처리하지 못한 예외는 맨 바깥에서 한 번에 처리한다
비검사 예외를 다룬 글과 앞 글에서는 중간 계층이 대응할 수 없는 장애를 비검사 예외로 맨 위까지 올려 보냈습니다. 그러면 맨 위 어딘가에서는 그 예외를 받아야 합니다. 웹 서버라면 요청이 들어오는 입구가, 콘솔 프로그램이라면 main이 그 자리입니다. 이곳에 두는 공통 처리기는 두 가지 일을 합니다. 사용자에게는 안전한 안내만 보여 주고, 운영자에게는 원인을 찾는 데 필요한 정보를 남깁니다.
다음 프로그램에서는 handle()이 공통 처리기 역할을 합니다. 실행할 작업은 Command 인터페이스로 받는데, main은 람다로 '게시글을 발행하는 작업'을 만들어 요청 식별자 REQ-42와 함께 넘깁니다. 이 main은 시스템 오류가 나는 경우만 실행하며, 입력 오류와 성공하는 경우는 연습 문제 3에서 확인합니다.
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 예외 처리 질문BoardService.publish()가 BoardSystemException을 던지면, 예외는 람다와 command.run()을 거쳐 handle()로 올라옵니다. 첫 번째 catch는 사용자가 고칠 수 있는 입력 오류인 BoardValidationException만 받으므로 건너뛰고, 두 번째 catch (Exception error)가 예외를 받습니다. 이 블록이 먼저 출력하는 첫 줄이 사용자에게 보여 줄 안내입니다. 여기에는 시스템 오류라는 사실과 요청 식별자만 있고, 예외 메시지는 없습니다. 예외 메시지에는 파일 경로, SQL 쿼리, 내부 서버 주소처럼 바깥에 알려지면 안 되는 정보가 들어 있을 수 있기 때문입니다.
둘째 줄은 운영자를 위한 기록으로, 같은 요청 식별자와 함께 예외의 클래스 이름과 메시지를 남깁니다. 사용자가 'REQ-42에서 오류가 났다'고 알려 오면 운영자는 이 식별자로 기록을 찾을 수 있습니다. 다만 이 예제의 기록은 문자열 한 줄이라서 스택 트레이스도, Caused by:로 이어지는 원인도, 억제된 예외도 담고 있지 않습니다. 실제 서비스에서는 로깅 라이브러리에 예외 객체 자체를 넘겨야 이 정보가 모두 기록됩니다. 예를 들어 SLF4J에서는 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을 구현한 자원은 try-with-resources로 닫습니다. 파일과 스트림, 소켓, JDBC의 Connection, Statement, ResultSet이 모두 여기에 속합니다. 역순으로 닫는 일과 억제된 예외를 붙이는 일을 언어가 대신 해 주므로, 같은 일을 finally로 직접 쓰는 것보다 짧고 실수할 여지도 적습니다.
반면 close() 호출로 표현되지 않는 정리는 finally가 더 직접적입니다. 잠금을 풀거나, 잠시 바꿔 둔 플래그를 되돌리는 일이 그렇습니다. 예를 들어 ReentrantLock은 lock() 바로 뒤에 try를 열고 finally에서 unlock()을 호출하는 모양으로 쓰며, 이 내용은 ReentrantLock을 다루는 글에서 자세히 봅니다.
닫다가 실패했을 때 어떻게 할지도 정해 두어야 합니다. try-with-resources에서는 본문이 성공해도 close()가 실패하면 그 예외가 주 예외가 되어 작업 전체가 실패로 끝납니다. 파일에 쓰는 버퍼 스트림처럼 close()가 남은 데이터를 마저 내보내는 자원이라면 이것이 맞는 동작입니다. 닫기에 실패했다면 데이터가 다 저장되지 않았을 수 있기 때문입니다. 반대로 닫기 실패가 결과에 영향을 주지 않는 자원이라면 기록만 남기고 넘어가도록 정할 수도 있지만, 어느 쪽이든 빈 catch로 조용히 삼켜서는 안 됩니다.
이 장에서 본 내용을 한데 모으면 이렇습니다. catch를 많이 쓴다고 예외 처리가 잘된 것은 아닙니다. 실패한 뒤에도 객체의 상태가 올바르고 열었던 자원이 모두 닫혀 있어야 합니다. 복구할 수 있는 실패는 복구할 방법을 아는 계층이 처리하고, 복구할 수 없는 실패는 원인을 연결한 채 맨 바깥까지 보내 사용자 안내와 운영자 기록으로 나눕니다.
정리
AutoCloseable을 구현한 객체를try괄호 안에서 선언하면, 블록을 어떻게 빠져나가든close()가 자동으로 호출됩니다. 자원 변수는try블록 안에서만 쓸 수 있습니다.- 자원이 여러 개면 선언한 순서대로 만들어지고 반대 순서로 닫힙니다. 만들다가 실패한 자원은 닫지 않고, 그 앞에서 만들어진 자원만 닫습니다.
- try-with-resources 문에 붙은
catch와finally는 자원을 모두 닫은 다음에 실행됩니다. - 가장 먼저 일어난 실패가 주 예외가 되고, 자원을 닫다가 생긴 실패는 주 예외의 억제된 예외로 붙습니다. 억제된 예외는
getSuppressed()로 꺼낼 수 있고, 원인(cause)과는 다른 관계입니다. - 자원은 연 쪽이 닫습니다. 인수로 받은 자원은 닫지 않고, 자원을 돌려주는 메서드는 닫는 일을 받은 쪽에 넘깁니다.
- 처리하지 못한 예외는 맨 바깥의 공통 처리기에서 받습니다. 사용자에게는 안전한 안내를 보여 주고, 운영자 기록은 예외 객체 전체를 담아 한 번만 남깁니다.
연습 문제
- 자원
first와second를 이 순서로 선언하고, 본문과 두 자원의close()가 모두 실패하게 만드세요. 자원을 만드는 것은 성공하고, 본문은IllegalStateException("work")를, 각close()는IllegalArgumentException("close-" + 이름)을 던집니다.main은IllegalStateException을 잡아primary=메시지를 출력한 뒤, 억제된 예외를 배열 순서대로suppressed=메시지형식으로 출력합니다. - 1번 해답에서 본문의
throw문을System.out.println("work");로 바꾸고,first의close()만 실패하고second의close()는 정상적으로 끝나도록 바꾸면 어떻게 될지 예상하세요. 주 예외의 타입과 메시지, 억제된 예외의 개수,main의catch가 실행되는지를 답하세요. CommonExceptionBoundary의main에 아래 두 줄을 추가하면 출력이 어떻게 될지 예상하세요.
handle("REQ-43", () -> { throw new BoardValidationException("title"); });
handle("REQ-44", () -> System.out.println("published"));해설 보기
1번은 실패하는 close()를 가진 자원 두 개를 선언하고, 본문에서 바로 예외를 던지면 됩니다.
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본문의 work 예외가 가장 먼저 일어났으므로 주 예외가 됩니다. 이어서 자원을 역순으로 닫으므로 second의 close()가 먼저 실패해 첫 번째 억제된 예외가 되고, first의 실패가 두 번째로 붙습니다. 그래서 getSuppressed() 배열의 순서는 자원을 닫은 순서와 같습니다. 주 예외는 끝까지 바뀌지 않으므로, 장애가 처음 시작된 곳과 정리 중에 생긴 문제를 한 번에 볼 수 있습니다.
본문 실패를 유지하고 정리 실패를 역순으로 덧붙이기
본문 주 예외 뒤 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와 두 추가 실패를 출력합니다. |
|
|
2번에서는 본문이 성공하므로 work가 출력된 뒤 자원을 역순으로 닫습니다. second는 정상적으로 닫히고, first의 close()가 던진 IllegalArgumentException: close-first가 처음 일어난 실패이므로 주 예외가 됩니다. 다른 실패가 없으니 억제된 예외는 0개입니다. 그런데 main의 catch는 IllegalStateException만 받으므로 이 예외를 잡지 못합니다. 결국 primary= 줄은 나오지 않고, Exception in thread "main" java.lang.IllegalArgumentException: close-first로 시작하는 스택 트레이스가 표준 오류에 출력되며 프로그램이 끝납니다. 이 결과를 primary= 형식으로 보려면 catch의 타입을 RuntimeException처럼 넓혀야 합니다.
3번의 출력은 다음과 같습니다.
status=system-error,request=REQ-42
board=REQ-42:BoardSystemException:database offline for 예외 처리 질문
status=bad-request,field=title
published
status=ok처음 두 줄은 본문과 같습니다. REQ-43의 람다는 BoardValidationException을 던지므로 첫 번째 catch가 받아 status=bad-request,field=title을 출력합니다. 사용자가 고칠 수 있는 입력 오류라서 운영자 기록은 남기지 않습니다. REQ-44의 람다는 published를 출력하고 정상적으로 끝나므로, command.run() 다음 줄이 실행되어 status=ok가 출력됩니다. REQ-43의 람다에만 중괄호가 있는 것은 throw가 값이 아니라 문장이어서 블록 안에 써야 하기 때문입니다.
다음 글
이것으로 예외를 다루는 12장을 마칩니다. 다음 장에서는 제네릭을 배웁니다. Object로 값을 받는 클래스에서 생기는 형변환 문제를 확인하고, 타입 매개변수를 선언한 제네릭 클래스로 잘못된 값을 컴파일 단계에서 막는 방법을 봅니다.