토비의 스프링 (4)


예외의 종류와 특징

예외를 어떻게 다뤄야 할까?
가장 큰 이슈는 체크 예외라고 불리는 명시적인 처리가 필요한 예외를 사용하고 다루는 방법이다.
자바에서 throw를 통해 발생시킬 수 있는 예외는 크게 세 가지가 있다.


첫째는 java.lang.Error 클래스의 서브클래스들이다. 에러는 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용된다.
주로 자바 VM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안 된다.

OutOfMemoryErrorThreadDeath같은 에러는 catch 블록으로 잡아봤자 아무런 대응 방법이 없기 때문이다.
따라서 시스템 레벨에서 특별한 작업을 하는 게 아니라면 애플리케이션에서는 이런 에러에 대한 처리는 신경 쓰지 않아도 된다.


java.lang.Exception 클래스와 그 서브클래스로 정의되는 예외들은 에러와 달리
개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용된다.

Exception 클래스는 다시 체크 예외와 언체크 예외로 구분된다.
전자는 Exception 클래스의 서브클래스이면서 RuntimeException 클래스를 상속하지 않은 것들이고,
후자는 RuntimeException을 상속한 클래스들을 말한다.

RuntimeException은 Exception의 서브클래스이므로 Exception의 일종이긴 하지만 자바는 이 RuntimeException과 그 서브클래스는 특별하게 다룬다.

일반적으로 예외라고 하면 Exception 클래스의 서브클래스 중에서 RuntimeException을 상속하지 않은 것만을 말하는
체크 예외라고 생각해도 된다.
체크 예외가 발생할 수 있는 메소드를 사용할 경우 반드시 예외를 처리하는 코드를 함께 작성해야 한다.
그렇지 않으면 컴파일 에러가 발생한다.


java.lang.RuntimeException 클래스를 상속한 예외들은
명시적인 예외처리를 강제하지 않기 때문에 언체크 예외라고 불린다.
또는 대표 클래스 이름을 따서 런타임 예외라고도 한다.

에러와 마찬가지로 이 런타임 예외는 catch 문으로 잡거나 throws로 선언하지 않아도 된다.
물론 명ㅁ시적으로 잡거나 throws로 선언해도 상관없다.

런타임 예외는 주로 프로그램의 오류가 있을 떄 발생하도록 의도된 것들이다.
대표적으로 오브젝트를 할당하지 않은 레퍼런스 변수를 사용하려고 시도했을 때 발생하는 NullException이나,
허용되지 않는 값을 사용해서 메소드를 호출할 때 발생하는 IllegalArgumentException 등이 있다.

이런 예외는 코드에서 미리 조건을 체크하도록 주의 깊게 만든다면 피할 수 있다.
피할 수 있지만 개발자가 부주의해서 발생할 수 있는 경우에 발생하도록 만든 것이 런타임 예외이다.

따라서 런타임 예외는 예상하지 못했던 예외상황에서 발생하는 게 아니기 때문에
굳이 catch나 throws를 사용하지 않아도 되도록 만든 것이다.


예외처리 방법

첫 번째 예외처리 방법은 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것이다.

사용자에게 상황을 알려주고 다른 작업 흐름으로 자연스럽게 유도해서 예외상황을 해결하는 방법이다.

네트워크가 불안해서 가끔 서버에 접속이 잘 안되는 열악한 환경에 있는 시스템이라면
원격 DB 서버에 접속하다 실패해서 SQLException이 발생하는 경우에 재시도를 해볼 수 있다.
네트워크 접속이 원활하지 않아서 예외가 발생했다면 일정 시간 대기했다가 다시 접속을 시도해보는 방법을 사용해서
예외상황으로부터 복구를 시도할 수 있다.
물론 정해진 횟수만큼 재시도해서 실패했다면 예외 복구는 포기한다.


두 번째 방법은 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다.
throws 문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch 문으로 일단 예외를 잡은 후에
로그를 남기고 다시 예외를 던지는 것이다.

빈 catch 블록으로 잡아서 예외가 발생하지 않은 것처럼 만드는 경우는, 드물지만 특별한 의도를 가지고 예외를 복구했거나
아무 개념이 없어서 그런 것이지 회피한 것은 아니다.
예외처리를 회피하려면 반드시 다른 오브젝트나 메소드가 예외를 대신 처리할 수 있도록 던져줘야 한다.

예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 한다.
콜백/템플릿처럼 긴밀한 관계에 있는 다른 오브젝트에게 예외처리 책임을 분명히 지게 하거나,
자신을 사용하는 쪽에게 예외를 다루는 게 최선의 방법이라는 분명한 확신이 있어야 한다.


마지막으로 예외를 처리하는 방법은 예외 전환을 하는 것이다.
예외 회피와 비슷하게 예외를 복구해서 정상적인 상태로는 만들 수 없기 때문에 예외를 메소드 밖으로 던지는 것이다.
하지만 에외회피와는 달리 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다는 특징이 있다.

보통 두 가지 목적으로 사용된다.

