GitHub

https://github.com/Choidongjun0830

Java

자바의 예외

gogi masidda 2024. 2. 14. 17:11

자바 클래스 구성

  • Object: 예외도 객체. 모든 객체의 최상의 부모는 Object이다.
  • Throwable: 최상위 예외. 
  •  Error
    • 메모리 부족, 스택오버플로우
    • JVM이나 하드웨어 등 시스템의 문제로 발생하는 것. 애플리케이션 내 코드로 해결할 수 없다.
    • 상위 예외를 catch로 잡으면 그 하위 예외까지 잡는다. 따라서 상위 예외인 Throwable도 잡으면 Error도 함께 잡을 수 있기 때문에 잡으면 안된다. Exception부터 필요한 예외라고 생각하고 잡으면 된다.
  • Exception
    • 사용자의 잘못된 조작이나 개발자의 코딩 로직 실수로 인해 발생하는 프로그램 오류
    • 예외처리 코드를 통해 프로그램을 종료하지 않고 다시 정상 실행상태가 되도록 할 수 있다.
    • Checked Exception과 Unchecked Exception으로 나뉜다.
  • Checked Exception
    • 반드시 명시적인 예외 처리를 해야함. 명시적인 예외 처리란 try-catch, throws를 말함. 
      • try-catch: 예외를 잡아서 그 자리에서 처리
      • throws: 메서드가 예외를 발생시킨다고 기술
    • 컴파일 시점에서 예외 발생과 제대로 처리했는지 확인한다.
    • 장점: 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 안전장치
    • 단점: 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 된다. 크게 신경쓰고 싶지 않은 예외까지도 신경써야 한다. 
    • 대표적으로 IOException, SQLException이 있다.
  • Unchecked Exception
    • 컴파일러가 체크하지 않는 언체크 예외. 런타임 예외라고도 불린다. 
    • Runtime Exception을 상속하는 클래스. 컴파일 후 런타임 시에 발생할 수 있는 예외. 명시적인 예외 처리를 해주지 않아도 컴파일이 된다.
    • 개발자가 예외 처리 코드의 추가 여부를 결정한다.
    • 장점: 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 
    • 단점: 개발자가 실수로 누락할 수 있다.
    • 대표적으로 NullPointerException, IndexOutOfBoundsException. IllegalArgumentException이 있다.

예외의 기본 규칙

  1. 예외는 잡아서 처리하거나, 처리할 수 없으면 밖으로 던져야 한다.
    1. ex) 리포지토리에서 예외가 발생했는데 그것을 리포지토리에서 처리할 수 없으면 서비스 계층으로 예외를 던진다. 그리고 서비스 계층에서 예외처리가 가능하면 서비스 계층에서 처리하고, 컨트롤러에게는 정상 흐름을 반환한다. 
    2. 하지만 예외를 계속 처리하지 못하면 호출한 곳으로 예외를 계속 던지게 된다. 
  2. 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리된다.

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

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

언제 체크 예외를 사용하고, 언제 언체크 예외를 사용하는지

기본 원칙

  • 기본적으로 언체크(런타임) 예외를 사용
  • 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용
    • 해당 예외를 잡아서 반드시 처리해야 하는 문제일 때만 체크 예외를 사용해야함.
    • 예시 -> 이 경우에 체크 예외로 만들어두면 컴파일러를 통해 놓친 예외를 인지할 수 있다. 
      • 계좌 이체 실패 예외
      • 결제시 포인트 부족 예외
      • 로그인 ID, PW 불일치 예외

 

체크 예외 문제점

1. 복구 불가능한 예외: 대부분의 예외는 복구 불가능하다. SQLException은 SQL 문법 문제나 데이터베이스 서버 다운이나, 데이터베이스 자체에 문제가 발생한 경우 등의 문제가 있을 때 발생한다. 이런 문제들은 대부분 복구가 불가능하다. 대부분의 서비스나 컨트롤러는 이런 문제를 해결할 수 없어서 개발자가 빠르게 오류를 인지하는 것이 중요하다. 

2. 의존 관계 문제: 체크 예외이기 때문에 컨트롤러나 서비스 입장에서는 본인이 처리할 수 없어도 어쩔 수 없이 throws를 통해 던지는 예외를 선언해야 한다.

서비스에서 SQLException을 던지는 부분을 선언하고, JDBC가 아니라 JPA로 기술을 바꾼다면, SQLException이 아니라 JPAException에 의존하도록 고쳐야 한다. 서비스나 컨트롤러는 어차피 본인이 해결할 수 없는 예외인데 의존해야하는 큰 문제가 생긴다. 

