Language/Java

[Java] 예외 처리 (try, catch , throws, throw, Exception, Error)

08genie 2023. 11. 24. 21:03
반응형

Exception과 Error 차이

자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 '에러(Error)'와 '예외(Exception)' 두 가지로 구분하였습니다.

 

에러(Error)는 메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고,

예외(Exception)는 발생하더라도 수습될수 있는 비교적 덜 심각한 것입니다.

 

에러가 발생하면, 프로그램의 비정상적인 종료를 막을 길이 없지만, 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있습니다.

 

자바에서는예외(Exception)뿐만이 아닌 에러(Error) 역시 클래스로 정의하였습니다. 모든 클래스의 조상은 Object클래스이므로 Exception과 Error클래스 역시 Object클래스의 자손들입니다.

 

에러 클래스 상속구조
출처: https://gksdudrb922.tistory.com/156

 

Throwable클래스를 앞서 설명한 예외(Excpetion)클래스 계층 구조와 에러(Error)클래스가 상속하는 구조입니다.

 


자바가 제공하는 예외 계층 구조

자바는 실행(runtime) 시 발생할 수 있는 예외(Exception)를 클래스로 정의하였습니다.

모든 예외의 최고 조상은 Exception클래스이며, 상속계층도를 Exception클래스로부터 도식화하면 다음과 같습니다.

예외계층구조
출처: https://gksdudrb922.tistory.com/156

 

 


예외 구분 (Checked Exception, Unchecked Exception)

예외 계층 구조
https://hahahoho5915.tistory.com/67

 

위 그림에서 볼 수 있듯이 예외 클래스들은 다음과 같이 두 그룹으로 나눠질 수 있습니다.

 

Checked Exception

  • RuntimeException 의 하위 클래스가 아니면서 Exception 클래스의 하위 클래스
  • 반드시 에러 처리를 해줘야 함
  • 컴파일 시점에 확인된 exception
  • 주로 외부의 영향으로 발생할 수 있는 것들로서, 프로그램 사용자들의 동작에 의해서 발생하는 경우가 많음

EX)

IOException : 입출력 예외가 발생했을 때
SQLException : 데이터베이스 액세스 오류 또는 기타 오류에 대한 정보를 제공하는 예외
ClassNotFoundException : 동적으로 클래스를 문자열로 로딩하다가 클래스가 없는 경우
FileNotFoundException : 지정된 파일을 찾을 수 없을 때

 

 

Unchecked Exception

  • RuntimeException 의 하위 클래스
  • 에러 처리를 강제하지 않음
  • 실행 중에(runtime) 에 발생할 수 있는 예외
  • 프로그래머의 실수에 의해서 발생될 수 있는 예외들로 자바의 프로그래밍 요소들과 관계가 깊음

EX)
ArrayIndexOutOfBoundsException : 배열의 범위를 벗어났을 때
NullPointerException : 값이 null이 참조변수를 참조했을 때

 


자바 예외처리 (try, catch, throws, throw, finally)

프로그램 실행도중 발생하는 예외는 프로그래머가 이에 대한 처리를 미리 해주어야 합니다.

 

발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외(uncaught exception)는 JVM의 '예외 처리기(UncaughtExceptionHandler)'가 받아서 예외의 원인을 화면에 출력합니다.

 

예외 처리를 하기 위해서는 앞으로 소개할 다양한 방식을 사용할 수 있습니다.

 

1. try catch

try {
    //예외가 발생할 가능성이 있는 문장들을 넣습니다.
} catch (Exception1 e1) {
    //Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적습니다.
} catch (Exception2 e2) {
    //Exception2이 발생했을 경우, 이를 처리하기 위한 문장을 적습니다.
} catch (ExceptionN eN) {
    //ExceptionN이 발생했을 경우, 이를 처리하기 위한 문장을 적습니다.
}

 

하나의 try블럭 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch블럭이 올 수 있으며,

이 중 발생한 예외의 종류와 일치하는 단 한 개의 catch블럭만 수행됩니다.

발생한 예외의 종류와 일치하는 catch블럭이 없으면 예외는 처리되지 않습니다.

 

catch블럭 괄호()내에는 처리하고자 하는 예외와 참조변수 하나를 선언해야 합니다.

예외가 발생하면, 발생한 예외에 해당하는 클래스의 인스턴스가 만들어집니다.

 

 

예제) try블럭 안에서 정수(0)를 0으로 나누려 했기 때문에 'ArithmeticException' 발생

public class TryCatch {
 
