자바 예외 처리 완전 정리
자바를 공부하다 보면 문법보다 더 먼저 부딪히는 것이 있다. 바로 에러다. 분명 문법은 맞는 것 같은데 실행이 안 되거나, 실행은 되는데 중간에 갑자기 프로그램이 멈추는 경우가 있다. 이럴 때 자주 등장하는 개념이 바로 예외 처리(Exception Handling)다.
예외 처리는 단순히 에러를 피하는 기술이 아니다. 프로그램이 예상하지 못한 상황을 만났을 때 어떻게 반응할지를 미리 정해 두는 방식이다. 즉, 정상 흐름만 생각하는 것이 아니라 실패할 수 있는 상황까지 고려해서 코드를 짜는 태도와 연결된다. 그래서 자바에서 예외 처리는 문법 파트이면서도, 동시에 프로그램의 안정성을 만드는 핵심 요소라고 볼 수 있다.
1. 예외란 무엇인가
예외는 프로그램 실행 중 발생하는 문제 상황이다. 예를 들어 0으로 나누거나, 존재하지 않는 파일을 읽으려 하거나, 배열 범위를 벗어난 인덱스에 접근하거나, 문자열을 숫자로 바꾸려는데 숫자가 아닌 값이 들어온 경우 등이 있다.
이런 상황은 코드 작성 시점에는 괜찮아 보여도 실행 도중 실제 데이터나 사용자 입력에 따라 발생할 수 있다. 즉, 예외는 단순 오타와는 다르다. 컴파일 자체가 안 되는 문법 오류와 달리, 예외는 실행 중에 터지는 문제라는 점이 중요하다.
2. 에러와 예외는 같은가
처음 공부할 때 에러와 예외를 같은 말처럼 쓰기 쉽다. 하지만 자바에서는 조금 다르게 본다.
Error는 시스템 수준에서 발생하는 심각한 문제에 가깝다.
예를 들어 JVM 자체의 문제나 메모리 부족 같은 상황이다.
이런 것은 보통 프로그램 코드에서 복구하기 어렵다.
반면 Exception은 코드로 어느 정도 처리할 수 있는 문제다. 예를 들어 파일이 없을 수 있으니 다른 경로를 안내하거나, 숫자 입력이 잘못되면 다시 입력받게 만들 수 있다. 즉, 개발자가 대응할 수 있는 문제라는 점에서 예외 처리가 중요해진다.
3. 예외가 왜 필요한가
예외 처리가 없다면 프로그램은 작은 문제 하나만 만나도 그대로 종료될 수 있다. 예를 들어 사용자가 나이를 입력해야 하는데 "스물다섯"이라고 적었다고 하자. 이때 숫자 변환이 실패하면 프로그램 전체가 그냥 끝나버리면 안 된다. 잘못된 입력이라는 사실을 알려주고 다시 입력받게 해야 한다.
즉, 예외 처리는 프로그램을 더 안전하게 만들고, 사용자 경험도 훨씬 낫게 만든다. 무조건 "문제가 생기지 않게 하자"보다 "문제가 생겨도 무너지지 않게 하자"에 가깝다.
4. 자바의 예외 계층 구조
자바의 예외는 클래스 구조로 되어 있다. 가장 위에는 Throwable이 있고, 그 아래에 Error와 Exception이 있다. 그리고 Exception 아래에 우리가 자주 보는 여러 예외 클래스가 존재한다.
대표적으로 많이 보는 예외는 다음과 같다.
NullPointerException
ArrayIndexOutOfBoundsException
NumberFormatException
ArithmeticException
IOException
SQLException
이 이름들을 외우는 것도 필요하지만, 더 중요한 것은 어떤 상황에서 왜 발생하는지 이해하는 것이다.
5. Checked Exception과 Unchecked Exception
자바 예외는 크게 두 종류로 나눌 수 있다. 바로 Checked Exception과 Unchecked Exception이다. 이 구분은 매우 중요하다.
5-1. Checked Exception
컴파일 단계에서 반드시 처리하라고 요구하는 예외다. 대표적으로 IOException, SQLException 등이 있다. 이런 예외는 try-catch로 처리하거나, 메서드에 throws로 넘기지 않으면 컴파일 자체가 되지 않는다.
5-2. Unchecked Exception
RuntimeException 계열 예외다. 컴파일러가 강제로 처리하라고 하지는 않는다. 대표적으로 NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException, NumberFormatException 등이 있다.
Unchecked Exception은 "안 잡아도 된다"가 아니라 대부분 개발자의 실수나 잘못된 사용에서 많이 발생한다는 뜻에 가깝다. 그래서 오히려 더 자주 보게 된다.
6. 가장 기본적인 try-catch
예외 처리를 시작할 때 가장 먼저 배우는 문법이 try-catch다. 예외가 발생할 수 있는 코드를 try 블록 안에 넣고, 문제가 생기면 catch 블록에서 잡아서 처리한다.
public class Main {
public static void main(String[] args) {
try {
int result = 10 / 0;
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없습니다.");
}
System.out.println("프로그램 종료");
}
}
위 코드에서는 10 / 0 때문에 ArithmeticException이 발생한다. 예외 처리가 없다면 프로그램은 바로 종료된다. 하지만 catch에서 잡아 주었기 때문에 문구를 출력하고 아래 코드까지 계속 실행할 수 있다.
7. 예외 객체를 잡는다는 의미
catch 블록에서 받는 e는 단순한 변수 하나가 아니다. 예외 상황에 대한 정보를 담고 있는 객체다. 예를 들어 어떤 예외가 발생했는지, 메시지가 무엇인지, 어느 줄에서 문제가 났는지 같은 정보를 가지고 있다.
public class Main {
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (NumberFormatException e) {
System.out.println("예외 메시지: " + e.getMessage());
e.printStackTrace();
}
}
}
getMessage()는 예외 메시지를 확인할 때 사용하고, printStackTrace()는 어느 위치에서 문제가 발생했는지 추적할 때 자주 사용한다. 개발 중 디버깅할 때 매우 중요하다.
8. finally는 언제 쓰는가
finally는 예외 발생 여부와 관계없이 마지막에 반드시 실행해야 하는 코드를 넣는 곳이다. 예를 들어 파일 닫기, DB 연결 해제, 스트림 종료 같은 자원 정리에 자주 사용한다.
public class Main {
public static void main(String[] args) {
try {
System.out.println("작업 시작");
int num = 10 / 2;
System.out.println(num);
} catch (ArithmeticException e) {
System.out.println("예외 발생");
} finally {
System.out.println("무조건 실행");
}
}
}
예외가 발생하든 안 하든 finally는 실행된다. 그래서 정리 작업을 넣기에 적합하다. 다만 최근에는 try-with-resources 문법이 더 자주 쓰이기도 한다.
9. 여러 예외를 처리하는 방법
하나의 코드에서 여러 종류의 예외가 발생할 수 있다. 이럴 때는 catch를 여러 개 둘 수 있다.
public class Main {
public static void main(String[] args) {
try {
String str = null;
System.out.println(str.length());
int num = Integer.parseInt("abc");
System.out.println(num);
} catch (NullPointerException e) {
System.out.println("null 값을 참조했습니다.");
} catch (NumberFormatException e) {
System.out.println("숫자로 변환할 수 없습니다.");
}
}
}
중요한 점은 catch 순서다. 부모 예외를 먼저 쓰면 자식 예외는 도달할 수 없어서 문제가 된다. 즉, 더 구체적인 예외를 먼저 쓰고, 더 넓은 예외를 뒤에 써야 한다.
10. Exception을 한 번에 잡는 것은 괜찮을까
가끔 모든 예외를 한 번에 잡기 위해 Exception으로 처리하는 경우가 있다. 문법상 가능은 하지만 항상 좋은 방식은 아니다.
try {
// 예외 발생 가능 코드
} catch (Exception e) {
System.out.println("예외가 발생했습니다.");
}
이렇게 하면 편해 보이지만, 어떤 예외가 발생했는지 명확하지 않아질 수 있다. 디버깅도 어려워지고, 상황별 대응도 힘들어진다. 그래서 정말 공통 처리 목적이 아니라면 가능한 한 구체적인 예외를 잡는 습관이 좋다.
11. throws는 무엇인가
예외를 현재 메서드에서 직접 처리하지 않고, 이 메서드를 호출한 쪽으로 넘길 수도 있다. 이때 사용하는 것이 throws다.
import java.io.IOException;
public class Main {
public static void readFile() throws IOException {
throw new IOException("파일 읽기 실패");
}
public static void main(String[] args) {
try {
readFile();
} catch (IOException e) {
System.out.println("main에서 예외 처리: " + e.getMessage());
}
}
}
readFile 메서드는 IOException을 직접 처리하지 않고 넘겼다. 그리고 main에서 try-catch로 받아서 처리했다. 즉, throws는 "여기서 해결 안 할 테니 호출한 쪽에서 처리해라"라는 의미에 가깝다.
12. throw와 throws의 차이
처음 자바를 배울 때 throw와 throws를 자주 헷갈린다. 둘은 완전히 다르다.
throw는 예외를 실제로 발생시키는 키워드다.
throws는 메서드가 어떤 예외를 넘길 수 있는지 선언하는 키워드다.
public class Main {
public static void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("나이는 음수가 될 수 없습니다.");
}
System.out.println("입력된 나이: " + age);
}
public static void main(String[] args) {
checkAge(-1);
}
}
위 코드는 age가 0보다 작으면 예외를 직접 발생시킨다. 이처럼 프로그램에서 특정 조건을 잘못된 상황으로 판단할 때 개발자가 의도적으로 예외를 던질 수 있다.
13. 사용자 정의 예외
기본 제공 예외만으로는 의미 전달이 부족한 경우가 있다. 예를 들어 잔액 부족, 로그인 실패 횟수 초과, 잘못된 주문 상태 같은 것은 프로그램의 도메인에 맞는 예외를 직접 만드는 것이 더 읽기 좋다.
class InsufficientBalanceException extends Exception {
public InsufficientBalanceException(String message) {
super(message);
}
}
public class Main {
public static void withdraw(int balance, int amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException("잔액이 부족합니다.");
}
System.out.println("출금 완료");
}
public static void main(String[] args) {
try {
withdraw(1000, 2000);
} catch (InsufficientBalanceException e) {
System.out.println(e.getMessage());
}
}
}
이렇게 하면 단순히 Exception이라고 쓰는 것보다 왜 예외가 발생했는지 더 분명하게 전달할 수 있다. 프로젝트 규모가 커질수록 이런 방식은 코드 가독성에 큰 도움이 된다.
14. RuntimeException은 언제 사용하는가
사용자 정의 예외를 만들 때 Exception을 상속할지 RuntimeException을 상속할지도 고민하게 된다.
Exception을 상속하면 Checked Exception이 된다. 즉, 호출하는 쪽에서 반드시 처리하거나 throws로 넘겨야 한다. 반면 RuntimeException을 상속하면 Unchecked Exception이 된다. 즉, 강제 처리는 아니지만 실행 중 문제로 나타난다.
보통 복구 가능한 상황, 호출하는 쪽에서 처리해 줘야 의미가 있는 상황이면 Checked Exception을 고민할 수 있다. 반대로 개발자의 잘못된 호출, 잘못된 상태, 사전에 막아야 하는 논리 오류라면 RuntimeException이 더 자연스러운 경우도 많다.
15. 예외 메시지는 왜 중요할까
예외를 던질 때 메시지를 대충 쓰거나 비워 두는 경우가 있다. 하지만 메시지는 문제 원인을 추적할 때 매우 중요하다. 나중에 로그를 보거나 유지보수할 때 "왜 실패했는지"가 드러나야 한다.
예를 들어 그냥 "에러 발생"보다 "회원 번호가 존재하지 않아 조회할 수 없습니다." 같이 구체적으로 적는 편이 훨씬 좋다.
16. 실무 느낌으로 보는 예외 처리 흐름
예를 들어 게시글 상세 조회 기능을 만든다고 해 보자. 게시글 번호를 받아 DB에서 찾았는데 해당 번호가 없을 수 있다. 이때 그냥 null을 반환하면 나중에 다른 곳에서 NullPointerException이 터질 수 있다.
대신 "게시글을 찾을 수 없음"이라는 예외를 명확하게 던지면 컨트롤러나 서비스 레이어에서 이를 잡아 적절한 화면이나 메시지로 바꿔 줄 수 있다. 즉, 예외 처리는 단순히 try-catch 한 줄이 아니라 프로그램 흐름을 설계하는 방식과도 연결된다.
17. 자주 하는 실수
17-1. 예외를 무조건 숨기는 경우
catch는 했지만 아무 처리도 하지 않는 경우가 있다. 이렇게 하면 문제는 발생했는데도 원인을 찾기 매우 어려워진다.
try {
// 코드
} catch (Exception e) {
}
이건 정말 조심해야 한다. 최소한 로그라도 남기거나, 왜 무시해도 되는 상황인지 분명해야 한다.
17-2. 무조건 Exception으로만 잡는 경우
편하다고 모든 예외를 통으로 처리하면 상황별 대응이 어려워진다. 가능하면 구체적인 예외를 보고 처리하는 습관이 좋다.
17-3. 예외를 남발하는 경우
모든 분기마다 예외를 던지면 코드가 오히려 무거워질 수 있다. 예외는 정말 비정상 상황일 때 사용하는 것이 좋다. 단순 조건문으로 충분한 경우까지 전부 예외로 처리할 필요는 없다.
17-4. 예외 메시지를 대충 쓰는 경우
나중에 본인이 다시 봐도 이해 안 되는 메시지는 도움이 안 된다. 누가 봐도 원인을 알 수 있게 작성하는 것이 중요하다.
18. 예외 처리와 디버깅
예외가 발생하면 겁먹기 쉽지만, 사실 예외 메시지와 스택 트레이스는 문제를 찾는 단서다. 어느 클래스, 어느 메서드, 어느 줄에서 문제가 났는지를 보여 주기 때문이다. 그래서 예외가 났다고 바로 코드를 다 뜯어고치기보다, 메시지와 발생 위치를 먼저 차분히 보는 습관이 중요하다.
특히 NullPointerException은 "어디선가 null이 들어왔다"는 뜻이므로 해당 줄만 보는 것이 아니라 그 값이 어디서 만들어지고 어디서 전달됐는지 추적해야 해결된다. 즉, 예외는 증상이고 원인은 그 앞 단계에 숨어 있는 경우가 많다.
19. 예외 처리를 공부할 때 꼭 같이 봐야 하는 것
예외 처리는 단독으로 끝나는 주제가 아니다. 입력값 검증, 메서드 설계, 객체 상태 관리, 로그 처리, 파일/DB 자원 관리와 연결된다.
예를 들어 null이 들어오지 않게 설계하면 NullPointerException 자체를 줄일 수 있다. 입력값 검증을 앞에서 잘하면 NumberFormatException도 줄일 수 있다. 즉, 좋은 예외 처리는 예외를 잘 잡는 것만이 아니라 예외가 덜 발생하게 구조를 짜는 것까지 포함한다.
20. 마무리
자바 예외 처리는 단순히 try-catch 문법을 외우는 파트가 아니다. 프로그램이 실패할 수 있다는 사실을 인정하고, 그 실패를 어떻게 다룰지 정하는 과정이다. "문제가 생기지 않는 코드"를 만드는 기술이 아니라, "문제가 생겨도 무너지지 않는 코드"를 만드는 기술이다.
'Computer Science > Java' 카테고리의 다른 글
| [Java] 자바 컬렉션 프레임워크 완전 정리 (0) | 2026.03.14 |
|---|---|
| [JAVA] 다형성 (0) | 2026.02.15 |
| [JAVA] 10 컬렉션(List / Set / Map)과 제네릭 (0) | 2026.02.15 |
| [JAVA] 08 객체지향(OOP) 4대 특징 (0) | 2026.02.15 |
| [JAVA] 07 클래스와 객체 (0) | 2026.02.15 |