[JAVA] <자바 기초 完> 예외처리 try-catch-finally 등

2023. 12. 18. 15:24프로그래밍 언어/Java

1. 프로그램 오류와 예외 클래스

프로그램 오류는 발생시점에 따라 컴파일 에러, 런타임 에러, 논리적 에러로 나뉜다. 컴파일 에러는 컴파일 전에 인지할 수 있고 논리적 에러는 단순 실행으로는 인지하기 어렵다. 런타임 에러는 프로그래머의 역량에 따라 사전에 방지할 수 있고 따라서 런타임 에러를 집중적으로 공부할 필요가 있다.

 

런타임 에러는 에러와 예외로 나뉜다. 에러는 메모리 부족이나 스택오버플로우 같은 일단 발생하면 복구할 수 없는 심각한 오류이고 예외는 발생하더라도 수습할 수 있는 비교적 덜 심각한 것이다. 에러가 발생하면 프로그램은 비정상적으로 종료되지만 예외는 발생하더라도 프로그래머의 적절한 코드로 비정상적인 종료를 막을 수 있다. 

 

예외와 오류 모두 클래스로 정의되어 있다. 모두 공통 조상인 Object를 가지며 모든 예외는 Exception의 자손이고 모든 에러는 Error의 자손이다. 그중 예외 클래스에 대해서 프로그래머가 처리할 수 있는 방법을 알아보자.


2. 예외 처리하기 - try-catch 문 

try-catch문으로 예외를 처리할 수 있다.

try {
  result = number / (int)(Math.random() * 10);
  System.out.println(result);
} catch (ArithmeticException e){
  System.out.println("0");
}

간단하게 이런 예시가 있는데, try문 안에서 예시가 발생하면 catch문에서 받아 catch문 내부 코드를 실행하게 된다. 감이 대충 잡힐텐데 예외 처리 순서를 보자.

  1. 발생한 예외에 해당하는 클래스의 인스턴스를 생성한다.
  2. 첫 번째 catch블럭부터 차례로 내려가면서 instacneof연산자를 이용해서 검사한다.
  3. 검사결과가 true인 catch블럭을 찾게 되면 try문을 빠져나가 catch문의 코드를 실행한다. 따라서 예제에서 예외가 발생하면 result는 출력하지 않고 바로 "0"을 출력하게 된다.

이런 식으로 예외처리를 할 수 있고, 예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨있다. 이 정보를 얻을 수 있는 유용한 메서드를 2개만 알아보자.

printStackTrace() : 예외발생 당시의 콜스택에 있던 메서드의 정보와 예외 메시지를 출력한다.

getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메세지(원인)을 얻을 수 있다.

 

실제 사용 예시를 보자.

import java.util.Scanner;

public class DivideByZeroExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("나눌 숫자를 입력하세요: ");
        int dividend = scanner.nextInt();

        System.out.print("나누는 숫자를 입력하세요: ");
        int divisor = scanner.nextInt();

        try {
            // 숫자를 0으로 나누려 시도
            int result = divide(dividend, divisor);
            System.out.println("나눈 결과: " + result);
        } catch (ArithmeticException e) {
            // 예외가 발생했을 때 처리
            System.out.println("0으로 나눌 수 없습니다.");
            e.printStackTrace(); // 스택 트레이스 출력
            System.out.println("에러 메시지: " + e.getMessage()); // 에러 메시지 출력
        } finally {
            // Scanner 닫기
            scanner.close();
        }
    }

    // 숫자를 나누는 메서드
    private static int divide(int dividend, int divisor) {
        return dividend / divisor;
    }
}

이 예제에서 5/0을 입력하면 에러가 발생하고 스택과 에러 메시지를 보여준다. 실행 결과는 다음과 같다.

 

나눌 숫자를 입력하세요: 5
나누는 숫자를 입력하세요: 0
0으로 나눌 수 없습니다.
java.lang.ArithmeticException: / by zero
at DivideByZeroExample.divide(DivideByZeroExample.java:33)
at DivideByZeroExample.main(DivideByZeroExample.java:17)
에러 메시지: / by zero

Process finished with exit code 0

 

결과를 보면 에러가 발생한 시점의 콜스택 가장 위에 있는 메서드 divide가 표시되고, 이를 호출한 main 메서드도 바로 다음에 나온다. 에러 메시지에서 예외가 발생한 원인도 보여준다.

 

또 유용한 try-catch문의 성질은 멀티 캐치문을 허용한다는 것이다. 사용 예시로 바로 보자.

import java.util.Scanner;

public class MultiCatchExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        try {
            System.out.print("나눌 숫자를 입력하세요: ");
            int dividend = Integer.parseInt(scanner.nextLine());

            System.out.print("나누는 숫자를 입력하세요: ");
            int divisor = Integer.parseInt(scanner.nextLine());

            int result = divide(dividend, divisor);
            System.out.println("나눈 결과: " + result);
        } catch (NumberFormatException | ArithmeticException e) {
            // 멀티 캐치 블록에서 각 예외를 형변환하여 사용
            if (e instanceof NumberFormatException) {
                System.out.println("올바른 숫자 형식이 아닙니다.");
            } else if (e instanceof ArithmeticException) {
                System.out.println("0으로 나눌 수 없습니다.");
            }

            e.printStackTrace(); // 스택 트레이스 출력
            System.out.println("에러 메시지: " + e.getMessage()); // 에러 메시지 출력
        } finally {
            // Scanner 닫기
            scanner.close();
        }
    }

    private static int divide(int dividend, int divisor) {
        return dividend / divisor;
    }
}

이처럼 | 기호로 catch블럭을 합칠 수 있으며 발생한 예외에 대해 instacneof연산자와 형변환을 통해 예외를 다룰 수 있다.


3. 예외 발생시키기

throw 키워드로 예외를 발생시킬 수 있다.

import java.util.Scanner;

public class ThrowExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        try {
            System.out.print("양수를 입력하세요: ");
            int number = scanner.nextInt();

            if (number < 0) {
                throw new IllegalArgumentException("음수는 입력할 수 없습니다.");
            }

            System.out.println("입력한 숫자: " + number);
        } catch (IllegalArgumentException e) {
            System.out.println("예외가 발생했습니다: " + e.getMessage());
            e.printStackTrace(); // 스택 트레이스 출력
        } finally {
            scanner.close();
        }
    }
}

이와 같은 방식으로 예외를 발생시켜 원하지 않는 입력을 컨트롤할 수 있다.


4. finally 블럭

finally 블록은 위의 예시들에서 나왔지만 try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, 예외 발생에 관계없이 항상 수행되어야 하는 문장들을 넣는다. 예외가 발생한 경우는 try -> catch -> finally 순으로 실행되고 예외가 발생하지 않으면 try -> finally 순으로 실행된다.

 

이 외에도 예외 처리에 관한 많은 내용들이 있는데, 나머지 내용들은 프로그래밍을 하면서 필요성이 느껴질 때 찾아보고 사용하면 될 것 같다.