[Java] 다시 정리하는 "예외처리"

rara_kim·2022년 12월 13일
0

Java

목록 보기
39/39

예외(Exception)

자바에서는 컴퓨터 하드웨어의 오동작 또는 고장으로 인해 응용프로그램 실행 오류가 발생하는 것을 에러(Error)라고 한다. 그리고 에어 이외에 프로그램 자체에서 발생하는 오류는 예외(Exception)라고 부른다.

예외란 사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인해 발생하는 프로그램 오류를 말한다.
예외가 발생하면 프로그램이 곧바로 종류된다는 점에서는 에러와 비슷하다.
그러나 예외는 예외처리(Exception Handling) 를 통해 프로그램을 종류하지 않고 정상 실행 상태가 유지되도록 할 수 있다.

예외와 예외 클래스

예외는 두 가지 종류가 있다.
하나는 일반 예외(Exception)이고, 다른 하나는 실행 예외 실행 예외(Runtime Exception)이다.

일반 예외는 컴파일러 체크 예외라고도 하는데, 프로그램 실행 시 예외가 발생할 가능성이 높기 때문에 자바 소스를 컴파일하는 과정에서 해당 예외 처리 코드가 있는지 검사한다.
만약 예외 처리 코드가 없다면 컴파일 오류가 발생한다.

실행 예외는 컴파일러 넌(non) 체크 예외라고도 하는데, 실행 시 예측할 수 없이 갑자기 발생하기 때문에 컴파일하는 과정에서 예외 처리 코드가 있는지 검사하지 않는다.

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

모든 예외 클래스는 java.lang.Exception 클래스를 상속받는다.
일반 예외와 실행 예외 클래스를 RuntimeException 클래스를 기준으로 구별한다. RuntimeException의 하위 클래스가 아니면 일반 예외 클래스이고, 하위 클래스이면 실행 예외 클래스이다.

실행 예외(RuntimeException)

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

  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • NumberFormatException
  • ClassCastException

NullPointerException

자바 프로그램에서 가장 빈번하게 발생하는 실행 예외는 NullPointerException이다.
이 예외는 객체 참조가 없는 상태, 즉 null 값을 갖는 참조 변수로 객체 접근 연산자인 도트(.)를 사용했을 때 발생한다.
객체가 없는데 객체를 사용하려고 하니 예외가 발생하는 것이다.

public class NullPointerExceptionExample {
	public static void main(String[] args) {
    	String data = null;
        System.out.println(data.toString());   //NullPointerException 발생
    }
}

ArrayIndexOutOfBoundsException

배열에서 인덱스 범위를 초과할 경우 실행 예외인 ArrayIndexOutOfBoundsException이 발생한다.
예를 들어, 길이가 3인 int[] arr = new int[3] 을 선언했다면, 배열 항목을 지정하기 위해 arr[0] ~ arr[2]를 사용할 수 있다.
하지만 arr[3]을 사용하면 인덱스 범위를 초과하기 때문에 예외가 발생한다.

public class ArrayIndexOutOfBoundsExceptionExample {
	public static void main(String[] args) {
		String data1 = args[0];
		String data2 = args[1];

		System.out.println("args[0]: " + data1);  //ArrayIndexOutOfBoundsException 발생
		System.out.println("args[1]: " + data2);
	}
}

위 예제를 실행하면 ArrayIndexOutOfBoundsException이 발생한다.
그 이유는 2개의 실행 매개값을 주지 않았기 때문에 args[0], args[1]과 같이 인덱스를 사용할 수 없기 때문이다.
코드를 아래와 같이 수정하면 예외가 발생하지 않는 프로그램이 된다.
배열값을 읽기 전에 배열의 길이를 먼저 조사하고 실행 매개값이 없거나 부족할 경우 조건문을 이용해서 사용자에게 알려준다.

public class ArrayIndexOutOfBoundsExceptionExample2 {
	public static void main(String[] args) {
		if(args.length == 2) {
			String data1 = args[0];
			String data2 = args[1];
			System.out.println("args[0]: " + data1);  			
			System.out.println("args[1]: " + data2);
		} else {
			System.out.println("두 개의 실행 매개값이 필요합니다.");
		}
	}
}

NumberFormatException

리턴 타입메소드 이름(매개 변수)설명
intInteger.parseInt(String s)주어진 문자열을 정수로 변환해서 리턴
doubleDouble.parseDouble(String s)주어진 문자열을 실수로 변환해서 리턴

이 메소드들은 매개값인 문자열이 숫자로 변환될 수 있다면 숫자를 리턴하지만, 숫자로 변환될 수 없는 문자가 포함되어 있다면 NumberFormatException을 발생시킨다.

public class NumberFormatExceptionExample {
	public static void main(String[] args) {
		String data1 = "100";
		String data2 = "a100";
		
		int value1 = Integer.parseInt(data1);
		int value2 = Integer.parseInt(data2);   //NumberFormatException 발생

		int result = value1 + value2;
		System.out.println(data1 + "+" + data2 + " = " + result);
	}
}

ClassCastException

타입 변환(Casting)은 상위 클래스와 하위 클래스 간에 발생하고 구현 클래스와 인터페이스 간에도 발생한다.
이러한 관계가 아니라면 클래스는 다른 타입으로 변환할 수 없기 때문에 ClassCastException이 발생한다.

public class ClassCastExceptionExample {
	public static void main(String[] args) {
		Dog dog = new Dog();
		changeDog(dog);

		Cat cat = new Cat();
		changeDog(cat);         //ClassCastException 발생
	}

	public static void changeDog(Animal animal) {
		Dog dog = (Dog) animal;     //ClassCastException 발생 가능
	}
}

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

