[Java] 자바의 Exception 관련 기본 개념을 알아보자

@melonturtle · February 04, 2023 · 12 min read

이번 글에선 우선 자바에서 Exception에 대한 기본 개념들을 알아보고 다음 글에서 어떻게하면 코드에서 잘 쓸 수 있을 지 알아보자.

프로그램 에러

  • 프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우, 그 때 원인을 프로그램 에러 또는 오류라고 한다.

프로그램 에러의 종류

  1. 컴파일 에러(compile-time error)
  2. 런타임 에러(runtime error)
  3. 논리적 에러(logical error)

    컴파일도 잘되고 실행도 잘되지만 의도와 다르게 동작

    • 예: 상품 재고가 음수가 됨

실행 도중 발생하는 에러

컴파일을 정상적으로 마쳐도 프로그램 실행 시에 에러가 발생할 수 있다. 이런 런타임 에러를 방지하기 위해서는 프로그램의 실행도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 해야 한다.

자바에선 런타임에 발생할 수 있는 오류를 Error와 Exception 두 가지로 구분했다.

  1. Error - 에러

    일단 발생하면 복구할 수 없는 심각한 오류

    예) OutOfMemoryError, StackOverflowError

  2. Exception - 예외

    발생하더라도 수습할 수 있는 정도. 프로그래머가 미리 적절한 코드를 작성해놓으면 프로그램의 비정상적인 종료를 막을 수 있음

Exception과 Error 클래스의 상속 구조

throwable hierarchy

  • 자바에선 실행 시 발생할 수 있는 오류(Exception과 Error)도 객체이다. 그리고 모든 클래스의 조상은 Object이므로, Exception과 Error클래스도 Obejct의 자손이다.
  • Throwable 클래스는 모든 에러와 예외의 super클래스다. Throwable을 상속한 타입이여야 자바의 throw문에서 던져질 수 있고, catch절의 인자로 전달될 수 있다. Throwable은 만들어졌을 때 쓰레드의 execution stack의 스냅샷을 갖고 있어 StackTrace 정보를 갖고 있다.

    • throwable을 상속해서 쓸 수도 있지만 사용하지 말자.

Error

error hierarchy

시스템 레벨에서 발생하여 개발자가 어떻게 할 수 없는 것

  • OutOfMemoryError: JVM에 설정된 메모리의 한계 벗어남

    • 힙 사이즈 부족, 너무 많은 class 로드, 가용가능한 swap이 없을 때

    이를 해결하기 위해 dump 파일 분석, jvm 옵션 수정등을 할 수 있다.

Exception

exception hierarchy

  • 모든 Exception의 최고 조상은 Exception 클래스이다.
  • 예외 클래스는 또 두 개의 그룹으로 나눠질 수 있다.

    1. RuntimeException 클래스와 자손들
    2. RuntimeException 클래스와 자손들이 아닌, Exception 클래스와 자손들

RuntimeException 클래스들: Unchecked Excpetion

  • 주로 프로그래머의 실수에 의해서 발생 가능
    • IndexOutOfBoundsException: 자바의 배열 범위 벗어남
    • NullPointerException: null 참조
    • ClassCastException: 변환할 수 없는 클래스로 변환
    • ArithmeticException: 정수를 0으로 나눔
    • IllegalArgumentException: 잘못된 인자 전달 시 발생
    • IllegalStateException: 객체의 상태가 메소드 호출에는 부적절
    • UnsupportedOperationException: 객체가 요청된 operation을 지원하지 않는 경우
  • Unchecked: 예외처리를 필수적으로 하지 않아도 컴파일 된다
  • Error와 그 자손들도 Unchecked 에러이다. try-catch 블럭으로 처리할 수 없기 때문이다.

    • 에러는 보통 JVM이 자원 부족 등 더 이상 수행을 계속할 수 없는 상황을 나타낼 때 사용한다. 따라서 Error 클래스를 상속해 하위 클래스를 만드는 일은 자제하자.

Exception 클래스들: Checked Exception

  • 주로 외부의 영향, 프로그램 사용자의 동작에 의해 발생
    • IOException
    • FileNotFoundException
    • ClassNotFoundException
    • DataFormatException
  • Checked: 예외 처리를 하지 않으면 컴파일 되지 않는다.

Exception Handling

  • 정의: 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것
  • 목적: 예외의 발생으로 인한 실행 중인 프로그램의 갑작스런 종료를 막고, 정상적인 실행상태를 유지할 수 있도록 하는 것

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

예외 발생 시엔 직접 try-catch문을 이용해 처리하거나, 예외를 던져서 호출하는 쪽이 책임을 지게 할 수 있다.