Exception을 아예 던져버리면 해결되긴 하지만, 다른 예외도 함께 던져버린다. 그래서 하면 안된다. 

 

언체크 예외

public class UnCheckedAppTest {

    @Test
    void unchecked() {
        Controller controller = new Controller();
        Assertions.assertThatThrownBy(() -> controller.request())
                .isInstanceOf(RuntimeException.class);
    }

    static class Controller {
        Service service = new Service();

        public void request(){
            service.logic();
        }
    }

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

        public void logic(){
            repository.call();
            networkClient.call();
        }
    }

    static class NetworkClient {
        public void call(){
            throw new RuntimeConnectException("연결 실패");
        }
    }

    static class Repository {
        public void call(){
            try {
                runSQL();
            } catch (SQLException e) {
                throw new RuntimeSQLException(e);
            }
        }

        public void runSQL() throws SQLException {
            throw new SQLException("ex");
        }
    }

    static class RuntimeConnectException extends RuntimeException {
        public RuntimeConnectException(String message) {
            super(message);
        }
    }

    static class RuntimeSQLException extends RuntimeException {
        public RuntimeSQLException(Throwable cause) {
            super(cause);
        }
    }
}

SQLException을 RuntimeSQLException으로 전환해서 예외를 던진다.

시스템에서 발생한 예외는 대부분 복구 불가능 예외이다. 런타임 예외를 사용하면 서비스나 컨트롤러가 복구 불가능한 예외를 신경쓰지 않아도 된다. 복구 불가능한 예외는 일관성있게 공통으로 처리해야 한다.

런타임 예외는 해당 객체가 처리할 수 없는 예외는 무시하면 된다. 따라서 체크 예외처럼 예외를 강제로 의존하지 않아도 된다.

런타임 예외는 놓칠 수도 있어서 문서화가 중요하다.

 


예외 포함과 스택 트레이스

예외를 전환할 때는 꼭 기존 예외를 포함해야 한다. 그렇지 않으면 스택 트레이스를 확인할 때 문제가 발생한다.

 

static class RuntimeSQLException extends RuntimeException {
        public RuntimeSQLException(Throwable cause) {
            super(cause);
        }
    }

이렇게 RuntimeSQLException을 선언하고, 

 

static class Repository {
        public void call(){
            try {
                runSQL();
            } catch (SQLException e) {
                throw new RuntimeSQLException(e);
            }
        }

        public void runSQL() throws SQLException {
            throw new SQLException("ex");
        }
    }

이렇게 SQLException이 들어왔을 때 RuntimeSQLException으로 돌렸다.

 

@Test
    void printEx() {
        Controller controller = new Controller();
        try {
            controller.request();
        } catch (Exception e) {
            log.info("ex", e); //스택 트레이스 출력
        }
    }

    static class Controller {
        Service service = new Service();

        public void request(){
            service.logic();
        }
    }

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

        public void logic(){
            repository.call();
            networkClient.call();
        }
    }

그리고 이렇게 스택 트레이스를 출력하면, 결과로

[Test worker] INFO h.j.exception.basic.UnCheckedAppTest -- ex
hello.jdbc.exception.basic.UnCheckedAppTest$RuntimeSQLException: java.sql.SQLException: ex at hello.jdbc.exception.basic.UnCheckedAppTest$Repository.call(UnCheckedAppTest.java:58) at hello.jdbc.exception.basic.UnCheckedAppTest$Service.logic(UnCheckedAppTest.java:42) at hello.jdbc.exception.basic.UnCheckedAppTest$Controller.request(UnCheckedAppTest.java:33) at hello.jdbc.exception.basic.UnCheckedAppTest.printEx(UnCheckedAppTest.java:23)
...
Caused by: java.sql.SQLException: ex at hello.jdbc.exception.basic.UnCheckedAppTest$Repository.runSQL(UnCheckedAppTest.java:63) at hello.jdbc.exception.basic.UnCheckedAppTest$Repository.call(UnCheckedAppTest.java:56)

이렇게 출력된다. RuntimeSQLException의 생성자로 cause를 넣어주기 때문에 SQLException으로부터 발생한 예외임을 알 수 있다. 하지만 이것을 넣어주지 않으면 예외를 제대로 확인할 수 없게 된다.

728x90