위 예제를 실행하면 예외가 발생한다.
Cat객체를 매개값으로 주는 경우 Dog 타입으로 변환할 수 없기 때문이다.
이렇게 잘돗된 매개값이 들어올 수 있기 때문에 instanceof를 이용해 타입 체크를 먼저 해주는 것이 좋다.

public static void changeDog(Animal animal) {
    if(animal instanceof Dog) {
		Dog dog = (Dog) animal;     //ClassCastException 발생 가능
    }    
}



예외 처리(Exception Handling)

자바 컴파일러는 소스 파일을 컴파일할 때 일반 예외가 발생할 가능성이 있는 코드를 발견하면 컴파일 에러를 발생시켜 개발자가 강제적으로 예외 처리 코드를 작성하도록 요구한다.
그러나 실행 예외는 컴파일러가 체크해주지 않기 때문에 개발자의 경험을 바탕으로 예외 처리 코드를 작성해야 한다.

예외 처리 코드

try-catch-finally 블록은 생성자 내부와 메소드 내부에서 작성되어 일반 예외와 실행 예외가 발생할 경우 예외 처리를 할 수 있도록 해준다.

try {
	예외 발생 가능 코드
} catch(예외클래스 e) {
	예외 처리
} finally {
	항상 실행;
}
  • try 블록에는 예외 발생 가능 코드가 위치한다.
  • try 블록의 코드가 예외 발생 없이 정상 실행되면 catch 블록의 코드는 실행되지 않고 finally 블록의 코드를 실행한다.
    만약 try 블록의 코드에서 예외가 발생하면 즉시 실행을 멈추고 catch블록으로 이동하여 예외 처리 코드를 실행한다.
    그리고 finally 블록의 코드를 실행한다.
  • finally 블록은 생략 가능하다. 예외 발생 여부와 상관없이 항상 실행할 내용이 있을 경우에만 finally 블록을 작성해주면 된다. 심지어 try 블록과 catch 블록에서 return문을 사용하더라도 finally 블록은 항상 실행된다.

일반 예외 처리

public class TryCatchFinallyExample {
	public static void main(String[] args) {
		try {
			Class clazz = Class.forName("java.lang.String2");
		} catch(ClassNotFoundException e) {
			System.out.println("클래스가 존재하지 않습니다.");   //결과: 클래스가 존재하지 않습니다.
		}
	}
}

실행 예외는 컴파일러가 예외 처리 코드를 체크하지 않기 때문에 IDE에서도 빨간 밑줄이 생기지 않는다.
오로지 개발자의 경험에 의해 예외 처리를 해주어야 한다.

실행 예외 처리

public class TryCatchFinallyRuntimeExceptionExample {
	public static void main(String[] args) {
		String data1 = null;
		String data2 = null;
		try {
			data1 = args[0];
			data2 = args[1];
		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("실행 매개값의 수가 부족합니다.");
			return;
		}

		try{
			int value1 = Integer.parseInt(data1);
			int value2 = Integer.parseInt(data2);
			int result = value1 + value2;
			System.out.println(data1 + "+" + data2 + " = " + result);
		} catch (NumberFormatException e) {
			System.out.println("숫자로 변환할 수 없습니다.");
		} finally {
			System.out.println("다시 실행하세요.");
		}
	}
}

예외 종류에 따른 처리 코드

다중 Catch

try 블록 내부는 다양한 예외가 발생할 수 있다.
이 경우 발생되는 예외별로 예외 처리 코드를 다르게 하려면 다중 catch 블록을 작성하면 된다.

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

public class CatchByExceptionKindExample {
	public static void main(String[] args) {
		try {
			String data1 = args[0];
			String data2 = args[1];

			int value1 = Integer.parseInt(data1);
			int value2 = Integer.parseInt(data2);
			int result = value1 + value2;
			System.out.println(data1 + "+" + data2 + " = " + result);

		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("실행 매개값의 수가 부족합니다.");
		} catch (NumberFormatException e) {
			System.out.println("숫자로 변환할 수 없습니다.");
		} finally {
			System.out.println("다시 실행하세요.");
		}
	}
}

catch 순서

다중 Catch 블록을 작성할 때 주의할 점은 상위 예외 클래스가 하위 예외 클래스보다 아래쪽에 위치해야 한다는 것이다.
try 블록에서 예외가 발생했을 때, 예외를 처리해줄 catch 블록은 위에서부터 차례대로 검색된다.
만약 상위 예외 클래스의 catch 블록이 위에 있다면, 하위 예외 클래스의 catch 블록은 실행되지 않는다.
하위 예외는 상위 예외를 상속했기 때문에 상위 예외 타입도 되기 때문이다.

예외 떠넘기기

메소드 내부에서 예외가 발생할 수 있는 코드를 작성할 때 try-catch 블록으로 예외를 처리하는 것이 기본이지만, 경우에 따라서는 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다.
이때 사용하는 키워드가 throws이다.
throws 키워드는 메소드 선언부 끝에 작성되어 메소드에서 처리하지 않는 예외를 호출한 곳으로 떠넘기는 역할을 한다.
throws 키워드 뒤에는 떠넘길 예외 클래스를 쉼표로 구분해서 나열해주면 된다.

public class ThrowsExample {
	public static void main(String[] args) {
		try {
			findClass();
		} catch (ClassNotFoundException e) {
			System.out.println("클래스가 존재하지 않습니다.");   //결과: 클래스가 존재하지 않습니다.
		}
	}

	public static void findClass() throws ClassNotFoundException {
		Class clazz = Class.forName("java.lang.String2");
	}
}

profile
느리더라도 꾸준하게

0개의 댓글