return이 먼저 평가되고 finally가 실행된 뒤 반환되며, finally의 return은 예외를 삼킨다는 것을 보여준다.
public class Main {
static int normalReturn() {
int x = 1;
try {
System.out.println("try");
return x; // 반환값 1이 여기서 확정
} finally {
x = 99; // 반환값에 영향 없음
System.out.println("finally (x=" + x + ")");
}
}
@SuppressWarnings("finally")
static int finallyReturn() {
try {
throw new IllegalStateException("사라질 예외");
} finally {
return -1; // 예외를 삼키고 -1 반환 — 절대 하지 말 것
}
}
public static void main(String[] args) {
System.out.println("결과: " + normalReturn());
System.out.println("결과: " + finallyReturn());
// 출력:
// try
// finally (x=99)
// 결과: 1
// 결과: -1
}
}여러 자원이 역순으로 닫히고, 본문 예외와 close 예외가 모두 보존됨을 보여준다.
class Resource implements AutoCloseable {
private final String name;
private final boolean failOnClose;
Resource(String name, boolean failOnClose) {
this.name = name;
this.failOnClose = failOnClose;
System.out.println(name + " 열림");
}
@Override
public void close() {
System.out.println(name + " 닫힘");
if (failOnClose) throw new IllegalStateException(name + " close 실패");
}
}
public class Main {
public static void main(String[] args) {
try (Resource db = new Resource("DB", true);
Resource file = new Resource("FILE", false)) {
System.out.println("작업 중...");
throw new RuntimeException("본문 예외");
} catch (RuntimeException e) {
System.out.println("잡힘: " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println(" suppressed: " + s.getMessage());
}
}
// 출력:
// DB 열림
// FILE 열림
// 작업 중...
// FILE 닫힘
// DB 닫힘
// 잡힘: 본문 예외
// suppressed: DB close 실패
}
}에러 코드 enum을 가진 추상 부모, HTTP 상태 단위의 중간 계층, 문맥 정보를 담은 구체 예외를 설계한다.
enum ErrorCode {
ORDER_NOT_FOUND(404, "주문을 찾을 수 없습니다"),
INSUFFICIENT_STOCK(400, "재고가 부족합니다"),
PAYMENT_DECLINED(502, "결제가 거절되었습니다");
final int status;
final String defaultMessage;
ErrorCode(int status, String defaultMessage) { this.status = status; this.defaultMessage = defaultMessage; }
}
abstract class BusinessException extends RuntimeException {
private final ErrorCode code;
protected BusinessException(ErrorCode code, String detail) {
super(code.defaultMessage + " (" + detail + ")");
this.code = code;
}
protected BusinessException(ErrorCode code, String detail, Throwable cause) {
super(code.defaultMessage + " (" + detail + ")", cause);
this.code = code;
}
public ErrorCode getCode() { return code; }
}
class OrderNotFoundException extends BusinessException {
OrderNotFoundException(String orderId) {
super(ErrorCode.ORDER_NOT_FOUND, "orderId=" + orderId);
}
}
class InsufficientStockException extends BusinessException {
InsufficientStockException(String sku, int requested, int available) {
super(ErrorCode.INSUFFICIENT_STOCK, "sku=" + sku + " 요청=" + requested + " 재고=" + available);
}
}
class PaymentDeclinedException extends BusinessException {
PaymentDeclinedException(long amount, Throwable cause) {
super(ErrorCode.PAYMENT_DECLINED, "amount=" + amount, cause);
}
}
public class Main {
static void handle(Runnable action) {
try {
action.run();
} catch (BusinessException e) { // 글로벌 핸들러: 부모 하나로 전부 잡음
System.out.println("[" + e.getCode().status + "] " + e.getCode() + ": " + e.getMessage()
+ (e.getCause() != null ? " / cause=" + e.getCause().getMessage() : ""));
}
}
public static void main(String[] args) {
handle(() -> { throw new OrderNotFoundException("O-404"); });
handle(() -> { throw new InsufficientStockException("SKU-1", 5, 2); });
handle(() -> { throw new PaymentDeclinedException(30_000, new java.io.IOException("PG timeout")); });
// 출력:
// [404] ORDER_NOT_FOUND: 주문을 찾을 수 없습니다 (orderId=O-404)
// [400] INSUFFICIENT_STOCK: 재고가 부족합니다 (sku=SKU-1 요청=5 재고=2)
// [502] PAYMENT_DECLINED: 결제가 거절되었습니다 (amount=30000) / cause=PG timeout
}
}리포지토리의 checked 예외를 서비스 계층 예외로 전환하면서 cause를 보존하고, Caused by: 체인이 어떻게 남는지 보여준다.
import java.sql.SQLException;
class DataAccessException extends RuntimeException {
DataAccessException(String message, Throwable cause) { super(message, cause); }
}
class OrderRepository {
String findById(String id) throws SQLException { // 하위 계층: checked
throw new SQLException("Connection refused: db-primary:5432");
}
}
class OrderService {
private final OrderRepository repo = new OrderRepository();
String getOrder(String id) {
try {
return repo.findById(id);
} catch (SQLException e) {
throw new DataAccessException("주문 조회 실패: orderId=" + id, e); // 전환 + 체이닝
}
}
}
public class Main {
public static void main(String[] args) {
try {
new OrderService().getOrder("O-1");
} catch (DataAccessException e) {
System.out.println(e.getMessage());
System.out.println("원인: " + e.getCause().getClass().getSimpleName() + " - " + e.getCause().getMessage());
}
// 출력:
// 주문 조회 실패: orderId=O-1
// 원인: SQLException - Connection refused: db-primary:5432
//
// (e.printStackTrace()를 하면 아래처럼 Caused by: 가 붙어 나온다)
// DataAccessException: 주문 조회 실패: orderId=O-1
// at OrderService.getOrder(Main.java:...)
// Caused by: java.sql.SQLException: Connection refused: db-primary:5432
// at OrderRepository.findById(Main.java:...)
}
}예외로 루프를 끝내는 코드와 정상 분기 코드의 실행 시간을 비교해 예외 생성이 얼마나 비싼지 보여준다.
public class Main {
static class EndOfArray extends RuntimeException {
EndOfArray() { super("end"); } // 매번 새 객체 + 스택트레이스 캡처
}
static long withException(int[] arr) {
long sum = 0;
try {
for (int i = 0; ; i++) {
if (i == arr.length) throw new EndOfArray(); // 끝을 예외로 알림
sum += arr[i];
}
} catch (EndOfArray e) {
return sum;
}
}
static long withBranch(int[] arr) {
long sum = 0;
for (int i = 0; i < arr.length; i++) sum += arr[i];
return sum;
}
public static void main(String[] args) {
int[] arr = new int[10];
int loops = 200_000;
long t0 = System.nanoTime();
for (int i = 0; i < loops; i++) withException(arr);
long exceptionMs = (System.nanoTime() - t0) / 1_000_000;
t0 = System.nanoTime();
for (int i = 0; i < loops; i++) withBranch(arr);
long branchMs = (System.nanoTime() - t0) / 1_000_000;
System.out.println("예외 방식: " + exceptionMs + "ms");
System.out.println("분기 방식: " + branchMs + "ms");
// 출력 (환경마다 다름, 수십 배 차이가 일반적):
// 예외 방식: 180ms
// 분기 방식: 2ms
}
}예외 객체를 만들 때 fillInStackTrace()가 호출 스택 전체를 캡처한다. 이 비용이 예외가 비싼 주된 이유다.
참고로 arr[i]의 ArrayIndexOutOfBoundsException 같은 JVM 내장 예외는 HotSpot이 "자주 발생하는 곳"에서 스택트레이스 없는 객체를 재사용하는 최적화(OmitStackTraceInFastThrow)를 해서 벤치마크 차이가 작게 나올 수 있다. 그래서 위 예제는 직접 new로 던진다.
운영에서 이 최적화가 걸리면 스택트레이스가 사라져 원인 추적이 어려워지므로, 이것 역시 예외를 흐름 제어에 쓰면 안 되는 이유다.