5 분 소요


예외 계층

56

  • Object: 예외도 객체이기 때문에 최상위 부모도 Object이다.
  • Throwable: 최상위 예외이다. 하위에 ExceptionError가 있다.
  • Error: 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구 불가능한 시스템 예외이다.
    • 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다.
    • 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡힌다.
    • 따라서, 애플리케이션 로직에서는 Throwable 예외도 잡으면 안되는데, 앞서 얘기한 Error 예외도 함께 잡을 수 있기 때문이다.
    • 이런 이유로 애플리케이션 로직은 Exception부터 필요한 예외로 생각하고 잡으면 된다.
    • 참고로 Error도 언체크 예외이다.
  • Exception: 체크 예외
    • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
    • Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다.
    • 단, RuntimeException은 예외이다.
  • RuntimeException: 언체크 예외, 런타임 예외
    • 컴파일러가 체크하지 않는 언체크 예외이다.
    • RuntimeException과 그 자식 예외는 모두 언체크 예외이다.
      • RuntimeException과 그 하위 예외를 런타임 예외라고 부른다.


예외 기본 규칙

예외는 잡아서 처리하거나, 처리할 수 없으면 밖으로 던져야한다.

예외 처리

57

  • 5번에서 예외를 처리하면 이후에는 애플리케이션 로직이 정상 흐름으로 동작한다.

예외 던짐

58

  • 예외를 처리하지 못하면 호출한 곳으로 예외를 계속 던지게 된다.

2가지 기본 규칙

  • 1.예외는 잡아서 처리하거나 던져야 한다.
  • 2.예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리된다.
    • 예를 들어 Exceptioncatch로 잡으면 그 하위 예외들도 모두 잡을 수 있다.
    • 예를 들어 Exceptionthrows로 던지면 그 하위 예외들도 모두 던질 수 있다.

예외를 처리하지 못하고 계속 던지면?

  • 자바 main() 쓰레드의 경우 예외 로그를 출력하면서 시스템이 종료된다.
  • 웹 애플리케이션의 경우 여러 사용자의 요청을 처리하기 때문에 하나의 예외 때문에 시스템이 종료되면 안된다.
    • WAS가 해당 예외를 받아서 처리하는데, 주로 개발자가 지정한 오류 페이지를 사용자에게 보여준다.


체크 예외 기본 이해

  • Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다.
    • RuntimeException은 제외
  • 체크 예외는 잡아서 처리하거나, 밖으로 던지도록 선언해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.

쳬크 예외 예제

@Slf4j
public class CheckedTest {
    
    @Test
    void checked_catch() {
        Service service = new Service();
        service.callCatch();
    }

    @Test
    void checked_throw() {
        Service service = new Service();
        assertThatThrownBy(() -> service.callThrow())
                .isInstanceOf(MyCheckedException.class);
    }

    /**
    * Exception을 상속받은 예외는 체크 예외가 된다.
    */
    static class MyCheckedException extends Exception {
        public MyCheckedException(String message) {
            super(message);
        }
    }


    /**
    * Checked 예외는
    * 예외를 잡아서 처리하거나, 던지거나 둘중 하나를 필수로 선택해야 한다.
    */
    static class Service {
        Repository repository = new Repository();

        // 예외를 잡아서 처리하는 코드
        public void callCatch() {
            try{
                repository.call();
            } catch(MyCheckedException e) {
                // 예외 처리 로직
                log.info("예외 처리 message={}", e.getMassage(), e);
            }
        }

        /**
        * 체크 예외를 밖으로 던지는 코드
        * 체크 예외는 예외를 잡지 않고 밖으로 던지려면 throws 예외를 메서드에 필수로 선언해야한다.
        */
        public void callThrow() throws MyCheckedException {
            repository.call();
        }

    }

    static class Repository {
        public void call() throws MyCheckedException {
            throw new MyCheckedException("ex");
        }
    }
}
  • MyCheckedExceptionException을 상속받았기 때문에 체크 예외가 됐다.
  • 참고로 RuntimeException을 상속받으면 언체크 예외가 된다. 이런 규칙은 자바 언어에서 문법으로 정한 것이다.
  • 예외가 제공하는 여러가지 기본 기능중에 오류 메시지를 보관하는 기능도 있다.
    • 예제에서 보는 것 처럼 생성자를 통해서 해당 기능을 그대로 사용하면 편리하다.

checked_catch

  • service.callCatch()에서 예외를 처리했기 때문에 테스트 메서드까지 예외가 올라오지 않는다.
  • 체크 예외를 잡아서 처리하려면 catch(..) 를 사용해서 예외를 잡으면 된다.
  • 여기서는 MyCheckedException 예외를 잡아서 처리한다.
  • catchMyCheckedException의 상위 타입인 Exception을 적어주어도 MyCheckedException을 잡을 수 있다.
  • catch에 예외를 지정하면 해당 예외와 그 하위 타입 예외를 모두 잡아준다.
  • 정확하게 MyCheckedException만 잡고 싶다면 catchMyCheckedException을 적어주어야 한다.

Catch 실행 순서

  • 1.test -> service.callCatch() -> repository.call()[예외 발생, 던짐]
  • 2.test <- service.callCatch()[예외 처리] <- repository.call()
  • 3.test[정상 흐름] <- service.callCatch() <- repository.call()

