Spring DB : 체크 예외 / 언체크 예외

    이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다. 

    예외 계층

    스프링은 예외 추상화를 통해 Repository 계층에서 Service 계층으로 예외가 누수되는 문제를 해결해준다. 스프링의 예외 추상화를 공부하기 전에 먼저 자바의 기본적인 예외와 관련된 내용을 정리하고자 한다. 



    예외 계층 살펴보기

    • 자바 예외는 Throwable 클래스에서 시작된다. Throwable은 각각 Exception / Error 클래스로 나누어진다.
    • 상위 예외를 Catch로 잡으면 하위 계층의 예외까지 함께 잡힌다.
    • Throwable은 Catch로 잡으면 안된다. Error 예외까지 함께 잡히기 때문이다.Exception 계층부터 예외를 잡는다. 
    • Error
      • 메모리 부족 / 심각한 시스템 오류처럼 어플리케이션이 복구 불가능한 시스템 예외다. 이런 예외는 어플리케이션에서 처리할 수 없기 때문에 개발자는 이 예외가 발생해도 아무런 조치를 하면 안된다.어플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외다.
    • Exception
      • Exception을 상속받은 모든 클래스는 체크 예외다.
      • 체크 예외는 컴파일러가 컴파일 전, 예외가 존재하는지 체크해준다. 
    • RuntimeException
      • Exception의 하위 예외지만, RuntimeException은 언체크 예외다.
      • RuntimeException의 모든 자식 클래스는 언체크 예외가 된다.
      • 언체크 예외는 컴파일러가 체크하지 않는 예외다.

    즉, 자바에서는 Exception 예외를 통해 어플리케이션 내에서 예외를 처리한다. 그리고 이 예외는 체크 예외와 언체크 예외로 나뉘는데, 체크 예외는 컴파일러가 컴파일 전에 체크를 해준다. 언체크 예외는 컴파일러가 예외가 발생하든 말든 상관없이 내버려둔다. 

     

    예외 기본 규칙

    • 예외를 Catch해서 처리해준다.
    • 예외를 처리할 수 없으면 Throws를 이용해 상위로 던진다.

    예외는 위 두 가지 중 반드시 하나로 처리를 해야한다.

    1. Controller → Service → Repository 순서로 호출된다.
    2. Repository 계층에서 예외가 발생된다. Repository는 예외를 처리할 수 없어 호출한 Service로 예외를 던진다.
    3. Service는 던져진 예외를 잡아서(Catch) 처리해준다. 예외가 처리되었기 때문에 정상적인 흐름으로 어플리케이션이 동작한다. 

    1. Controller → Service → Repository 순으로 호출된다.
    2. Repository에서 예외 발생한다. 처리할 수 없어 Service 계층으로 던진다.
    3. Service 계층도 예외를 처리할 수 없어 Controller 계층으로 던진다. 

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

    예외를 처리하지 못하고, 계속 던지면 어떻게 될까?

    • 자바 main() 쓰레드
      • 예외 로그를 출력하면서 시스템이 종료된다.
    • Web Application
      • 여러 사용자의 요청을 처리하기 때문에 하나의 잘못된 요청으로 시스템이 종료되면 안된다. WAS가 예외를 받아서 처리하는데, 주로 사용자에게 오류 페이지를 보여준다.

     

    체크 예외 기본 이해

    Exception + 하위 예외는 체크 예외다. 이 체크 예외는 컴파일러가 컴파일 하기 전, 오류가 있는지를 확인한다. 체크 예외는 컴파일 전에 표시가 되며, 컴파일 하기 전에 모든 예외를 Catch로 처리 / Throw로 던지거나를 처리해야한다. 그렇게 하지 않을 경우 컴파일 오류가 발생한다.



    체크 예외 코드 실습

    1. 체크 예외를 처리하지 않으면 계속 상위층으로 Throw된다.
    2. 체크 예외를 Catch해서 처리하는 경우 예외를 Throw 하지 않아도 된다.

    체크 예외 코드 실습을 했다. 이 코드 실습에서는 두 가지 목표를 가진다.

    MyCheckedException : 체크 예외 작성

    /**
     * Exception 상속 예외 → 체크 예외
     */
    static class MyCheckedException extends Exception{
        public MyCheckedException(String message) {
            super(message);
        }
    }
    • Exception을 상속받은 MyCheckedException을 만든다.
    • Exception을 상속받았기 때문에 이 예외는 체크 예외가 된다. 따라서 컴파일 시점에 예외 처리를 완료해야한다.

     

    Repository 클래스 생성

    static class Repository{
        public void call() throws MyCheckedException {
            throw new MyCheckedException("ex");
        }
    }
    • Repository 클래스를 생성한다.
    • Repository 클래스는 call() 메서드가 호출되면 MyCheckedException을 던져준다.

     

    Service 클래스 생성

    static class Service{
        Repository repository = new Repository();
    
        /**
         * Checked 예외는 Catch / Throw 하나를 선택해야함.
         */
    
        public void callCatch() {
            try {
                repository.call();
            } catch (MyCheckedException e) {
                log.info("예외 처리, message = {}", e.getMessage());
            }
        }
    
        public void callThrow() throws MyCheckedException {
            repository.call();
        }
    }
    • Service 클래스는 Repository.call()을 통해 MyCheckedException을 불러온다.
    • 두 가지 메서드를 만든다
      • callCatch() : Repository에서 던져진 Exception을 Catch로 처리한다.
      • callThrow() : Repository에서 던져진 Exception을 throws로 던진다.

     

    테스트 코드 작성

    @Test
    void checked_Catch() {
        Service service = new Service();
        service.callCatch();
    }
    
    @Test
    void checked_Throw() {
        Service service = new Service();
        assertThatThrownBy(() -> service.callThrow()).isInstanceOf(MyCheckedException.class);
    }
    • 다음과 같이 테스트 코드를 작성한다.
    • checked_Catch()
      • callCatch()를 호출한다. 이 메서드는 서비스 영역에서 Repository에서 발생한 체크 예외를 처리해준다. 따라서 테스트 코드에서는 예외 처리를 위한 추가 로직을 처리할 필요가 없다.
    • checked_Throw()
      • callThrow()를 호출한다. 이 메서드는 Repository에서 발생한 예외를 Service 영역에서도 던져준다. 따라서 테스트 코드까지 예외가 올라온다.

     

    정리

    • Exception을 상속 받은 예외는 체크 예외가 된다.
    • RunTimeException을 상속 받은 예외는 언체크 예외가 된다.
    • 예외를 Catch / Throw 할 때 상위 타입의 예외를 던지면 하위 타입도 함께 처리가 된다.
      • Exception을 Throw 하는 것은 좋지 않다. 내가 처리할 수 있는 로직까지도 모두 Throw하기 때문이다. 따라서 내가 던질 예외만 명시적으로 작성하는 것이 좋은 코드가 된다.

     

    체크 예외의 장/단점

    체크 예외는 컴파일러가 컴파일 시점에 예외가 발생하는 것을 확인한다. 개발자는 이 예외를 Throw / Catch로 처리를 해줘야 정상적으로 컴파일을 처리할 수 있다.

    •  장점
      • 컴파일러가 개발자가 실수로 예외를 누락하지 않도록 돕는다.
    • 단점
      • 개발자가 모든 체크 예외를 반드시 처리해야한다. 신경 써도 아무 의미가 없는 예외까지 모두 신경을 써야하게 된다. 
      • 의존관계에 따른 단점도 존재한다. (Repository에서 발생한 SQL Exception이 Service 계층으로 올라옴)

     

     

    언체크 예외

    • 언체크 예외는 기본적으로 체크 예외와 동일하지만, 컴파일러가 체크하지 않는다는 점이 다르다.
    • 언체크 예외는 컴파일러가 체크하지 않기 때문에 Catch / Throws가 강제되지 않는다.
    • 언체크 예외가 발생해서 Catch되지 않을 경우 자동으로 Throws 된다.

    정리하면 체크 예외는 컴파일 시점에 반드시 예외를 Catch / Throws를 해야한다. 반면 언체크 예외는 컴파일 시점에 처리되지 않은 예외가 발생할 경우, 자동으로 Throws가 된다는 점이다. 

    코드 실습

    이번에는 언체크 예외를 하나 만들어, 체크 예외와 동일하게 테스트를 해보려고 한다. 체크 예외와 다르게 언체크 예외에는 Throws 메서드가 강제되지 않는 것을 확인한다. 

    MyUncheckedException 클래스 생성

     

    /**
     * RuntimeException 상속 예외 → 언체크 예외
     */
    static class MyUnCheckedException extends RuntimeException{
        public MyUnCheckedException(String message) {
            super(message);
        }
    }
    • RuntimeException을 상속받은 예외를 만든다.
    • RuntimeException을 상속받았기 때문에 이 예외는 언체크 예외가 된다.

     

    Repository 클래스 생성

    static class Repository{
        public void call() throws MyUnCheckedException {
            throw new MyUnCheckedException("ex");
        }
    }
    • Repository 클래스는 언체크 예외를 생성해서 던져준다.

     

    Service 클래스 생성

    static class Service{
        Repository repository = new Repository();
    
        public void callCatch() {
            try {
                repository.call();
            } catch (MyUnCheckedException e) {
                log.info("예외 처리, message = {}", e.getMessage());
            }
        }
    
        public void callThrow() {
            repository.call();
        }
    }
    • Catch를 하는 부분은 체크 예외와 동일하다. 단, 강제 되지는 않는다. 잡고 싶지 않으면 잡지 않아도 된다. 
    • Throw를 하는 부분은 반드시 필수적이지는 않다. 왜냐하면 언체크 예외는 명시적으로 예외를 잡거나 던지지 않으면, 자동으로 Throws를 해준다.

     

    테스트 코드 작성

    @Test
    void unChecked_Catch() {
        Service service = new Service();
        service.callCatch();
    }
    
    @Test
    void unChecked_Throw() {
        Service service = new Service();
        assertThatThrownBy(() -> service.callThrow()).isInstanceOf(MyUnCheckedException.class);
    }
    • callCatch()는 Service에서 예외가 처리된다.
    • callThrow()에서는 Service에서 명시적으로 예외를 던지지 않았는데, 자동으로 던져진다. 즉, 언체크 예외는 개발자가 직접 모든 예외를 신경쓰지 않아도 된다는 것을 보여준다.

     

    언체크 예외의 장/단점

    언체크 예외는 주로 생략을 한다. 그렇지만 중요한 예외의 경우 throws에 개발자가 명시해둘 수 있다. 이렇게 하면 특정 언체크 예외를 개발자가 알아보고 좀 더 신경을 써서 처리할 수 있다는 장점이 있다.

    • 장점
      • 신경쓰고 싶지 않은 언체크 예외는 모두 무시할 수 있다. 
      • 언체크 예외는 기본적으로 현재 위치에서 Catch / Throw가 되지 않는 경우 자동으로 상위 계층으로 Throw 되기 때문이다. 
    • 단점
      • 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 즉, 런타임 시점에 에러가 발생할 수 있다.

    댓글

    Designed by JB FACTORY