본문 바로가기
Programming/Java

[자바/Java] 예외(Exception)

by 코딩하는 랄로 2023. 10. 8.
728x90

예외(Exceiption)이란

개발자들이라면 필연적으로 예외가 발생하는 상황을 마주한다. 자신이 작성한 코드를 실행할 때 어떠한 원인에 의해 프로그램이 정상 작동하지 않고 예외 또는 에러를 발생시키며 종료되는 상황은 빈번하게 일어난다.

 

이러한 상황을 프로그램의 오류가 발생했다고 하는데 이러한 오류는 크게 컴파일 오류런타임 오류 두가지로 나뉜다. 컴파일 오류는 문법상의 오류이기 때문에 쉽게 잡아낼 수 있지만 런타임 중 발생하는 오류인 런타임 오류는 해결하기가 까다롭다.

 

자바에서는 런타임 오류를 두가지로 나뉘는데 바로 에러(Error)예외(Exception)이다. 에러는 코드로는 해결할 수 없는 오류이지만 예외는 프로그래머가 직접 예측하여 막을 수 있는 처리 가능한 오류이다.

 

그렇기 때문에 자바를 사용하여 프로그래밍을 할 때에는 예외에 대한 처리를 할 수 있어야 한다.

 

 

 

예외 종류

본격적으로, 예외를 처리하는 방법에 대해서 다루기 전에 먼저 자바에서 예외의 종류가 어떠한 것이 있는지 가볍게 알아보겠다. 

 

위에서 예외는 런타임 오류와 컴파일 오류로 나뉜다고 하였는데 구체적으로 덧붙이자면 컴파일 오류는 컴파일 시 발생되는 예외, 반드시 예외 처리를 해야 하는 오류로 체크 예외(Checked Exception)이라고 한다.

 

반면에, 런타임 오류는 컴파일러가 예외를 체크하지 않는 런타임 중 발생할 수 있는 예외로, 꼭 예외처리를 해줄 필요 없는 비체크 예외(UnChecked Exception)이라고 한다.

 

비체크 예외로는 흔히 다음과 같은 예외가 있다.(많이 마주치는...)

 

  • ArithmeticException : 어떤 수를 0으로 나눌 때 발생
  • NullPointerException : Null 객체를 참조할 때 발생
  • ClassCastException : 적절치 못하게 클래스를 형변환할 때 발생
  • NegativeArraySizeException : 배열의 크기가 음수 값일 때 발생
  • OutOfMemoryException : 사용 가능한 메모리가 없을 때 발생
  • NoClassDefFoundException : 원하는 클래스를 찾지 못하였을 때 발생
  • ArrayIndexOutOfBoundsException : 배열을 참조하는 인덱스가 잘못되었을 때 발생

 

 

 

try - catch

자바에서 예외에 대한 처리를 하기 위해서는 try - catch문을 이용한다. 이름 그대로 해당 구문을 시도(try)하고 예외가 발생할 경우 잡는것(catch)이다. 

 

예를 들어 특정 수를 0으로 나누는 상황이 생길 수 있는 코드를 작성해야 한다면 다음과 같이 코드를 짤 수 있다.

try { 

    Scanner sc = new Scanner(System.in);
    
    int n = 10;
    int m = sc.nextInt();  // m의 값은 입력받기
    
    System.out.println(n / m); // n을 m으로 나눈 값 출력
    sc.close();
    
} catch(ArithmeticException e) {

    System.out.println("잘못된 입력 : m이 0값임!!");
    e.printStackTrace();
    
}

위와 같은 상황에선 만약 m값으로 0을 입력받게 되면 "특정 수를 0으로 나누게 되는 상황이 발생"하고 이 때 발생하는 오류를 잡아서 잘못된 입력이라는 것을 알려주는 코드이다.

 

위의 상황에서는 예외가 발생하는 경우가 하나이지만 코드가 길어질 수록 여러 예외가 발생할 수 있다. 이럴 때에는 catch문을 여러개를 사용하여 예외를 잡아 낼 수 있다.

try {

    // 예외들이 발생할 수 있는 코드
    
} catch (IOException e) {
    // IOException 에 대한 처리
} catch (FileNotFoundException e) {
    //FilenotFoundexception에 대한 처리
} catch (NullPointerException e) {
    //NullPointerException에 대한 처리
}

 

또한, 어떤 예외가 발생하든 하나의 catch문을 통해서 처리를 하고 싶다면, 예외 객체들의 최상위 부모 객체인 Exception을 통해서 처리를 할 수 있다.

