예외 기본 계층
Throwable : 최상위 예외
Error : 애플리케이션에서 복구 불가능한 시스템 예외이다. 이 예외를 잡으려고 해서는 안된다.
- 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡는다. 따라서 throwable 예외를 잡으면 안 되는데 이는 Error 예외도 함께 잡을 수 있기 때문이다. 따라서 Exception부터 필요한 예외로 생각하고 잡는다.
Exception : 컴파일러가 체크하는 체크 예외이다. 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외로 Exception과 그 하위 예외는 모두 체크 예외이다. 단 RuntimeException은 예외로 한다.
RuntimeException : 컴파일러가 예외를 던졌는지 잡앗는지 체크하지 않는 언체크 예외이다. Runtime의 자식 예외는 모두 언체크 예외이다. RumtimeException의 이름을 따라서 RuntimeException과 그 하위 언체크 예외를 런타임 예외라고도 부른다.
예외 기본 규칙
1. 예외는 잡아서 처리하거나 던져야 한다.
2. 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리된다.
따라서 Exceptiopn을 catch로 잡으면 그 하위 예외도 모두 잡을 수 있고 만약 throws로 던지면 하위 예외들도 모두 던질 수 있다.
3. 예외를 처리하지 못하고 계속 던지는 경우 자바의 main() 쓰레드 경우 예외 로그를 출력하면서 시스템이 종료됨.
체크 예외 이해
- 체크 예외는 catch로 잡아 처리하고나 throws로 던지도록 선언해야한다. 그렇지 않으면 컴파일 오류가 발생한다.
class MyCheckedException extends Exception
Exception을 상속받음으로써 체크 예외를 생성할 수 있다.
체크 예외의 장단점
체크 예외는 잡아서 처리할 수 없을 때 예외를 밖으로 던지는 throws를 필수로 선언해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.
이는 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 안전장치이다.
하지만 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 된다. 추가로 필요하지 않은 계층에서 예외를 처리하는 코드가 들어간다. 따라서 모든 계층에 불필요한 예외 코드가 생긴다.
언체크 예외 기본 이해
- RuntimeException과 그 하위 예외는 언체크 예외로 분류된다.
- 체크 예외와 그 차이는 throws를 선언하지 않고 생략할 수 있다. 이 경우 자동으로 예외를 던진다.
class MyUncheckedException extends RuntimeException
RuntimeException을 상속받은 예외는 언체크 예외가 된다.
언체크 예외의 장단점
언체크 예외는 예외를 잡아서 처리할 수 없을 때 throws 예외 선언을 생략할 수 있다.
이는 신경 쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 이는 신경쓰고 싶지 않은 예외의 의존관계를 참조하지 않아도 되는 장점이 있다.
하지만 이는 개발자의 실수로 예외를 누락할 수 있다.
언체크 예외든 체크 예외든 모든 예외는 잡거나 던지거나 둘 중 하나는 해야 한다. 다만 이를 컴파일러가 체크를 하냐 안 하냐 그 차이가 존재할 뿐이다.
체크 예외 활용
대원칙을 가지고 문제에 접근하면 하나씩 문제를 해결하는 것보다 효율적으로 문제를 처리할 수도 있다.
내가 작은 경험으로 본 예외를 처리하는 코드는 다음과 같았다.
1. 무조건 던진다.
2. 필터를 통해 예외를 걸러 적절한 오류를 노출시킨다.
이는 아마 수많은 체크 예외에 지친 개발자들이 예외에 대해 무조건 던지라는 무의식적인 습관을 물려주는 현상이라고 생각한다.
김영한 님은 이에 대해 큰 두 가지 원칙을 정하였다고 한다.
첫 번째론 기본적으로 언체크 예외를 사용하는 것
두 번째론 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용하자.
두 번째 방법인 경우에는 해당 예외를 반드시 처리해야 하는 문제일 때만 체크 예외를 사용해야 한다고 한다.
이해하기 쉽게 여러 가지 예시를 들어주는데 계좌 이체 실패 예외인 경우, 결제시 포인트 부족 예외인경우 로그인 ID, PW 불일치 예외인 경우 이러한 예시의 공통점을 살펴보면 개발자가 해당 예외를 잡아 처리할 수 있는 가능성이 있다는 것이다.
또한 심각한 비즈니스 문제는 개발자가 실수로 예외를 놓치면 안 된다고 판단할 수 있기에 컴파일러를 통해 놓친 예외를 인지할 수 있다는 장점이 있다.
정리하면 기본적으로 언체크 예외를 적용하는 원칙을 적용하고 내가 직접 비즈니스적 예외를 정의할 때
크리티컬 하고 반드시 개발자가 놓치면 안 되는 부분이라 생각되면 체크 예외를 적용한다.
그럼 왜 언체크 예외를 디폴트 값으로 가져가야 하는지 의문점이 들 수 있다.
체크 예외는 컴파일러가 예외를 체크하기 때문에 좀 더 안전할 것 같은데 왜 언체크 예외를 사용할까?
바로 다음과 같은 문제가 발생할 수 있기 때문이다.
SQLException, NetworkException과 같이 시스템 레벨에서 발생하는 문제들은 Service 계층에서 해결할 수 없다.
위와 같은 구조에서는 Service에서 해당 예외를 처리하도록 강제되기기에 결국 던질 수밖에 없다.
이 과정에서 throws의 선언이 반드시 필요하다.
다음으로 Controller에서 해당 예외를 받았다고 해도 처리할 수 없다. 이후 웹 애플리케이션이라면 필터 혹은 ControllerAdvice를 통해 예외를 공통으로 처리할 것이다.
이렇게 해결 불가능한 예외는 로그를 남기고 알림을 통해 개발자에게 전달되어서 해결되어야 한다.
여기서 큰 문제는 왜 해결하지 못하는 계층에서 해당 예외 관련된 코드를 포함하고 있다는 것이다.
이는 의존 관계에 대한 문제를 발생시킨다.
서비스 계층과 컨트롤러 계층이 구체적인 기술에 대해 의존하게 되고 이는 변경에 취약하게 된다.
즉 본인이 처리할 수 도 없는 예외를 받는 것 도 억울한데, 의존 관계 문제까지 가지고 간다는 것이다.
그러면 구체적인 기술의 예외 말고 부모 Exception을 throws 하면 되는 것 아닐까라고 생각할 수 있다.
또한 이렇게 처리하는 경우도 실제로 보았다. 이는 다음과 같은 문제를 발생시킬 수 있는데 바로 체크 예외의 목적 자체가 사라진다.
Exception 클래스는 모든 예외의 부모 타입이다. 따라서 모든 체크 예외를 던질 수 있기에 체크라는 목적이 무색하게 검사하지 않기 때문이다. 정말 중요한 체크 예외가 있는데 이를 놓치고 그냥 던지게 된다면, 혹은 개발자의 실수로 중요한 비즈니스 로직에서 반드시 잡아야 하는 체크 예외도 놓칠 수 있다.
결국 이 문제에 대한 대안으로는 언체크 예외를 활용하는 것이다.
기존 체크 예외를 던진다고 가정한 코드를 다음과 같이 변경해보자.
method () throws RuntimeException{}
혹은 RuntimeException을 상속받은 커스텀 예외를 던지도록 하자.
class RuntimeDataException extends RuntimeException {}
이때 우리는 체크 예외가 발생되는 계층에서 해당 예외를 try/catch로 잡은 후 새로 정의한 RuntimeException 혹은 RuntimeException으로 변경해서 던져야 한다.
이때 기존 Exception을 catch를 한 이후 RuntimeException을 던져야 기존 Exception의 stack trace를 볼 수 있다.
이를 통해 기존 체크 예외를 런타임 예외로 바꾸어 복구할 수 없는 예외를 처리함으로 써 문제를 해결하였다.
이러한 런타임 예외는 문서화를 잘해야 한다. 또는 코드에 throws 런타임 예외를 남겨서 중요한 예외를 인지할 수 있게 하여야 한다.
* 예외를 전환할 때는 꼭 기존 예외를 포함해야 한다.!
'JAVA > [JAVA] 바구니' 카테고리의 다른 글
equals 정의 (0) | 2022.11.10 |
---|---|
finalizer와 cleaner 사용을 피하라 (0) | 2022.11.07 |
SSL/TLS 서버 통신 (JSSE, TrustManager) (1) | 2021.11.06 |
짧)[JAVA] 객체지향 세계 (0) | 2021.04.25 |
[JAVA] JAVA Serialize (0) | 2021.04.23 |
댓글