ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [김영한 스프링] 19. 자바 예외 이해 - 체크, 언체크 예외 기본 이해
    Spring/스프링 DB 1편 - 데이터 접근 핵심 원리 2023. 12. 30. 02:37

    체크 예외 기본 이해

     

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

     

     

    체크 예외 전체 코드 - CheckedTest

    test/java/hello/jdbc/exception/basic/CheckedTest 생성

     

     

    package hello.jdbc.exception.basic;
    
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    
    import static org.assertj.core.api.Assertions.*;
    
    @Slf4j
    public class CheckedTest {
    
        @Test
        void checked_chatch() {
            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.getMessage(), e);
                }
            }
    
            /**
             * 체크 예외를 밖으로 던지는 코드
             * 체크 예외는 예외를 잡지 않고 밖으로 던지려는 throws 예외를 메서드에 필수로 선언해야한다.
             * @throws MyCheckedException
             */
            public void callThrow() throws MyCheckedException {
                repository.call();
            }
        }
    
        static class Repository {
            public void call() throws MyCheckedException {
                throw new MyCheckedException("ex");
            }
        }
    }

     

    Exception을 상속받은 예외는 체크 예외가 된다.

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

     

    먼저 예외를 잡아서 처리하는 코드를 실행해 보자.

    @Test
    void checked_catch() {
        Service service = new Service();
        service.callCatch();
    }
    • service.callCatch()에서 예외를 처리했기 때문에 테스트 메서드까지 예외가 올라오지 않는다.

     

    실행 순서를 분석해 보자.

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

     

     

    실행

    • 실행 결과 로그를 보면 첫 줄은 우리가 남긴 로그가 그대로 남는 것을 확인할 수 있다.
    • 그런데 두 번째 줄부터 예외에 대한 스택 트레이스가 추가로 출력된다.
    • 이 부분은 로그를 남길 때 로그의 마지막 인수에 예외 객체를 전달해 주면 로그가 해당 예외의 스택 트레이스를 추가로 출력해 주는 것이다.
      • log.info("예외 처리, message={}", e.getMessage(), e); <- 여기서 마지막에 있는 e 부분이다.

     

    체크 예외를 잡아서 처리하는 코드

    try {
        repository.call();
    } catch (MyCheckedException e) {
        //예외 처리 로직
    }
    • 체크 예외를 잡아서 처리하려면 catch(..)를 사용해서 예외를 잡으면 된다.
    • 여기서는 MyCheckedException 예외를 잡아서 처리한다.

     

    catch는 해당 타입과 그 하위 타입을 모두 잡을 수 있다.

    public void callCatch() {
        try {
            repository.call();
        } catch (Exception e) {
            //예외 처리 로직
        }
    }
    • catch에 MyCheckedException의 상위 타입인 Exception을 적어주어도 MyCheckedException을 잡을 수 있다.
    • catch에 예외를 지정하면 해당 예외와 그 하위 타입 예외를 모두 잡아준다.
    • 물론 정확하게 MyCheckedException만 잡고 싶다면 catch에 MyCheckedException을 적어주어야 한다.

     

    예외를 처리하지 않고, 밖으로 던지는 코드를 살펴보자.

    @Test
    void checked_throw() {
        Service service = new Service();
        assertThatThrownBy(() -> service.callThrow())
            .isInstanceOf(MyCheckedException.class);
    }
    • service.callThrow()에서 예외를 처리하지 않고, 밖으로 던졌기 때문에 예외가 테스트 메서드까지 올라온다.
    • 테스트에서는 기대한 것처럼 MyCheckedException 예외가 던져지면 성공으로 처리한다.

     

    실행 순서를 분석해 보자.

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

     

    체크 예외를 밖으로 던지는 코드

    public void callThrow() throws MyCheckedException {
        repository.call();
    }
    • 체크 예외를 처리할 수 없을 때는 method() throws 예외를 사용해서 밖으로 던질 예외를 필수로 지정해 주어야 한다. 여기서는 MyCheckedException을 밖으로 던지도록 지정해 주었다.

     

    체크 예외를 밖으로 던지지 않으면 컴파일 오류 발생

    public void callThrow() {
        repository.call();
    }
    • throws를 지정하지 않으면 컴파일 오류가 발생한다.
      • Unhandled exception: hello.jdbc.exception.basic.CheckedTest.MyCheckedException
    • 체크 예외의 경우 예외를 잡아서 처리하거나 또는 throws를 지정해서 예외를 밖으로 던진다는 선언을 필수로 해주어야 한다.

     

    참고로 체크 예외를 밖으로 던지는 경우에도 해당 타입과 그 하위 타입을 모두 던질 수 있다

    public void callThrow() throws Exception {
        repository.call();
    }
    • throws에 MyCheckedException의 상위 타입인 Exception을 적어주어도 MyCheckedException을 던질 수 있다.
    • throws에 지정한 타입과 그 하위 타입 예외를 밖으로 던진다.
    • 물론 정확하게 MyCheckedException만 밖으로 던지고 싶다면 throws에 MyCheckedException을 적어주어야 한다.

     

     

    체크 예외의 장단점

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

    • 장점 : 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 훌륭한 안전장치이다.
    • 단점 : 하지만 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 된다. 크게 신경 쓰고 싶지 않은 예외까지 모두 챙겨야 한다. 추가로 의존관계에 따른 단점도 있는데 이 부분은 뒤에서 설명하겠다.

     

     

    언체크 예외 기본 이해

     

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

     

    체크 예외 VS 언체크 예외

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

     

     

    언체크 예외 전체 코드 - UncheckedTest

    test/java/hello/jdbc/exception/basic/UncheckedTest 생성

     

     

    package hello.jdbc.exception.basic;
    
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    
    import static org.assertj.core.api.Assertions.*;
    
    @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.getMessage(), e);
                }
            }
    
            /**
             * 예외를 잡지 않아도 된다. 자연스럽게 상위로 넘어간다.
             * 체크 예외와 다르게 throws 예외 선언을 하지 않아도 된다.
             */
            public void callThrow() {
                repository.call();
            }
        }
    
        static class Repository {
            public void call() {
                throw new MyUncheckedException("ex");
            }
        }
    }

     

    언체크 예외를 잡아서 처리하는 코드

    try {
        repository.call();
    } catch (MyUncheckedException e) {
        //예외 처리 로직
        log.info("error", e);
    }
    • 언체크 예외도 필요한 경우 이렇게 잡아서 처리할 수 있다.

     

    언체크 예외를 밖으로 던지는 코드 - 생략

    public void callThrow() {
        repository.call();
    }
    • 언체크 예외는 체크 예외와 다르게 throws 예외를 선언하지 않아도 된다.
    • 말 그대로 컴파일러가 이런 부분을 체크하지 않기 때문에 언체크 예외이다.

     

    언체크 예외를 밖으로 던지는 코드 - 선언

    public void callThrow() throws MyUncheckedException {
        repository.call();
    }
    • 참고로 언체크 예외도 throws 예외를 선언해도 된다. 물론 생략할 수 있다.
    • 언체크 예외는 주로 생략하지만, 중요한 예외의 경우 이렇게 선언해 두면 해당 코드를 호출하는 개발자가 이런 예외가 발생한다는 점을 IDE를 통해 좀 더 편리하게 인지할 수 있다.(컴파일 시점에 막을 수 있는 것은 아니고, IDE를 통해서 인지할 수 있는 정도이다.)

     

     

    언체크 예외의 장단점

    언체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외를 생략할 수 있다. 이것 때문에 장점과 단점이 동시에 존재한다.

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

     

     

    정리

    체크 예외와 언체크 예외의 차이는 사실 예외를 처리할 수 없을 때 예외를 밖으로 던지는 부분에 있다. 이 부분을 필수로 선언해야 하는가 생략할 수 있는가의 차이다.

     

     

    출처 :  https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1

     

    스프링 DB 1편 - 데이터 접근 핵심 원리 강의 - 인프런

    백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔

    www.inflearn.com

Designed by Tistory.