자바 백엔드 입문 7편. try-catch·throw·체크드 예외와 언체크드 예외의 정확한 차이까지 자바 예외 처리를 화재 경보 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 7편이에요. 이번 7편은 자바 코드를 안정적으로 굴리는 핵심 — 자바 예외 처리 를 풀어 가요.
예외 처리가 헷갈리는 이유
자바 코드 보면 try·catch·finally·throw·throws 같은 키워드가 등장해요. 또 "체크드 예외"·"언체크드 예외" 라는 용어. 그리고 어떤 예외는 "꼭 잡아야 하고" 어떤 건 "안 잡아도 되고" — 룰이 복잡해요.
이 글에서는 화재 경보 비유로 풀어요. 예외 발생 = "화재 경보 울림", try-catch = "경보 잡고 진화", throw = "경보 직접 울리기". 끝까지 따라오시면 자바 예외 처리의 전체 그림이 한 번에 잡혀요.
예외란 무엇인가
코드 실행 중 "비정상 상황" 이 발생할 때 — 자바는 예외(Exception) 라는 객체를 던져요.
int[] arr = new int[3];
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException 발생
예외 발생 시 — 기본적으로 프로그램 종료. 단 try-catch 로 잡으면 "진화" 가능.
try-catch — 기본 구조
try {
int result = 10 / 0; // ArithmeticException 발생
} catch (ArithmeticException e) {
System.out.println("0으로 못 나눠요: " + e.getMessage());
}
System.out.println("프로그램 계속"); // 실행됨
3단계:
1. try 블록 — 예외 발생 가능 영역
2. catch 블록 — 예외 타입별 처리
3. 예외 잡힘 → catch 실행 → 이후 코드 정상 실행
여러 catch — 예외 종류별
try {
file = openFile("data.txt");
int n = Integer.parseInt(file.readLine());
} catch (IOException e) { // 파일 관련
log.error("파일 읽기 실패", e);
} catch (NumberFormatException e) { // 숫자 파싱 실패
log.error("숫자 형식 아님", e);
} catch (Exception e) { // 그 외 모든 예외 (최후 안전망)
log.error("알 수 없는 에러", e);
}
위에서 아래로 매칭 — 첫 매치 catch만 실행. 구체 → 일반 순서로 박아야 함. Exception e 를 맨 위에 박으면 그 아래 catch는 영영 도달 불가.
finally — 무조건 실행
FileReader reader = null;
try {
reader = new FileReader("data.txt");
// 작업
} catch (IOException e) {
log.error("파일 에러", e);
} finally {
if (reader != null) {
reader.close(); // 예외 났든 안 났든 무조건 닫기
}
}
finally = "try 통과·예외 발생 모두 무조건 마지막에 실행". 자원 정리(파일·DB 연결 close)에 표준 패턴. 단 — 더 깔끔한 try-with-resources가 있어요.
try-with-resources — 자동 close (자바 7+)
try (FileReader reader = new FileReader("data.txt")) {
// 작업
} catch (IOException e) {
log.error("파일 에러", e);
}
// reader.close() 자동 호출
괄호 안에 박힌 자원은 — 블록 끝나면 자동으로 close() 호출. finally 박을 필요 없음. AutoCloseable 인터페이스 구현한 객체(FileReader·BufferedReader·DB Connection 등)면 OK.
한국 회사 표준 = try-with-resources. finally 명시는 옛 스타일.
throw — 예외 직접 던지기
public void withdraw(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("금액은 양수여야 합니다");
}
if (amount > balance) {
throw new IllegalStateException("잔액 부족");
}
balance -= amount;
}
throw = "내가 예외를 직접 발생". 비즈니스 룰 위반 시 자주.
throws — 메서드 시그니처에 선언
public String readFile(String path) throws IOException { // ← throws 선언
FileReader reader = new FileReader(path); // IOException 발생 가능
return reader.readLine();
}
// 호출자
public void process() {
try {
String content = readFile("data.txt"); // 무조건 try-catch
} catch (IOException e) {
log.error("실패", e);
}
}
throws IOException = "이 메서드는 IOException 던질 수 있으니 호출자가 잡아라". 체크드 예외일 때만 필요.
체크드 vs 언체크드 — 가장 헷갈리는 부분
자바 예외는 두 종류.
체크드 예외 (Checked Exception)
Exception직접 상속 (RuntimeException 제외)- 예:
IOException·SQLException·InterruptedException - 컴파일러가 강제 — 메서드에
throws선언 또는try-catch필수 - 안 잡으면 컴파일 에러
- 의미 = "외부 자원 관련 — 처리해야 정상 동작 가능"
언체크드 예외 (Unchecked Exception)
RuntimeException상속- 예:
NullPointerException·ArrayIndexOutOfBoundsException·IllegalArgumentException - 컴파일러가 강제 안 함
try-catch안 해도 컴파일 통과 (런타임에 폭발할 뿐)- 의미 = "프로그래머 실수 — 코드 자체를 고쳐야 함"
사용 룰
| 상황 | 추천 |
|---|---|
| 외부 자원 (파일·DB·네트워크) | 체크드 예외 (또는 처리 후 RuntimeException으로 래핑) |
| 잘못된 매개변수 | IllegalArgumentException (언체크드) |
| 잘못된 상태 | IllegalStateException (언체크드) |
| 비즈니스 룰 위반 | 커스텀 RuntimeException (언체크드) |
| Spring 환경 | 거의 99% 언체크드 |
모던 자바 흐름 = 언체크드 우세. 체크드 예외는 "안 잡으면 컴파일 통과 안 됨" 강제가 오히려 코드 더럽힘 (throws SQLException, IOException, ... 줄줄). Spring도 거의 모든 예외를 RuntimeException 자식으로 설계.
커스텀 예외 만들기
비즈니스 도메인 예외는 직접 정의.
public class InsufficientBalanceException extends RuntimeException { // 언체크드
private final long currentBalance;
public InsufficientBalanceException(long currentBalance) {
super("잔액 부족: 현재 " + currentBalance);
this.currentBalance = currentBalance;
}
public long getCurrentBalance() {
return currentBalance;
}
}
// 사용
public void withdraw(long amount) {
if (amount > balance) {
throw new InsufficientBalanceException(balance);
}
balance -= amount;
}
비즈니스 의미가 명확한 예외 — 디버깅·로그 한결 깔끔.
예외 체이닝 — 원인 추적
원인 예외를 감싸 다시 던질 때:
try {
repository.save(order);
} catch (DataAccessException e) {
throw new OrderSaveFailedException("주문 저장 실패", e); // ← 원인 예외 전달
}
new OrderSaveFailedException(msg, cause) 두 번째 인자로 원인 예외 전달. 로그에 원본 stack trace 보존돼서 디버깅 한결 쉬워요.
Spring에서 예외 처리 — @ControllerAdvice
이 시리즈 30편 @ExceptionHandler 에서 깊이 — 컨트롤러 전반의 예외를 한 곳에 모아 처리.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InsufficientBalanceException.class)
public ResponseEntity<ErrorResponse> handle(InsufficientBalanceException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("INSUFFICIENT_BALANCE", e.getMessage()));
}
}
비즈니스 예외를 도메인 곳곳에 던지면 — 공통 핸들러가 HTTP 응답으로 변환. Spring 백엔드의 표준.
예외를 잡고 아무 일도 안 하는 것이 가장 위험. catch (Exception e) { } 같은 빈 catch가 박혀 있으면 — 시스템 에러가 묵살돼 디버깅 불가. 최소한 log.error("msg", e) 박기.
한 줄 정리 — 자바 예외 처리 = try-catch-finally + throw/throws + 체크드 vs 언체크드 구분. 모던 자바는 언체크드 우세. try-with-resources 표준. Spring은 @ControllerAdvice로 통합 처리.
시험 직전 한 번 더 — 자바 예외 처리 입문자가 매번 헷갈리는 것
- 예외 = 비정상 상황 발생 시 던지는 객체
- try-catch = 기본 처리 구조
- catch 순서 = 구체 → 일반 (Exception을 위에 박으면 X)
- finally = 무조건 마지막 실행 (자원 정리)
- try-with-resources = 자동 close, 한국 회사 표준 (자바 7+)
- throw = 예외 직접 던지기
- throws = 메서드 시그니처에 "이 예외 던진다" 선언
- 체크드 예외 → throws 또는 try-catch 강제
- 언체크드 예외 → 컴파일러 강제 X
- 체크드 =
IOException·SQLException·InterruptedException - 언체크드 =
NullPointerException·IllegalArgumentException·RuntimeException자식 - 모던 자바 = 언체크드 우세 — Spring 99% 언체크드
- 커스텀 예외 =
extends RuntimeException - 예외 체이닝 =
new XxxException(msg, cause)로 원인 보존 - 빈 catch 절대 금지 — 최소
log.error("msg", e) e.getMessage()= 예외 메시지e.printStackTrace()= 디버깅용 (운영은 log 사용)e.getCause()= 원인 예외- Spring @ControllerAdvice = 전역 예외 핸들러
- 컨트롤러 단에서 예외 → HTTP 응답으로 변환
- 자바 백엔드 = 예외 잘 다루는 일
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
다음 글: