[Java] 자바의 예외(Exception) 처리

1. 에러(Error) 예외(Exception)

 

 

일반적으로 자바에서는 잡을 필요도 없고, 회복또한 불가능한 것을 에러(Error)라고 하며, 잡을 있고 회복이 가능한 것을 예외(Exception)이라고 부른다. 위 그림에서 나타나는 에러들을 보면 쉽게 이해할 있을 것이다. 예를 들어 OutOfMemoryError라는 에러는메모리 부족이라는 예상할 없고, 프로그램상으로 해결할 수도 없는 문제를 나타내고 있다. 아마 자연스럽게 느껴질텐데, 이러한 이유로 프로그램을 주로 개발자 에러보다는 예외에 대해서 고려하게 된다.

 

 

 

2. 예외(Exception)의 종류

 

 

 자바에서 모든 예외도 객체이므로 Object를 상속받으며, 최상위 예외로는 Throwable클래스를 갖는다. Throwable 클래스는 Exception과 Error로 나뉜다. Error는 애플리케이션 개발자가 해결할 수 없는 시스템 에러(ex-OutOfMemoryError)이고, Exception이 프로그래머가 처리 가능한 예외이다. 일반적으로 Exception은 체크 예외(checked exception)과 언체크 예외(unchecked-exception)으로 분류한다.

 

- 체크 예외(checked exception)

: 컴파일러가 체크하는 예외이다. 반드시 throws로 예외를 던질 수 있음을 명시하거나, try-catch문으로 예외를 처리해주어야 한다.

- 언체크 예외(unchecked exception)

: 컴파일러가 체크하지 않는 언체크 예외이다. Exception의 하위 클래스인 RuntimeException부터 그 하위 예외들은 모두 언체크 예외로 분류한다.

 

 

1. 개발할 때는 언체크 예외를 사용하자. 체크 예외를 사용하면 반드시 명시적으로 처리하거나, 던져주어야 하기 때문에 코드가 복잡해질 뿐 아니라, 의존관계에 대한 문제가 발생한다. 따라서 체크 예외는 컨트롤러나 서비스에서 잡아서 처리해야 하는 예외일 경우만 사용하고, 그 외에는 런타임 예외를 사용하여 @ControllerAdvice등으로 일괄적으로 잡아 처리해주도록 하자.

 

2. 언체크 예외를 사용할 경우, 컴파일러가 예외처리 로직을 검증해주지 않기 때문에 문서화해놓는것이 좋다. 또한 발생하는 체크 예외를 받아서 언체크예외로 전환해주는 경우에는, 반드시 넘어오는 체크예외를 파라미터 마지막에 포함하여 로그로 stacktrace가 출력되도록 하자.

 

 

 

3. 체크 예외의 사용에 관한 고찰

 

그렇다면 예외, 중에도 체크 예외에 대해서 이야기해보자. 코드를 작성하다 빨간색 밑줄이 그어지고, 사용한 라이브러리 메소드 등에서 (체크)예외가 발생해서 올라왔다고 하자. 우리는 두가지 선택지가 있다. 잡거나(try-catch), 던지거나(throws). 그렇다면 예외는 어디서 잡아서 처리해주어야 하는가? 처음 예외가 발생한 메소드를 호출한 곳에서 처리해야 하는가? 아니면 콜스택의 가장 상위단계에서 잡아 처리해야 할까?

 


 에러처리의 가장 중요한 원칙 책임지지 않을 바에는 잡지도 말라라는 말이 있다. 예외는 항상 의미있는 방식으로 처리해야 한다는 뜻이다. 예를 들어 서비스 계층에서 비즈니스 규칙을 담은 체크예외(예를 들면 잔고에 돈이 모자라다는 NotEnoughMoneyException이 있다고 하자)가 발생했다고 가정하면 사용자에게 비즈니스적인 메세지를 보여주거나, 다른 페이지로 리다이렉트하는 방식으로 동작하게 된다.

 이러한 작업은 콜스택의 가장 상위단계에서 처리하는 것이 알맞은 처리 방법이라고 예상할 있다. 예외가 처음 발생한 서비스 계층에서 리다이렉트를 위해 컨트롤러의 HttpResponse객체를 참조하는 것은 분명 어색함을 동반하기 때문이다. 화면에 관한 내용(리다이렉트) 서비스 계층의 책임이 아니기에, 계층화된 설계의 목적에도 벗어나게 된다. 뿐만 아니라 비즈니스 예외를 체크예외로 명시할 경우 비즈니스 규칙을 상세하게 명시하고 구현상의 세부사항은 숨기는 것이 좋다는 원칙에도 부합하기에 이러한 경우 throws 통해서 controller계층까지 던져주는 것은 충분히 합리적이다.

 

 

 그런데 이번에는 데이터베이스에서 발생 가능한 체크 예외 SQLException 처리해야 한다고 하자. 비즈니스 요구조건이 SQLException 발생시 리다이렉트시켜주는 것이라고 가정했을 , 위와 동일하게 리다이렉트를 위해서 HttpResponse 참조할 있는 컨트롤러에서 체크 예외를 잡아 처리해주었다. 과연 이것이 맞는 처리방식이라고 할 수 있을까?

 SQLException 컨트롤러까지 올라가는 과정에서 서비스 계층의 메서드들은 SQLException 던지게 되고, 이것은 중간 단계의 모든 메소드들이 JDBC API 의존성이 생기게 된다 것을 의미한다. 추후 JDBC API 아닌 다른 구현체로 변경하게 되면 SQLException 던졌던 모든 메소드들과 try-catch문을 수정해야 한다는 것을 의미한다. 결국 기본적인 객체지향의 추상화까지 위반하게 되는 것이다.

 

 이렇게 동일한 체크 예외라도 비즈니스 규칙을 담은 체크 예외와 일반적인 체크 예외의 사용은 차이를 가짐을 알 수 있다.

 

 

 

4. 예외를 처리하도록 강제해야 하는가? 

 

 위 내용의 결론이라고 이야기해도 될 것 같다. 위에서 언급했듯, 체크 예외는 발생하는 예외를 잡아서 처리할 것을 강제하고 있다. 이는 한편으로는 예측 가능한 예외에 대한 명시적인 처리를 통해 프로그램의 안정성에 도움이 되는 것처럼 보이지만, 강제성에 따라오는 여러 문제들이 존재한다.


C#
창시자 엔더스 헤일버그는 이러한 체크 예외에 대해서 "버전관리(Versioning) 확장성(Scalability) 문제를 가지고 있다"라고 언급한다. 결국 비슷한 맥락인데, 간단하게 말하면 라이브러리 버전관리로 인해 추가, 삭제되는 체크 예외는 라이브러리를 가져다 쓰는 사용자 코드에까지 영향을 미치게 되며, 대규모 시스템에서 여러 라이브러리를 가져다 사용하면 수십개의 체크 에러를 처리하게  수도 있다는 이야기이다. 

 이러한 이유로 불필요한 체크 예외는 사용하지 않거나 런타임 예외로 변환하여 사용하는 프로젝트가 많고, 여러 라이브러리에서도 체크 예외를 언체크 예외로 변환하여 던져주고는 한다(스프링 프레임워크에서는 SQLException 추상화한 DataAccessException RuntimeException 상속하고 있다).  나아가 코틀린에서는 체크 예외를 구분하지 않아 강제로 throws해줄 필요가 없게 만들었다.

 

 

 

참고:

https://www.artima.com/articles/the-trouble-with-checked-exceptions

https://okky.kr/articles/362305

https://toss.tech/article/kotlin-result

'[ Languages ] > Java' 카테고리의 다른 글

[Java] Collection  (0) 2024.08.10
[Java] Thread-safe하게 변수 관리하는 방법들  (0) 2023.11.03
[Java] 람다식 이해하기  (0) 2023.02.07
[Java] stream  (0) 2022.08.07
[Java] 자바의 동작 원리와 특징  (0) 2022.07.08