본 게시글은 혼자 공부하는 자바 (저자 : 신용권)의 책과 유튜브 영상을 참고하였고, 개인적으로 정리하는 글임을 알립니다.


자바에서는 컴퓨터 하드웨어 오동작 또는 고장으로 인해 응용프로그램 실행 오류가 발생하는 것을 에러(error)라고 하고, 에러 이외에 프로그래머가 잘못 코딩하거나 잘못 조작하여 발생하는 오류를 예외(exception)라고 부른다.

 

예외에는 두 가지 종류가 있다

  • 일반 예외 : 프로그램 실행 시 예외가 발생할 가능성이 높기 때문에 자바 소스를 컴파일하는 과정에서 해당 예외 처리 코드가 있는지 검사를 함. 만약 예외 처리코드가 없다면 컴파일 오류가 발생
  • 실행 예외 : 실행 시 예측할 수 없이 갑자기 발생하기 때문에 컴파일하는 과정에서 예외 처리 코드가 있는지 검사하지 않음

자바에서는 예외를 클래스로 관리함. JVM은 프로그램을 실행하는 도중에 예외가 발생하면 해당 예외 클래스로 객체를 생성함. 그러고 나서 예외 처리 코드에서 예외 객체를 이용할 수 있도록 해준다. 모든 예외 클래스는 java.lang.Exception 클래스를 상속받는다.

일반 예외와 실행 예외 클래스는 RuntimeException 클래스를 기준으로 구별한다. RuntimeException의 하위 클래스가 아니면 일반 예외 클래스이고, 하위 클래스이면 실행 예외 클래스이다. 클래스 상속 관계에서 부모에 RuntimeException이 있다면 실행 예외 클래스이다.

 

실행 예외

실행 예외는 컴파일러가 체크하지 않기 때문에 오로지 개발자의 경험에 의해서 예외 처리 코드를 작성해야한다. 만약 개발자가 실행 예외에 대해 예외 처리 코드를 넣지 않았을 경우, 해당 예외가 발생하면 프로그램은 곧바로 종료된다.

 

NullPointerException

객체 참조가 없는 상태, 즉 null 값을 갖는 참조 변수로 객체를 접근했을 때 발생한다. 객체가 없는데 객체를 사용하려고 하니까 예외가 발생하는 것이다.

package TestPackage;
public class Test {
	public static void main(String[] args) {
		String data = null;
		System.out.println(data.toString());
	}
}
/*
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toString()" because "data" is null
	at TestPackage.Test.main(Test.java:5)
*/

 

ArrayIndexOutOfBoundsException

배열의 인덱스를 벗어난 접근을 하려고할 때 발생하는 에러이다. 예를 들어 배열 인덱스 0~3까지 4칸짜리 배열이 있는데 4에 접근하려고 하면 발생하는 오류이다.

package TestPackage;
public class Test {
	public static void main(String[] args) {
		int index;
		int[] arr = {1,2,3,4};
		index = 4;
		arr[index] = 15;
		
	}
}
/*
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
	at TestPackage.Test.main(Test.java:7)
*/

 

NumberFormatException

문자를 숫자로 바꿔주는 메소드를 사용할 때, 숫자로 바꿀 수 없는 문자를 매개 값으로 넘길 때 발생하는 예외이다.

package TestPackage;
public class Test {
	public static void main(String[] args) {
		String data1 = "100";
		String data2 = "a100";
				
		int value1 = Integer.parseInt(data1);
		int value2 = Integer.parseInt(data2); //예외 발생
		
		int result = value1 + value2;
		System.out.println(data1 + "+" + data2 + "=" + result);
	}
}
/*
Exception in thread "main" java.lang.NumberFormatException: For input string: "a100"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:668)
	at java.base/java.lang.Integer.parseInt(Integer.java:786)
	at TestPackage.Test.main(Test.java:8)
*/

 

ClassCastException

이 예외는 타입 변환을 할 때 발생하는데, 클래스의 경우 부모, 자식 관계가 아니거나 인터페이스의 경우 인터페이스와 구현 클래스 관계가 아닌데 타입 변환을 하려고 할 때 발생하는 예외이다.

package TestPackage;
public class Test {
	public static void main(String[] args) {
		Dog dog = new Dog();
		changeDog(dog);
		
		Cat cat = new Cat();
		changeDog(cat);
	}
	public static void changeDog(Animal animal) {
			Dog dog = (Dog) animal; 				//ClassCastException
	}
}
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
/*
Exception in thread "main" java.lang.ClassCastException: class TestPackage.Cat cannot be cast to class TestPackage.Dog (TestPackage.Cat and TestPackage.Dog are in unnamed module of loader 'app')
	at TestPackage.Test.changeDog(Test.java:11)
	at TestPackage.Test.main(Test.java:8)
*/

 

InputMismatchException

정수를 입력해야하는데 문자를 입력하는 경우 발생하는 예외이다.

이 오류를 처리하려면 java.util.InputMismatchException를 import 해주어야 한다.

package TestPackage;
import java.util.InputMismatchException;
import java.util.Scanner;
public class Test {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int inputInt;
		System.out.print("정수를 입력하세요 : ");
		try {
			inputInt = sc.nextInt();
		}
		catch(InputMismatchException e)
		{
			System.out.println("경고 : 정수가 아닌 것을 입력하였습니다.");
		}
	}
}
/*
정수를 입력하세요 : ㅁㄴㅇㅁㄴㅇ
경고 : 정수가 아닌 것을 입력하였습니다.
*/

 

예외 처리

try - catch - finally문