try {

} catch(Exception e) {
    // 어떤 예외든 상관없음
}

 

특정 몇가지 예외에 대한 처리는 하고, 그외 나머지 예외들은 Exception을 통해서 포괄적으로 처리를 하고 싶다면 다음과 같이 처리할 수 있다.

try {
    // 예외들이 발생할 수 있는 코드
} catch (IOException e) {
    // IOException 에 대한 처리
} catch (FileNotFoundException e) {
    //FilenotFoundexception에 대한 처리
} catch (Exception e) {
    //그 외 나머지 예외 처리
}

 

이 때에 중요한 포인트는, Exception에 대한 처리 구문이 가장 마지막에 와야한다는 점이다. catch문은 에외를 잡아낼 때 해당 catch문에 바로 매칭되는 것이 아닌 위쪽부터 아래로 catch문을 내려가면서 자신한테 맞는 첫번째 catch문을 실행하기 때문에 Exception이 위쪽으로 오게 되면 모두 Exception에서 예외를 처리하게 되는 것이다.

try {
    // 예외들이 발생할 수 있는 코드
} catch (Exception e) {
    //모든 예외 처리
} catch (FileNotFoundException e) {
    //FilenotFoundexception 발생해도 Exception에서 처리
} catch (IOException e) {
    // IOException이 발생해도 Exception에서 처리
}

 

 

 

finally

예외가 발생할 경우, try문을 돌다가 catch문에서 예외를 잡게된다. 이 때에 남은 try문은 더이상 실행되지 않는 것이다. 하지만, 코드를 작성하다 보면 반드시 실행되어야 하는 코드들이 있다.

 

예를 들어, 다음 코드에서 Scanner를 닫는 sc.close()는 실행해주는 것이 좋다.

try { 

    Scanner sc = new Scanner(System.in);
    
    int n = 10;
    int m = sc.nextInt();  // m의 값은 입력받기
    
    System.out.println(n / m); // n을 m으로 나눈 값 출력
    sc.close();
    
} catch(ArithmeticException e) {

    System.out.println("잘못된 입력 : m이 0값임!!");
    e.printStackTrace();
    
}

만약 m값이 0이 아나라면 실행되지만 0이라면 Scanner를 연 채 프로그램이 종료되는 상황이 발생한다. 이런 상황을 막을 수 있는 구문이 바로 finally이다.

 

finally는 예외가 발생하든 하지 않든, 무조건 실행되는 구문으로 위와 같은 예제는 다음과 같이 처리할 수 있다.

try { 

    Scanner sc = new Scanner(System.in);
    
    int n = 10;
    int m = sc.nextInt();  // m의 값은 입력받기
    
    System.out.println(n / m); // n을 m으로 나눈 값 출력
    
} catch(ArithmeticException e) {

    System.out.println("잘못된 입력 : m이 0값임!!");
    e.printStackTrace();
    
} finally {

    sc.close();
    
}

 

 

 

throws

throws는 이름 그대로 예외를 던진다라는 것을 뜻한다. "예외를 던진다"라고만 하면 어떤 의미인지 와닿지 않을 수 있다. 이 문장에 주어와 목적어를 덧붙이게 되면 다음과 같은 문장이 된다.

예외가 발생할 수 있는 메소드가 그 메소드를 사용하는 사용자에게 예외(처리)를 던진다,

 

즉, 해당 메소드를 사용하는 코드에서 다음과 같이 예외 처리를 해주어야 하는 것이다.

public static void divide(int n, int m) thorws ArithmeticException {

    if(m == 0) throw new ArithmeticException("0으로 나눌 수 없음"); //예외를 던점
    System.out.println(n / m);

}


public static void main(String[] args) { 
    try {
        Scanner sc = new Scanner(System.in);
    
        int n = 10;
        int m = sc.nextInt();  // m의 값은 입력받기
    
        divide(n, m)  //divide 함수 호출
    
    } catch(ArithmeticException e) {  //divide 함수가 던진 예외를 처리

        System.out.println("잘못된 입력 : m이 0값임!!");
        e.printStackTrace();
    
    } finally {

        sc.close();
    
    }
}

 

divide함수가 m이 0일 때, throw를 통해 ArithmeticException을 해당 메소드를 사용하는 main()에게 던지게 되고 이를 받은 main이 try - catch문을 통해 처리를 해주는 코드이다.

throw new Exception("예외발생")  // new를 통해 생성한 예외 객체를 throw를 통해 던짐

 

728x90