try-catch

  • if문과 달리 try나 catch 블럭 내에 포함된 문장이 하나뿐이어도 괄호를 생략할 수 없다.
  • if-else처럼 catch 문을 여러개 적을 수 있고 예외의 종류와 일치하는 catch 블럭이 있으면 예외가 처리된다.
  • 예외가 발생하면 try 블럭에서 나머지는 실행되지 않는다.
  • 처리 과정

    • 예외가 발생하면 발생한 예외에 해당하는 클래스의 인스턴스가 만들어 진다. 그리고 예외가 발생한 문장이 try-catch문의 try블럭에 포함되어 있다면 이 예외를 처리할 수 있는 catch블럭이 있는지 찾는다.
    • 첫번째 catch블럭부터 내려가면서 catch블럭의 괄호내에 선언된 참조변수의 종류와, 생성된 예외 인스턴스에 instanceof 연산자를 이용해 검사 결과가 true가 나올 때까지 반복한다.
    • 모든 예외는 Exception 클래스의 자손이므로 Exception 클래스 타입의 참조 변수를 선언해놓으면 어떤 예외가 발생하더라도 처리된다.

Method

try-catch 시 유용하게 쓸 수 있는 Exception 메서드들

  • printStackTrace(): 예외 발생 당시의 Call Stack에 있었던 메서드의 정보와 예외 메세지 출력
  • getMessage(): 발생한 예외클래스의 인스턴스에 저장된 메세지 String 반환
  • getStackTrace(): printStackTrace를 보완하여 StackTraceElement[]라는 문자열 배열로 변경해서 출력하고 저장

멀티 catch블럭

| 기호를 이용해서 여러 catch 블럭을 하나로 합칠 수 있다. (|는 여기서 논리 연산자가 아닌 기호이다.) 개수에는 제한이 없다.

try {
  ...
} catch (ExceptionA e | ExceptionB e) {
  e.printStackTrace();
}
  • | 기호로 연결된 예외 클래스가 조상과 자손 관계라면 컴파일 에러. 조상 클래스만 적어도 되므로 불필요한 코드라는 의미의 에러다.

예외 발생시키기 throw

throw를 사용해서 고의로 예외를 발생시킬 수 있다.

throw new Exception("메세지");
// 혹은
Exception e = new Exception("메세지");
throw e;

이 때 생성자에 String을 넣어 주면, 이 String이 Exception 인스턴스에 메세지로 저장되며, getMessage() 를 통해 얻을 수 있다.

메서드에 예외 선언 throws

예외 처리를 현재 메소드가 직접 처리하지 않고 호출한 곳에다가 예외의 발생 여부를 통보한다.

void method() throws Exception1, Exception2 {
  //...
}

일반적으로 메서드에 예외 선언 시 RuntimeException들은 적지 않는다. 적어도 상관은 없지만 처리를 안해줘도 되므로 반드시 처리해주어야 하는 예외만 선언한다.

이 때 Checked Exception이 메서드의 선언부에 명시되어 있다면, 예외를 던진 계층부터 처리한 계층까지의 모든 메소드에서 시그니처를 수정하고 처리를 해줘야 한다.

메서드 내부에서 처리해도 되는 건 자체적으로 try-catch로 처리하고, 메서드의 인자가 잘못되어 다시 받아야 하는 것 같이 메서드 내에서 자체적으로 해결이 안될 때 예외를 메서드에 선언해서 호출한 메서드에서 처리하도록 할 수 있다.

  • 메서드 내부와 호출한 쪽에서 모두 처리해야할 경우엔, 메서드 내부 catch블럭에서 처리 후 또 throw를 하여 호출한 쪽에서도 처리하게 할 수 있다. (exception re-throwing)

chained exception

한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면 A를 B의 원인 예외라고 한다. 이 때 원인 예외를 지정해서 다른 예외를 발생시킬 수 있다.

try {

} catch (SpaceException e) {
  // 새로운 예외 생성
  InstallException ie = new InstallException("설치중 예외 발생");
  // 발생한 예외를 원인 예외로 지정한다.
  ie.initCause(e);
  throw ie;
}

이 방식은 checked예외를 unchecked 예외로 바꾸기 위해서도 사용할 수 있다.

throw new RuntimeException(new ChecekdException(".."));

initCause 대신에 RuntimeException의 생성자로 Throwable 타입의 원인을 넘길 수도 있다.

사용자 정의 예외

  • Exception이나 RuntimeException을 상속해서 예외 클래스를 만들 수 있다. 하지만 가능하면 새로운 예외 클래스를 만들기보다 기존의 예외 클래스를 활용하자.
  • 이 때 Exception을 상속해야할까 RuntimeException을 상속해야할까?

    • Exception을 상속하면 Checked예외로 꼭 처리해줘야 한다.
    • 다음 글에서 한번 알아보자!

다음 글

이제 기본 개념을 알아봤으니 잘 활용하는 방법을 알아보자

@melonturtle
공부하면서 남긴 기록입니다. 문제가 있다면 언제든지 알려주세요!