1. 예외처리(Exception)
→ 오류 발생 가능성이 있는 부분에 대한 처리를 미리 프로그래밍 해주는 것
1) 컴파일러 체크 예외 (Checked Exception)
- 자바 소스를 컴파일하는 과정에서 예외 처리 코드를 검사하여 예외 처리 코드가 없다면 컴파일 오류 발생
- 실행하지 않고도 오류가 발생한 것을 알 수 있음
- Checked Exception은 필요하지 않다고 결론이 남
2) 실행 예외 (Runtime Exception)
- 컴파일하는 과정에서 예외처리 코드를 검사하지 않는 예외
- 개발자들 사이에서 실행 예외만 신경쓰기로 합의, 오로지 개발자의 경험에 의해 처리
- 실행 예외에 대한 예외처리 코드를 넣지 않았을 때 해당 예외가 발생하면 프로그램 즉시 종료
RuntimeException을 상속받는 예외들은 모두 Unchecked Exception이다.
둘 다 확인 시점은 실행 단계이고, rollback 여부는 설정에 따라 달라진다.
2. 실행 예외 (Unchecked Exception = Runtime Exception)
1) NullPointerException
→ 객체 참조가 없는 상태, null 값을 갖는 참조 변수로 객체 접근 연산자인 "."을 사용했을 때 발생
public class NullPointerEx {
public static void main(String[] args) {
String str = null;
//str = "HELLO";
// .toLowercase()는 모든 문자를 소문자로 만들어줌
System.out.println(str.toLowerCase());
}
}
참조형 변수로 선언한 str이 null값이므로 위와 같은 오류가 발생하는 것을 확인할 수 있다.
2) ArrayIndexOutOfBoundsException
→ 배열에서 인덱스 범위를 초과하여 사용할 경우 발생
public class ArrayIndexEx {
public static void main(String[] args) {
int [] arr = {3, 6, 9};
// 있지도 않은 인덱스 번호 조회하기, 그러나 문법상 오류는 없음
System.out.println(arr[3]);
}
}
배열의 길이가 3인데 존재하지 않는 3번 인덱스 값을 조회했으므로 위와 같은 오류가 발생하는 것을 확인할 수 있다.
3) NumberFormatException
→ 문자열로 되어 있는 데이터를 숫자로 변경하는 경우 발생
public class NumberFormatEx {
public static void main(String[] args) {
String a = "35";
String b = "21";
System.out.println(a + b);
// str -> int 변환
int i = Integer.parseInt(a); // 문자 35를 숫자 35로 변환
int j = Integer.parseInt(b); // 문자 21을 숫자 21로 변환
System.out.println(i + j);
// parseInt는 문자열 내부에 순수한 정수가 들어있어야 변환을 실행하며
// 정수값이 아니라면 NumberFormatException이 발생
String s = "Hello";
System.out.println(Integer.parseInt(s));
}
}
s 변수는 String 타입으로 정수값이 아닌데 parseInt()는 문자열 내부에 순수한 정수가 있어야함 변환을 실행하기 때문에 위와 같은 오류가 발생하는 것을 확인할 수 있다.
4) ClassCastException
→ 상속 관계나 인터페이스 관계가 없는 클래스들을 억지로 형 변환할 경우 발생
☆ 형 변환은 부모클래스 -자식클래스, 구현클래스-인터페이스 간에 발생 → 이 관계가 아니라면 다른 클래스로 타입 변환 불가
//하나의 클래스 파일에 2개 이상의 클래스 선언 가능 (자주 사용하지는 않음)
// 상속관계 : 부모 Animal을 상속한 자식 Dog, Cat
class Animal{}
class Dog extends Animal{}
class Cat extends Animal{}
public class ClassCastEx {
public static void main(String[] args) {
Dog d = new Dog();
Animal da = d;
d = (Dog)da;
System.out.println("타입 변환 성공 : Animal -> Dog");
Animal c = new Cat();
Dog d2 = (Dog)c; // Cat은 Dog 타입으로 변환 불가
System.out.println("타입 변환 성공? : Dog -> Cat");
}
}
Cat과 Dog는 상속 관계에 있는 클래스가 아니므로 위와 같은 오류가 발생하는 것을 확인할 수 있다.
3. 예외처리코드
1) try ~ catch
- 예외 발생 시 갑작스러운 종료를 막고 정상 실행을 유지할 수 있도록 처리하는 코드
- try~catch~finally 블록 : 생성자나 메서드 내부에서 작성되어 컴파일 예외나 실행 예외가 발생할 경우 예외처리
- try 블록 : 예외 발생 가능성이 있는 코드 작성 → 예외 발생 시 실행을 멈추고 catch 블록으로 이동
- finally 블록 : 예외 발생 여부와 상관없이 항상 실행 → 필수는 X, 로직이 블럭과 관련 있을 시 작성
※ finally 구문이 실행되지 않는 경우
① finally 구문 이전에 System.exit() 구문 호출 시
② 컴퓨터가 꺼져서 시스템이 멈추었을 시
③ finally 블록 내부에서 예외가 발생했을 시
public class TryCatchEx1 {
public static void main(String[] args) {
int i = 10;
int j = 5;
try { // 예외가 발생할 가능성이 있는 코드를 넣는 구역
System.out.println(i / j); // 예외 발생 가능성이 있음
System.out.println("예외 발생하지 않을 때만 실행됨");
} catch(Exception e) { //catch 블럭에는 Exception의 종류를 기입
System.out.println("0으로 나눠서 catch 블럭으로 넘어왔습니다.");
} finally { // try, catch 둘 중 어느 블럭이라도 실행되면 마무리 블럭 실행
System.out.println("어쨌든 잘 마무리 했습니다.");
}
}
}
변수 j가 5라면 try 블럭 안의 내용과 finally 블럭 내용이 출력된다.
변수 j가 0이라면 예외가 발생하여 catch 블럭으로 넘어가고 해당 내용과 finally 블럭 내용이 출력된다.
2) 다중 catch
- 여러 가지 예외가 발생한다면 다중 catch 블록 작성
- 상위 예외 클래스가 하위 예외 클래스보다 아래쪽에 위치해야 함
- 상위 예외 클래스의 catch 블록이 위에 있다면 하위 예외 클래스의 catch 블록은 실행되지 않음
- 자바 7 버전부터 하나의 catch 블록에서 여러 개의 예외 처리 가능
- catch() 괄호 안에 동일하게 처리하고 싶은 예외를 |로 연결 (※ 두 예외가 상속 관계에 있으면 안됨)
ublic class MultiCatchEx {
public static void main(String[] args) {
String data1 = "30";
String data2 = "11";
try {
// NumberFormatException 발생 가능 (정수가 아닌 값이 들어갈 경우)
int i = Integer.parseInt(data1);
int j = Integer.parseInt(data2);
// ArithmeticException 발생 가능 (j에 0이 들어갈 경우)
int result = i / j;
System.out.println("i / j = " + result);
// NullPointerException 발생 가능
String str = null;
str.charAt(0); // 0번째 문자 얻기인데 null값인 경우
} catch(NumberFormatException | NullPointerException e) {
System.err.println("데이터를 숫자만 넣어주세요.");
System.err.println("혹은 문자를 제대로 만들어주세요.");
} catch(ArithmeticException e) {
System.err.println("0으로 나눌 수 없습니다.");
} catch(Exception e) { // 범용 에러 처리 (대부분의 에러를 다 커버)
System.err.println("알 수 없는 에러가 발생했습니다.");
System.err.println("복구중입니다.");
}
}
}
catch 블록은 위에서부터 차례대로 검색되고, 예외 발생 시 해당하는 예외처리를 할 수 있게 된다.
위 예제의 경우 NullPointerException 예외만 발생하므로 첫번째 catch 블록의 내용만 출력하는 것을 확인할 수 있다.
3) throws
- 메서드나 생성자를 호출한 곳으로 예외를 던지는 방법 → 예외 처리를 직접 수행하지 않고 메서드 호출자에게 던짐
- try 블록 내부에서 호출되어야 하고, catch 블록에서 떠넘겨 받은 예외 처리를해야 함
- throws가 붙어있는 메서드는 반드시 try 블록 내부에서 호출되어야 하고, catch 블록에서 떠넘겨 받은 예외를 처리해야 함
- main() 메서드에서 throws를 사용하는 것은 예외처리를 JVM에게 넘기겠다는 의미
※ JVM은 예외를 직접 처리해주지 않고 예외 메시지만 출력하고 프로그램 종료
public class ThrowsEx {
public static String[] greetings = {"안녕", "싸왓디", "헬로"};
// 예외의 원인이 메서드 선언부가 아닌 호출부에 있을 경우
// 메모리 영역이 다르므로 예외처리를 메서드 호출지역으로 떠넘겨줘야 함
// 이를 throws라고 하고, 메서드 혹은 생성자 호출 시 예외처리를 강요할 때 사용
public static void greet(int idx) {
try {
System.out.println(greetings[idx]);
} catch(ArrayIndexOutOfBoundsException e){
// 코드 안적어도됨
}
}
public static void main(String[] args) {
// throws가 붙어 있는 메서드나 생성자 호출 시에는
// 해당 메서드를 try 블록 내부에서 호출해야 예외처리를 진행해줌
greet(3);
}
}
위 예제에서 greetings 배열의 크기가 3인데 4번째 인덱스를 조회하고 있으므로 ArrayIndexOutOfBoundsException이 발생하고, 이 경우 위와 같이 greet 함수에서 예외처리를 해줄 수 있다. 하지만, 객체 지향 측면에서 해당 메서드의 책임이 많아지므로 위 코드는 좋은 코드가 아니다.
public class ThrowsEx {
public static String[] greetings = {"안녕", "싸왓디", "헬로"};
// 예외의 원인이 메서드 선언부가 아닌 호출부에 있을 경우
// 메모리 영역이 다르므로 예외처리를 메서드 호출지역으로 떠넘겨줘야 함
// 이를 throws라고 하고, 메서드 혹은 생성자 호출 시 예외처리를 강요할 때 사용
// 해당 예외가 발생하면 호출부(여기서는 main)에게 처리를 떠넘기는 것
public static void greet(int idx) throws Exception {
System.out.println(greetings[idx]);
}
public static void main(String[] args) {
// throws가 붙어 있는 메서드나 생성자 호출 시에는
// 해당 메서드를 try 블록 내부에서 호출해야 예외처리를 진행해줌
try {
greet(3);
} catch(Exception e) {
// .printSackTrace()는 예외발생 경로를 추적하는 메세지 출력
// 주로 개발 과정에서 예외의 원인을 역추적할 때 유용
e.printStackTrace();
}
System.out.println("프로그램 정상 종료!");
}
}
위와 같이 throws가 붙은 greet() 메서드 호출 시 해당 메서드가 호출되고 있는 메인함수 쪽에서 예외처리를 진행할 수 있도록 하는 것이 바람직하다.
위와 같이 예외처리가 잘 이루어지고, 프로그램이 정상 종료된 것을 확인할 수 있다.
public class ThrowsEx2 {
public void aaa(int n) throws Exception {
System.out.println("aaa 호출");
int i = 10 / n;
System.out.println("계산 결과 : " + i);
System.out.println("aaa 실행 중");
}
public void bbb() throws Exception {
System.out.println("bbb 호출");
aaa(0);
System.out.println("bbb 실행 중");
}
public void ccc() throws Exception {
System.out.println("ccc 호출");
bbb();
System.out.println("ccc 실행 종료");
}
public ThrowsEx2() throws Exception {
System.out.println("생성자 호출");
ccc();
System.out.println("생성자 실행 종료");
}
public static void main(String[] args) {
try {
ThrowsEx2 te = new ThrowsEx2();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("진짜 코드 호출 끝!");
}
}
4) throw
- 예외를 강제로 발생시키려면 throw 키워드 이용 → 개발자가 직접 예외 클래스를 정의하여 만들 수 있음
public class ThrowEx {
public static int calcSum(int n) throws Exception {
// 프로그램이 throw 구문을 만나는 순간 예외를 즉시 발생시키고,
// 해당 예외를 처리해줄 catch 블록이 있는지 검색
if(n <= 0) {
throw new Exception(); // 예외도 클래스로 정의되기 때문에 인스턴스 생성
}
int sum = 0;
for(int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
public static void main(String[] args) {
try {
int result1 = calcSum(100);
System.out.println("1~100까지의 누적합 : " + result1);
int result2 = calcSum(-100);
System.out.println("1~100까지의 누적합 : " + result2);
} catch (Exception e) {
e.printStackTrace();
System.err.println("매개값을 양수로 전달해주세요.");
}
}
}
throw 키워드가 없다면 위 코드에서 논리적으로는 문제가 없기 때문에 정상적으로 작동한다.
하지만, 반복문 사용 시 i 값에 음수가 들어간다면 반복문을 실행하지 않기 때문에 위 예시에서는 throw를 사용하여 강제로 예외처리를 해주는 것이 좋다.
객체 생성 시 throw 키워드를 붙여주면 위와 같이 강제로 설정한 예외처리를 해주는 것을 확인할 수 있다.
5) 사용자 정의 예외
- 자바 표준 API에서 제공하는 예외 클래스만으로 다양한 종류의 예외를 표현할 수 없는 문제가 발생
- 일반 예외로 선언할 경우 Exception 클래스 상속, 실행 예외로 선언할 경우 RuntimeException 클래스 상속
- 사용자 정의 예외 클래스의 이름은 Exception으로 끝나는 것이 좋음
// 잔고 불충분 예외
// 사용자 정의 예외 클래스 생성 시 Exception 클래스나 RuntimeException 클래스 상속
public class BalanceInsufficientException extends RuntimeException {
// 일반적으로 사용자 정의 예외 클래스를 만들 때는
// 기본 생성자와 예외원인 메세지를 받는 생성자를 두 개 오버로딩하여 선언만 해줌
public BalanceInsufficientException() {};
public BalanceInsufficientException(String message) {
super(message);
}
}
// 음수값 입금 예외
public class DepositMinusMoneyException extends RuntimeException {
public DepositMinusMoneyException() {}
public DepositMinusMoneyException(String message) {
super(message);
}
}
public class Account {
private long balance;
public long getBalance() {
return this.balance;
}
// 음수로 입금할 경우 강제 예외처리
public void deposit(int money) throws DepositMinusMoneyException {
if (money < 0) {
throw new DepositMinusMoneyException("음수로 입금할 수 없습니다.");
}
this.balance += money;
}
// 인출 시 잔액이 부족한 경우 강제 예외처리
public void withdraw(int money) throws BalanceInsufficientException {
if(this.balance < money) {
throw new BalanceInsufficientException("잔고가 부족합니다.");
}
this.balance -= money;
}
}
public class MainClass {
public static void main(String[] args) {
Account acc = new Account();
try {
acc.deposit(100000);
System.out.println("입금 후 잔액 : " + acc.getBalance() + "원");
acc.withdraw(100000);
} catch(BalanceInsufficientException e) {
// 예외 클래스가 제공하는 getMessage() 메서드는 예외의 원인 메세지를 String 타입으로 리턴
// 자바 표준 API에서 제공하는 다양한 예외클래스들은 각각의 예외 원인 메세지가 기본적으로 객체 안에 저장
e.printStackTrace();
// 생성자에서 제공해준 메세지 그대로 출력
System.err.println(e.getMessage());
} catch(DepositMinusMoneyException e) {
e.printStackTrace();
System.err.println(e.getMessage());
}
System.out.println("출금 후 잔액 : " + acc.getBalance() +"원");
}
}
위와 같이 어떤 종류의 예외가 발생했는지 정확히 알 수 있기 때문에 유지보수성을 높일 수 있다.