checked_throw

  • service.callThrow()에서 예외를 처리하지 않고, 밖으로 던졌기 때문에 예외가 테스트 메서드까지 올라온다.
  • 테스트에서는 기대한 것 처럼 MyCheckedException 예외가 던져지면 성공으로 처리한다.
  • 체크 예외를 처리할 수 없을 때는 method() throws 예외를 사용해서 밖으로 던질 예외를 필수로 지정해주어야 한다. 여기서는 MyCheckedException을 밖으로 던지도록 지정해주었다.
  • throwsMyCheckedException의 상위 타입인 Exception을 적어주어도 MyCheckedException을 던질 수 있다.
  • throws에 지정한 타입과 그 하위 타입 예외를 밖으로 던진다.
  • 물론 정확하게 MyCheckedException만 밖으로 던지고 싶다면 throwsMyCheckedException을 적어주어야 한다.

Throw 실행 순서

  • 1.test -> service.callThrow() -> repository.call()[예외 발생, 던짐]
  • 2.test <- service.callThrow()[예외 던짐] <- repository.call()
  • 3.test[예외 도착] <- service.callThrow() <- repository.call()

실행 결과

[Test worker] INFO hello.jdbc.exception.basic.CheckedTest - 예외 처리 message=ex
hello.jdbc.exception.basic.CheckedTest$MyCheckedException: ex
    at 
hello.jdbc.exception.basic.CheckedTest$Repository.call(CheckedTest.java:xx)
    at 
hello.jdbc.exception.basic.CheckedTest$Service.callCatch(CheckedTest.java:xx)
    at hello.jdbc.exception.basic.CheckedTest.checked_catch(CheckedTest.java:xx)
  • 실행 결과 첫 줄은 로그가 그대로 남는 것을 확인할 수 있다.
  • 두 번째 줄 부터 예외에 대한 스택 트레이스가 추가로 출력된다.
  • 이 부분은 로그를 남길 때 로그의 마지막 인수에 예외 객체를 전달해주면 로그가 해당 예외의 스택 트레이스를 추가로 출력해주는 것이다.
    • log.info("예외 처리, message={}", e.getMessage(), e); 이 코드에서 마지막에 e부분.

체크 예외의 장단점

체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외를 필수로 선언해야 한다. 그렇지 않으면 컴파일 오류가 발생하고, 이것 때문에 장점과 단점이 동시에 존재한다.

  • 장점: 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 훌륭한 안전 장치이다.
  • 단점: 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야 한다.


언체크 예외 기본 이해

  • RuntimeException과 그 하위 예외는 언체크 예외로 분류된다.
  • 언체크 예외는 컴파일러가 예외를 체크하지 않는 예외다.
  • 언체크 예외는 체크 예외와 기본적으로 동일하지만, 예외를 던지는 throws를 선언하지 않고 생략할 수 있다. 이 경우 자동으로 예외를 던진다.

체크 예외 Vs 언체크 예외

  • 체크 예외: 예외를 잡아서 처리하지 않으면 항상 throws에 던지는 예외를 선언해야 한다.
  • 언체크 예외: 예외를 잡아서 처리하지 않아도 throws를 생략할 수 있다.

언체크 예외 예제

@Slf4j
public class UncheckedTest {

    @Test
    void unchecked_catch() {
        Service service = new Service();
        service.callCatch();
    }

    @Test
    void unchecked_throw() {
        Service service = new Service();
        assertThatThrownBy(() -> service.callThrow())
                .isInstanceOf(MyUncheckedException.class);
    }

    // RuntimeException을 상속받은 예외는 언체크 예외가 된다.
    static class MyUncheckedException extends RuntimeException {
        public MyUncheckedException(String message) {
            super(message);
        }
    }

    /**
    * UnChecked 예외는
    * 예외를 잡거나, 던지지 않아도 된다.
    * 예외를 잡지 않으면 자동으로 밖으로 던진다.
    */

    static class Service {
        
        Repository repository = new Repository();

        // 필요한 경우 예외를 잡아서 처리.
        public void callCatch() {
            try{
                repository.call();
            } catch(MyUncheckedException e) {
                // 예외 처리 로직
                log.info("예외 처리 message={}", e.getMassage(), e);
            }
        }

        /**
        * 예외를 잡지 않아도 된다. 자연스럽게 상위로 넘어간다.
        * 체크 예외와 다르게 throws 예외 선언을 하지 않아도 된다.
        */
        public void callThrow() {
            repository.call();
        }
    }

    static class Repository {
        public void call() {
            throw new MyUncheckedException("ex");
        }
    }
}
  • 언체크 예외도 필요한 경우 잡아서 처리할 수 있다.
  • 언체크 예외는 체크 예외와 다르게 throws 예외를 선언하지 않아도 된다.
    • 말 그래도 컴파일러가 이런 부분을 체크하지 않기 때문에 언체크 예외이다.
  • 언체크 예외도 throws 예외를 선언해도 된다.
  • 언체크 예외는 주로 생략하지만, 중요한 예외의 경우 throws 예외를 선언해두면 해당 코드를 호출하는 개발자가 이런 예외가 발생한다는 점을 IDE를 통해 좀 더 편리하게 인지할 수 있다.

언체크 예외의 장단점

언체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외를 생략할 수 있다.

  • 장점: 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 체크 예외의 경우 처리할 수 없는 예외를 밖으로 던지려면 항상 throws 예외를 선언해야 했지만, 언체크 예외는 이 부분을 생략할 수 있다.
  • 단점: 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 반면에 체크 예외는 컴파일러를 통해 예외 누락을 잡아준다.


<출처 : 인프런 - 스프링 DB 1편 : 데이터 접근 핵심 원리(김영한)>

댓글남기기