(1) 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우,
의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서이다.

보통 전환하는 예외에 원래 발생한 예외를 담아서 중첩 예외로 만드는 것이 좋다.

(2) 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 방법이 있다.
중첩 예외를 이용해 새로운 예외를 만들고 원인이 되는 예외를 내부에 담아서 던지는 방식은 같다.

하지만 의미를 명확하게 하려고 전환하는 것이 아니다.
주로 예외처리를 강제하는 체크 예외를 언체크 에외인 런타임 예외로 바꾸는 경우에 사용한다.

대표적으로 EJBException을 들 수 있다. EJB 컴포넌트 코드에서 발생하는 대부분의 체크 예외는
비즈니스 로직으로 볼 때 의미 있는 예외이거나 복구 가능한 예외가 아니다.
이런 경우에는 런타임 예외인 EJBException으로 포장해서 던지는 편이 낫다.

RuntimeException클래스를 상속한 런타임 예외라서
런타임 예외로 만들어서 전달하면 EJB는 이를 시스템 익셉션으로 인식하고 트랜잭션을 자동으로 롤밷해주기 때문이다.

반대로 애플리케이션 로직상에서 예외조건이 발견되거나 예외상황이 발생할 수도 있다.
이런 것은 API가 던지는 예외가 아니라 애플리케이션 코드에서 의도적으로 던지는 예외이다.
이때는 체크 예외를 사용하는 것이 적절하다.
비즈니스적인 의미가 있는 예외는 이에 대한 적절한 대웅이나 복구 작업이 필요하기 때문이다.


예외처리의 전략

일반적으로 체크 예외가 일반적인 예외를 다루고, 언체크 예외는 시스템장애나 프로그램상의 오류에 사용된다고 했다.
문제는 체크 예외는 복구할 가능성이 조금이라도 있는 예외적인 상황이기 때문에 자바는 이를 처리하는 catch 블록이나
throws 선언을 강제하고 있다는 것이다.

하지만 자바 엔터프라이즈 서버환경에서 수만은 사용자가 동시에 요청을 보내고 각 요청이 독립적인 작업으로 취급된다.
하나의 요청을 처리하는 중에 예외가 발생했다면 해당 작업만 중지시키면 되지만,
서버의 특정 계층에서 예외가 발생할 때 작업을 일시 중지하고 사용자와 바로 커뮤니케이션하면서 예외상활을 복구할 수 있는 방법이 없다.

차라리 애플리케이션 차원에서 예외상황을 미리 파악하고, 예외가 발생하지 않도록 차단하는 것이 좋다.
자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치는 점점 떨어지고 있다.
자칫하면 throws Exception으로 아무런 의미도 없는 메소드들을 낳을 뿐이다.
그래서 대응이 불가능한 체크 예외라면 빨리 런타임 예외로 전환해서 던지는 게 낫다.


시스템 또는 외부의 예외상황이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고,
반드시 catch 해서 무엇인가 조치를 취하도록 요구하는 예외도 있다.
이런 예외들을 일반적으로 애플이케이션의 예외라고 한다.

예를들어, 사용자가 요청한 금액을 계좌에서 출금하는 기능을 가진 메서드를 구현한다고 생각해보자.
계좌에 남아있는 금액보다 더 큰 금액을 출금하려고 한다면 출금 작업은 중단되어야 한다.
이런 기능을 담은 메소드를 설계하는 방법 두 가지가 있다.

1. 정상적인 출금처리를 했을 경우와 잔고 부족이 발생했을 경우에 각각 다른 종류의 리턴 값을 돌려주는 것
이렇게 리턴 값으로 결과를 확인하고 예외상황을 체크하면 불편한 점이 있다.

우선 예외상황에 대한 리턴 값을 명확하게 코드화하고 잘 관리하지 않으면 혼란이 생길 수 있다.
사전에 상수로 정의해둔 표준 코드를 사용하지 않는다면 자칫 개발자 사이의 의사소통 문제로 인해 제대로 작동하지 않을 위험이 있다.

또 한가지 문제는 조건문이 너무 자주 등장한다는 것이다.
코드는 지저분해지고 흐름을 파악하고 이해하기가 힘들어질 것이다.

2. 정상적인 흐름을 따르는 코드는 두고, 잔고 부족과 같은 예외 상황에서는 비즈니스적인 의미를 띤 예외를 던지도록 만드는 것이다.
잔고 부족인 경우라면 InsufficientBalanceException 등을 던진다.

예외상황을 처리하는 catch 블록을 메소드 호출 직후에 둘 필요는 없다.
정상적인 흐름을 따르지만 예외가 발생할 수 있는 코드를 try 블록 안에 깔끔하게 정리해두고
예외상황 처리는 catch 블록에 모아둘 수 있기 떄문에 코드를 이해하기도 편하다.

이때 사용하는 예외는 의도적으로 체크 예외로 만든다.
그래서 개발자가 잊지 않고 자주 발생 가능한 예외상황에 대한 로직을 구현할 수 있도록
강제해주는 게 좋다.