package TestPackage;
public class Test {
	public static void main(String[] args) {
		System.out.println("정수를 입력하세요");
		String input = "10.2";
		try { //예외가 발생될 가능성이 있는 코드를 입력
			int temp = Integer.parseInt(input);//문자열을 정수로 변환
			System.out.println("입력된 값 : " + temp);
		}
		catch(Exception e){ //예외 처리
			System.out.println("잘못된 값을 입력했습니다.");
		}
		finally {
			System.out.println("예외가 발생하든 안하든 무조건 실행되는 finally");
		}
	}
}
/*
정수를 입력하세요
잘못된 값을 입력했습니다.
예외가 발생하든 안하든 무조건 실행되는 finally
*/
  • try 블록에는 예외 발생 가능 코드가 위치한다.
  • try 블록의 코드가 예외 발생 없이 정상 실행되면 catch 블록의 코드는 실행되지 않고 finally블록의 코드를 실행한다. 만약 try 블록의 코드에서 예외가 발생하면 실행을 멈추고 catch블록을 실행하여 예외 처리 코드를 실행함. 그 후 finally 블록의 코드를 실행한다.
  • finally 블록은 생략이 가능하다. 예외 발생 여부와 상관없이 항상 실행할 내용이 있을 경우에만 작성한다. finally 블록은 try블록과 catch 블록에서 return문을 만나더라도 반드시 실행된다.

 

다중 catch

발생하는 예외의 종류마다 예외 처리를 다르게 하려면 다중 catch 블록을 작성하면 된다.

catch블록이 여러 개라 할지라도 단 하나의 catch 블록만 실행된다. 그 이유는 try 블록에서 동시 다발적으로 예외가 발생하지 않고, 하나의 예외가 발생하면 즉시 실행을 멈추고 해당 catch 블록으로 이동하기 때문이다.

주의해야하는 점
다중 catch 블록을 작성할 때 주의할 점은 상위 예외 클래스가 하위 예외 클래스보다 아래쪽에 위치해야 한다는 것이다.
try 블록에서 예외가 발생했을 때, 예외를 처리해줄 catch 블록은 위에서부터 차례대로 검색된다. 만약 상위(부모) 예외 클래스의 catch블록이 위에 있다면, 하위(자식) 예외 클래스의 catch 블록은 실행되지 않는다.
package TestPackage;
import java.util.InputMismatchException;
import java.util.Scanner;
public class Test {
	public static void main(String[] args) {
		try {
			Scanner scanner = new Scanner(System.in);
			System.out.print("생성할 배열의 개수(정수)를 입력하세요 : ");
			int inputInt = scanner.nextInt();
			int []arr = new int[inputInt];
			
			System.out.print("입력할 데이터를 입력하세요 : ");
			int inputData = scanner.nextInt();
			
			System.out.print("데이터를 입력할 배열의 인덱스를 입력하세요 : ");
			inputInt = scanner.nextInt();
			arr[inputInt] = inputData;
			System.out.println("인덱스 [" + inputInt + "] : " + arr[inputInt]);
		}
		catch(InputMismatchException e) //정수 이외의 값이 들어오면 처리하는 블록
		{
			System.out.println("정수를 입력하세요");
		}
		catch(ArrayIndexOutOfBoundsException e) { // 배열의 인덱스가 잘못되었을 때 처리하는 블록
			System.out.println("입력된 배열의 인덱스가 잘못되었습니다..");
		}
		catch(Exception e) {
			System.out.println("알수없는 예외가 발생했습니다.");
		}
	}
}
/*
생성할 배열의 개수(정수)를 입력하세요 : 2
입력할 데이터를 입력하세요 : sdasd
정수를 입력하세요
*/

위 예제와 같이 모든 예외의 어머니(부모)인 Exception 클래스 이름은 가장 마지막 catch문에 작성해 준다.

 

예외 떠넘기기(throws)

경우에 따라서는 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다. 이때 사용하는 키워드가 throws이다.

throws 키워드는 메소드 선언부 끝에 작성되어 메소드에서 처리하지 않은 예외를 호출한 곳으로 떠넘기는 역할을 한다.

throws키워드 뒤에는 떠넘길 예외 클래스를 쉼표로 구분해서 나열해주면 된다. 발생할 수 있는 예외의 종류별로 나열하는 것이 일반적이지만 Exception만으로 모든 예외를 간단히 떠넘길 수도 있다.

 

예외를 떠넘김 당한(?) 곳(예외를 호출한 곳)에서 예외 처리를 하지 않으면 컴파일 에러가 발생하니 꼭 예외 처리를 해주어야 한다.

package TestPackage;
public class Test {
	public static void main(String[] args) {
		String data = null;
		try {
			method(data);
		}
		catch(NullPointerException e) {
			System.out.println("NullPointerException");
		}
		catch(Exception e) {
			System.out.println("알수없는 예외가 발생했습니다.");
			e.printStackTrace(); //어떤 예외가 어떻게 발생했는지 간단히 알려준다.
		}		
	}
	public static void method(String data) throws NullPointerException, Exception {
		System.out.println(data.toString());
	}
}
/*
NullPointerException
*/
printStackTrace()메소드
printStackTrace()메소드는 프로그램을 개발하는 과정에서 예외의 구체적인 정보를 알고 싶을 때 쓴다.
예외의 정보를 알았다면 꼭 지우자

main() 메소드에서도 throws 키워드를 사용해서 예외를 떠넘길 수 있는데, main메소드는 그러면 또 JVM으로 예외를 떠넘긴다. 예외를 떠넘김 받은 JVM은 예외의 내용을 콘솔에 출력하는 것으로 예외 처리를 한다.