    public static void main(String[] args) {
        try {
            System.out.println(1); 실행 O
            System.out.println(0 / 0); //예외발생!!!
            System.out.println(2); //실행 X
        } catch (ArithmeticException ae) {
            ae.printStackTrace();
            System.out.println("예외메시지 : " + ae.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        System.out.println(3); 실행 O
    }
}

 

 

<point 1>

try블럭 내에서 예외가 발생하면 그 즉시 catch문으로 이동하고 System.out.println(2)는 실행되지 않습니다.

 

<point 2>

예외가 발생하면 catch블럭 괄호()내의 예외클래스의 인스턴스에 대해 instanceof연산자를 이용해 차례대로 catch블럭을 검사하게 됩니다. (Exception은 모든 예외 클래스의 공통 조상) 예제의 경우 try블럭 내에서 'ArithmeticException'이 발생하고 이는 두 catch블럭, 'ArithmeticException ae', 'Exception e' 인스턴스 모두 instanceof 결과 true가 됩니다.

 

그러나 실제로는 첫 번째 catch블럭인 'ArithmeticException ae'에서 instanceof 검사에 일치하기 때문에 두 번째 catch문을 검사하지 않고 넘어가게 됩니다.

만일 try블럭 내에서 ArithmeticException이 아닌 다른 종류의 예외가 발생한 경우에는 두 번째 catch블럭인 Exception클래스 타입의 참조변수를 선언한 곳에서 처리되었을 것입니다.

 

-> 그래서 하나의 try블럭에 대해 여러 개의 catch블럭을 사용할 때는, Exception의 순서가 중요합니다.

 

<point 3>

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있으며, getMessage(),와 printStackTrace() 등 예외 인스턴스의 메서드를 통해서 이 정보들을 얻을 수 있습니다.

  • printStackTrace(): 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
  • getMessage(): 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

예제 결과를 보면 printStackTrace()를 사용해서 호출스택(Call Stack)에 대한 정보와 getMessage()를 통해서 예외 메시지를 출력한 것을 확인할 수 있습니다.

 

<point4 - 멀티 catch블럭>

JDK 1.7부터 여러 catch블럭을 '|' 기호를 이용해서, 하나의 catch블럭으로 합칠 수 있게 되었으며, 이를 '멀티 catch블럭'이라 합니다.

멀티 catch블럭을 이용하면 중복된 코드를 줄일 수 있습니다.

try {
    ...
} catch (ExceptionA e) {
    e.printStackTrace();
} catch (ExceptionB e2) {
    e2.printStackTrace();
}

 

-> 멀티 catch블럭

try {
    ...
} catch (ExceptionA | ExceptionB e) {
    e.printStackTrace();
}

 

여기서 주의할 점은, 만약 멀티 catch블럭의 연결된 예외 클래스가 서로 상속 관계에 있다면 컴파일 에러가 발생합니다.

왜냐하면, 두 예외 클래스가 상속 관계에 있다면, 그냥 부모 클래스만 써주는 것과 똑같기 때문입니다.

불필요한 코드는 제거하라는 의미에서 에러가 발생하는 것입니다.

 

2. throw

throw 키워드는 프로그래머가 고의로 예외를 발생시킬 수 있게 합니다.

public class Throw {

    public static void main(String[] args) {
        try {
        	System.out.println(1);
        	throw new Exception("고의로 발생");
        	System.out.println(2); //실행 X
        } catch (Exception e) {
        	System.out.println("에러 메시지 : " + e.getMessage());
        }
    }
}

 

+) 예외를 생성할 때 생성자에 String을 넣어 주면, 이 String이 Exception인스턴스에 메시지로 저장됩니다. 이 메세지는 getMessage()를 이용해서 얻을 수 있습니다.

 

3. throws

try-catch문을 사용하는 것 외에, 예외를 메서드에 선언해 처리하는 방법이 있습니다. throws 키워드를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 됩니다.

void method() throws Exception1, Exception2, ... ExceptionN {
    //메서드의 내용
}

 

자바에서는 이처럼 메서드를 작성할 때 메서드 내에서 발생할 가능성이 있는 예외를 메서드의 선언부에 명시하여 이에 대한 처리를 하도록 강요하기 때문에, 어떤 상황에 어떤 예외가 발생할지 try-catch를 통해 예측해야 하는 프로그래머들의 짐을 덜어주는 것을 물론 보다 견고한 프로그램 코드를 작성할 수 있도록 도와줍니다.

 

사실 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생한 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것입니다.

 

예외를 전달 받은 메서드가 또다시 자신을 호출한 메서드에세 전달하며, 이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main메서드에서도 예외가 처리되지 않으면, main메서드 마저 종료되어 예외로 인해 프로그램이 전체가 종료됩니다.

 

결국 예외가 발생한 메서드 혹은 이를 호출한 메서드 어느 한 곳에서는 try-catch문으로 예외처리를 해주어야 합니다. (또는 프레임워크에서 처리할 수 있도록 설정 필요)

 

예제1) method1 메서드에서 예외 처리

public class Throws {
 
    public static void main(String[] args) {
        method1();
    }
 
    static void method1() {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("method1 메서드에서 예외가 처리되었습니다.");
            e.printStackTrace();
        }
    }
}

 

예제2) main  메서드에서 예외 처리

public class Throws {
 
    public static void main(String[] args) {
        try {
            method1();
        } catch (Exception e) {
            System.out.println("main메서드에서 예외가 처리되었습니다.");
            e.printStackTrace();
        }
    }
 
    static void method1() throws Exception{
        throw new Exception();
    }
}

 

