자바를 실행하다보면 컴파일 단계에서 의도치 않은 오류가 발생할 수 있는데요. 이때 해당 오류가 발생될 때 사용자가 다른 작업을 수행하도록 코드를 구현할 수 있는데, 이를 예외 처리
라고 합니다.
이러한 예외 처리는 기본적으로 try
, catch
, finally
세 가지 키워드로 이루어져 있는데요.
try 블록문
은 예외가 발생될 수 있다고 예상되는 코드문이 입력되는 구간입니다. 여기서 해당 에러가 처리된다면 코드문 제어권이 catch 블록문으로 넘어가며, 에러가 발생되지 않으면 코드가 정상적으로 실행된 뒤 finally가 작성되지 않았다면 catch문을 거치지 않고 다음 코드가 실행되고, finally가 작성되었다면 finally 블록문으로 제어권이 넘어갑니다.
catch 블록문
은 try 블록문에서 에러 발생시 넘어온 제어권에 의해 추가로 사용자가 실행할 후속 처리 과정을 작성하는 곳이며, 매개변수로 에러가 예상되는 에러를 직접 작성해 줍니다. 주로 후속 및 정정 처리를 위한 코드들이 작성됩니다.
이때 catch의 매개변수에서 자주 쓰이는 멤버는 printStactTrace(발생 된 오류의 과정 출력),와 getMessage(예외가 발생된 원인이나 상황을 출력)가 있습니다.
finally 블록문
은 에러가 잡히든 잡히지 않든 마지막에 실행되는 블록문으로, DB나 리소스를 해제하거나 접속을 종료시키는 코드문이 작성되는 블록문으로, finally문을 작성할지 말지는 사용자의 선택사항입니다. 즉 finally 작성은 강요가 아니라는 점입니다.
public class ExceptionHandlingExample { public static void main(String[] args) { try { // divide 메서드에서 ArithmeticException 에러가 발생되면 try 블록의 나머지 코드문으들은 무시된 채 제어권이 catch문으로 이동됩니다. int result = divide(10, 0); System.out.println("Result: " + result); // 해당 에러가 발생할 경우 실행될 블록입니다. } catch (ArithmeticException e) { System.out.println("Error: Cannot divide by zero."); // 발생된 예외의 대표적 문구 System.out.println(e.getMessage()); // 오류의 발생 경로 출력 e.printStackTrace(); // 최종적으로 출력될 블록입니다. } finally { System.out.println("Division operation finished."); } } public static int divide(int a, int b) { return a / b; // try 블록문에 위와 같은 매개변수를 입력시 This line will throw ArithmeticException if b is 0 에러가 발생됩니다. } }
보통 catch문에 작성되는 예외 클래스들은 하위 클래스부터 상위 클래스 순으로 작성 하는데요.
이러한 예외의 대표적인 직속 관계는 Object(최상위 객체 클래스) -> Throwable(상위 클래스) -> Exception(실제 구현 클래스) -> 하위 예외 클래스들
순이며
예외에 대한 계보는 다음과 같습니다.
(자료 출처 : https://velog.io/@jaeseok-go/%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EA%B8%B0%EB%B2%95)
좀 더 구체적으로 대표되는 예외 처리는 다음과 같습니다.
Scanner 클래스를 사용하여 입력을 처리할 때 발생되며, 지정된 입력 형식과 사용자 입력 형식이 다를 경우 발생됩니다.
System.out.print("정수를 입력하세요: "); // 사용자가 정수 이외의 값을 입력시 에러 발생 int num = scanner.nextInt();
변수나 메서드의 반환값이 Null일때, Null 값을 갖는 객체를 사용하려 할 때 발생되며, 이는 곧 참조 객체가 안만들어졌거나, 값이 없는 상태를 의미합니다.
// 참조 클래스를 선언만 하면 Null로 저장 String s; // null이 저장되어 있으므로 length메서드를 호출할 수 없는 상태라 NullException 발생 s.length(); // 오류 발생
배열의 길이를 벗어난 범위의 인덱스에 접근할 때 발생됩니다.
// 3길이 만큼의 배열 선언 int[] numbers = {1, 2, 3}; // 배열의 길이를 벗어난 인덱스에 접근 시도 System.out.println(numbers[3]); // 오류 발생
숫자와 관련된 연산 중에 연산을 수행할 수 없을 경우 발생됩니다.
Scanner sc = new Scanner(System.in); int num1, num2; System.out.println("1번 정수 입력"); num1 = sc.nextInt(); System.out.println("1번 정수 입력"); num2 = sc.nextInt(); // num1과 num2를 작성할 때 연산이 불가능한 경우에 오류 발생 System.out.println("나눗셈 결과 : " + (num1 / num2)); // 10과 0을 입력하면 연산이 불가능 하므로 오류 발생
주어진 문자열을 숫자 형식으로 변환할 때 변환할 수 없을 정도로 올바르지 못한 값이 포함되어 있을 경우 발생됩니다.
// 문자열 선언 String str1 = "123"; String str2 = "123a"; // 숫자로 변환하는데 문제가 없으므로 예외 발생 X int number = Integer.parseInt(str1); // 숫자로 변환할 수 없는 문자열이므로 예외 발생 int number = Integer.parseInt(str2);
객체를 특정 타입으로 캐스팅할 때, 실제 객체의 타입이 캐스팅하려는 타입과 호환되지 않을 경우 발생됩니다.
// 문자열 타입의 값을 할당 Object obj = "This is a string"; // obj는 상속 관계에서 String 타입은 Integer 타입과 연관이 없으므로 Integer로 타입 캐스팅을 하려 할 때 에러 발생 Integer number = (Integer) obj
팁으로, 예외 처리를 세세하게 쓰면 예외 처리마다 처리할 코드를 작성 및 관리할 수 있다는 점에서 좋지만, 우리가 발생 될 예외를 예상하기란 쉽지 않기 때문에 세세히 쓰기가 또 어려운데요. 그래서 최상위 예외 클래스들에 해당되는 Exception
, 또는 Throwable
을 작성하는 것으로도 모든 예외들을 커버할 수 있습니다.
try-catch 문은 하나에 하나의 에러를 처리하는데, 프로그램을 실행하다보면 꼭 정해놓은 에러만 뜨라는 법은 없잖습니까?
이럴 경우 일어날 것으로 예상되는 에러들을 하나의 try-catch문에 작성하면 되는데, 이를 다중 예외 처리
라고 합니다.
사용법은 각각의 에러들이 예상되는 코드들을 try 블록문에 같이 작성한 후 각각의 에러 발생에 대한 후속 처리를 각각의 catch문으로 따로 작성하면 됩니다.
public class ExceptionHandlingExample { public static void main(String[] args) { try { int result = divide(10, 0); System.out.println("Result: " + result); } catch (ArithmeticException e) { System.out.println("Error: Cannot divide by zero."); } catch (NullPointerException e) { System.out.println("Error: NullPointerException occurred."); } finally { System.out.println("Division operation finished."); } } public static int divide(int a, int b) { return a / b; } }
throws
키워드는 해당 메서드를 실행하면서 발생할 수 있는 예외를 호출한 곳으로 떠넘겨 그곳에서 처리를 하게끔 만들 수 있는데요. 넘겨 받은 곳(메서드) 에서는 그 예외를 try-catch로 직접 처리해야 하나, 이는 선택 사항이므로 또 다른 곳에 떠 넘길수도(throw) 있습니다.
- 예외를 발생시키는 메서드
public static void readFile(String fileName) throws IOException { // 파일을 읽는 도중 예외 발생 가능 FileReader fileReader = new FileReader(fileName); BufferedReader reader = new BufferedReader(fileReader); String line = reader.readLine(); System.out.println("파일 내용: " + line); reader.close(); }
- 해당 메서드를 호출한 main 쪽에서 해당 에러가 발생했을 때 처리할 try-catch문을 작성
public static void main(String[] args) { try { // 예외를 떠넘기는(발생시키는) 메서드 호출 readFile("non_existing_file.txt"); } catch (IOException e) { // 예외 처리 System.out.println("파일을 읽는 중 오류 발생: " + e.getMessage()); } }
표준 라이브러리에는 사용자가 예상할 수 있는 예외가 존재하지 않을 수도 있는데요. 그럴때는 사용자가 직접 해당 상황에 발생 되어야만 하는 예외를 만드는 법을 제공합니다.
우선 예외 파일을 하나 만들어준 후 예외 클래스를 하나 생성하는데, Exception
예외 클래스를 상속 받습니다. 그리고 아래와 같이 생성자를 추가한 후 해당 예외를 사용하는 파일이 다른 패키지일 경우 임포트를 해줍니다.
// 사용자 정의 예외 클래스 정의 public class MyException extends Exception { // 기본 생성자 하나와 예외 메세지를 받아 부모(Exception)에 전달할 생성자를 각각 하나씩 만듭니다. public CustomException() { } // 생성자 추가 (예외 메시지를 받아서 부모 클래스의 생성자에 전달) public MyException(String message) { super(message); } }
그 후 특정 상황에서 예외를 발생 시킬 메서드를 준 후 특정 조건의 경우 해당 클래스에 대한 에러 메세지를 매개변수로 갖는 예외 클래스 객체를 throw
키워드를 이용해 생성합니다. (해당 메세지는 사용자가 정의한 클래스의 부모를 상속 받는 생성자 쪽으로 전달됩니다.)
그러면 해당 메서드를 호출한 곳에서는 try-catch문을 구현하여 해당 예외를 e.getMessage를 이용해 출력한 뒤 (선택 사항입니다.) 후속 처리를 진행해주면 됩니다.
public class ExceptionExample { // 메인 메서드 public static void main(String[] args) { try { // 예외 발생 메서드 호출 throw new MyException("사용자 커스텀 예외");; } catch (MyException e) { // 사용자 정의 예외 처리 System.out.println("Custom Exception Caught: " + e.getMessage()); } } }