두 예제 모두 main메서드가 method1()을 호출하며, method1()에서 예외가 발생합니다.

첫 번째 예제는 예외가 발생한 method1()에서 예외처리를 하고,

두 번째 예제는 method1()에서 throws 키워드로 자신을 호출한 메서드로 예외 처리를 떠넘깁니다. 그래서 main메서드에서 try-catch로 예외처리를 했습니다.

 

이처럼 예외가 발생한 메서드 method1()에서 예외를 처리할 수도 있고, 예외가 발생한 메서드를 호출한 메서드 main메서드에서 처리할 수도 있습니다.

 

4. finally

finally블럭은 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용됩니다. try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있습니다.

public static void main(String[] args) {
    try {
        //예외가 발생할 가능성이 있는 문장들을 넣습니다.
    } catch (Exception1 e1) {
        //예외처리를 위한 문장을 적습니다.
    } finally {
        //예외의 발생여부에 관계없이 항상 수행되어야 하는 문장들을 적습니다.
        //finally블럭은 try-catch문의 맨 마지막에 위치해야 합니다.
    }
}

 

 

예외가 발생한 경우에는 'try -> catch -> finally'의 순으로 실행되고,

예외가 발생하지 않은 경우에는 'try -> finally'의 순으로 실행됩니다.

 

5. try-with-resources

JDK 1.7부터 try-with-resources문이라는 try-catch문의 변형이 새로 추가되었습니다.

 

아래와 같이 입출력에 사용되는 클래스 중에서는 꼭 닫아줘야 하는 것들이 있습니다. 그래야 사용했던 자원(resources)이 반환되기 때문입니다.

FileInputStream fis = null;
DataInputStream dis = null;
 
try {
    fis = new FileInputStream("score.dat");
    dis = new DataInputStream(fis);
} catch (IOException ie) {
    ie.printStackTrace();
} finally {
    try {
        if (dis != null) {
            dis.close();
        }
    } catch (IOException ie) {
        ie.printStackTrace();
    }
}

 

이렇게 DataInputStream으로 데이터를 읽는 도중에 예외가 발생하더라도 close될 수 있도록 finally블럭 안에  close()를 넣었습니다.

단, close() 역시 예외를 발생시킬 수 있기 때문에 finally블럭 안에 try-catch문을 추가했습니다.

 

이렇게 되면 코드가 복잡해져서 별로 보기에 좋지 않습니다. 이러한 점을 개선하기 위해 try-with-resources문을 사용합니다.

try(FileInputStream fis = new FileInputStream("score.dat");
    DataInputStream dis = new DataInputStream(fis)) {
 
} catch (IOException ie) {
    ie.printStackTrace();
}

 

try문 괄호() 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try블럭을 벗어나는 순간 자동적으로 close()가 호출됩니다. 그 다음에 catch블럭 또는 finally블럭이 호출됩니다.

 


 

커스텀 예외 만들기

기존에 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있습니다.

보통 Exception 혹은 RuntimeException클래스로부터 상속받아서 사용자 정의 예외를 생성합니다.

public class MyException extends Exception {
    //에러 코드 값을 저장하기 위한 필드를 추가
    private final int ERROR_CODE; //생성자를 통해 초기화
 
    MyException(String message, int errorCode) {
        super(message);
        ERROR_CODE = errorCode;
    }
 
    MyException(String message) {
        this(message, 400); //ERROR_CODE를 400(기본값)으로 초기화
    }
    
    public MyException(String message, Throwable cause, int errorCode) {
        super(message, cause);
        ERROR_CODE = errorCode;
    }
 
    public int getErrorCode() {
        return ERROR_CODE;
    }
}

 

예제는 Exception클래스로부터 상속받아서 MyException클래스를 만든 것 입니다.

예제처럼 필요에 따라 변수나 메서드를 추가할 수 있습니다.

 

Exception클래스는 생성 시에 String 값을 받아 메시지로 저장할 수 있습니다. 따라서 MyException에도 메시지를 매개변수로 받는 생성자를 추가해주었습니다.

 

또한 ERROR_CODE 멤버변수를 추가해 에러코드 값도 저장할 수 있도록하였습니다.

이렇게 함으로써 MyException이 발생했을 때, catch블럭에서 getMessage()와 getErrorCode()를 사용해서 에러코드와 메시지를 모두 얻을 수 있을 것입니다.

 

+) 커스텀한 예외에서의 best practice 중 하나로 커스텀한 예외의 근본 원인을 생성자에 설정하는 것이 좋습니다.

try {
    throw new IOException();
} catch (IOException e) {
    throw new MyException("IOException 발생", e, 500);
}

 

이런 코드가 있다고 할때,

IOException이 발생함으로써 MyException이 발생할 때, 즉 MyException의 원인이 IOException이라면,

MyException의 생성자에 원인 예외를 추가해줘야 이후에 원인을 추적할 때 많은 도움이 됩니다.

 

 

 

 

 

Reference : https://gksdudrb922.tistory.com/156

https://steady-coding.tistory.com/583

https://hahahoho5915.tistory.com/67

 

반응형