예외를 처리할 때 반드시 지켜야 할 핵심 원칙은 한 가지다. 모든 예외는 적절하게 복구 되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다.
Exception
일반적으로 예외라고 하면 Exception 클래스의 서브클래스 중에서 RuntimeException 을 상속하지 않은 것만을 말하는 체크 예외라고 생각해도 된다. 체크 예외가 발생 할 수 있는 메소드를 사용할 경우 반드시 예외를 처리하는 코드를 함께 작성해야 한다. 사용할 메소드가 체크 예외를 던진다면 이를 catch 문으로 잡든지, 아니면 다시 throws를 정의해서 메소드 밖으로 던져야 한다. 그렇지 않으면 컴파일 에러가 발생한다.
Runtime Exception
피할 수 있지만 개발자가 부주의해서 발생 할 수 있는 경우에 발생하도록 만든 것이 런타임 예외다. 따라서 런타임 예외는 예상 하지 못했던 예외상황에서 발생하는 게 아니기 때문에 굳이 catch나 throws를 사용하지 않아도 되도록 만든 것이다.
예외 복구
예외처리 코드를 강제하는 체크 예외들은 이렇게 예외를 어떤 식으로든 복구할 가능 성이 있는 경우에 사용한다. API를 사용하는 개발자로 하여금 예외상황이 발생할 수 있음을 인식하도록 도와주고 이에 대한 적절한 처리를 시도해 보도록 요구하는 것이다
예외처리 회피
예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 한다. 콜백/템플릿처럼 긴밀한 관계에 있는 다른 오브젝트에게 예외처리 책임을 분명히 지게 하거나, 자신을 사용하는 쪽에서 예외를 다루는 게 최선의 방법이라는 분명한 확신이 있어야 한다.
예외 전환
예외 회피와 달리, 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다는 특징이 있다. 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미 를 부여해주지 못하는 경우에, 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서다.
보통 전환하는 예외에 원래 발생한 예외를 담아서 중첩 예외nested exception로 만드는 것이 좋다. 중첩 예외는 getCause() 메소드를 이용해서 처음 발생한 예외가 무엇인지 확인 할 수 있다.
체크 예외는 비즈니스 로직으로 볼 때 의미 있는 예외이거나 복구 가능한 예외가 아니다. 이런 경우에는 런타임 예외로 포장해서 던지는 편이 낫다. 반대로 애플리케이션 로직상에서 예외조건이 발견되거나 예외상황이 발생할 수도 있다. 이런 것은 API가 던지는 예외가 아니라 애플리케이션 코드에서 의도적으로 던지는 예외다. 이때는 체크 예외를 사용하는 것이 적절하다. 비즈니스적인 의미가 있는 예외는 이에 대한 적절한 대응이나 복구 작업이 필요하기 때문이다.
요약
예외를 처리하는 것이 바람직 혹은 가능한가? 라는 질문을 생각하자. 결국 예외란 처리되어서 프로그램이 그대로 정상 진행되거나 종료되거나 둘 중에 하나를 선택하는 작업이다. 서버 환경에서는 특정 요청이 권한이 없으면 로직을 수행할 가치가 없다. 그런 경우 예외를 잡을 필요없이 던지기만 한다면 해당 요청은 종료될 것이다. 반대로 자바 기반 어플리케이션(다운로드 한)을 생각해보자. 사용자가 비밀번호를 틀렸다고 프로그램이 종료되면 될 것인가? 이처럼 예외에 대해서 프로그램을 어떻게 처리할 것인지를 고민하고 그에 맞는 예외 처리 전략을 가져가면 된다.
일반적으로는 체크 예외가 일반적인 예외를 다루고, 언체크 예외는 시스템 장애나 프로그램상의 오류에 사용한다고 했다. 자바가 처음 만들어질 때 많이 사용되던 애플릿이나 AWT, 스윙Swing을 사용한 독립형 애플리케이션에서는 통제 불가능한 시스템 예외라고 할지라도 애플리케이션의 작업이 중단되지 않게 해주고 상황을 복구해야 했다.
하지만 자바 엔터프라이즈 서버환경은 다르다. 수많은 사용자가 동시에 요청을 보내 고 각 요청이 독립적인 작업으로 취급된다. 하나의 요청을 처리하는 중에 예외가 발생하면 해당 작업만 중단시키면 그만이다. 독립형 애플리케이션과 달리 서버의 특정 계층에서 예외가 발생했을 때 작업을 일시 중지하고 사용자와 바로 커뮤니케이션하면서 예외 상황을 복구할 수 있는 방법이 없다. 런타임 예외로 포장해 던져 버려서 그 밖의 메소드들이 신경 쓰지 않게 해 주는 편이 낫다.
예외를 다른 것으로 바꿔서 던지는 예외 전환의 목적은 두 가지라고 설명했다. 하나는 앞 에서 적용해본 것처럼 런타임 예외로 포장해서, 굳이 필요하지 않은 catch/throws를 줄여주는 것이고, 다른 하나는 로우 레벨의 예외를 좀 더 의미 있고 추상화된 예외로 바꿔서 던져주는 것이다.
구현 기술마다 다른 예외, 우리는 어떻게 사용해야할까?
스프링은 자바의 다양한 데이터 액세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException 계층구조 안에 정리해놓았다. 아래와 같은 형태로, 다양한 구현체에서 발생하는 예외를 크게는 DataAccessException으로 분류하고 상속을 통해 세부적인 사항들 또한 일관된 예외를 반환한다.
JdbcTempate과 같이 스프링의 데이터 액세스 지원 기술을 이용해 DAO를 만들면 사용 기술에 독립적인 일관성 있는 예외를 던질 수 있다. SQL Exception이 구현 기술에 따라 다르게 반응하지 못하는 한계를 스프링이 정의한 예외로 추상화시켜 놓았기 때문이다.
결국 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException 예외 추상화를 적용하면 데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO를 만들 수가 있다. (참고로 인터페이스에 사용이란 Dao를 인터페이스로 분리해, 특정 기술에 종속적이지 않도록 설계하는 것을 말한다)
주의점
이렇게 스프링을 활용하면 DB 종류나 데이터 액세스 기술에 상관없이 키 값이 중복 이 되는 상황에서는 동일한 예외가 발생하리라고 기대할 것이다.
하지만 안타깝게도 DuplicateKeyException은 아직까지는 JDBC를 이용하는 경우에만 발생한다. 데이터 액세스 기술을 하이버네이트나 JPA를 사용했을 때도 동일한 예외가 발생할 것으로 기대하지만 실제로 다른 예외가 던져진다.
그 이유는 SQLException에 담긴 DB의 에러 코드를 바로 해석하는 JDBC의 경우와 달리 JPA나 하이버네이트, JDO 등에서는 각 기술이 재 정의한 예외를 가져와 스프링이 최종적으로 DataAccessException으로 변환하는데, DB 의 에러 코드와 달리 이런 예외들은 세분화되어 있지 않기 때문이다. - 아직 모든 예외가 명확하게 추상화 되어 있지 않다.
결론
결국 스프링이 잘 적립한 DataAccessException을 활용하는 것이 바람직하지만, 특정 기술에 따라 예상하지 못한 결과가 반환될 수 있다. 이런 경우 로직에서 적절한 DataAccessException의 하위 클래스로 전환하는 방법 등을 사용해서 최대한 일관된 예외 전략을 가져가는 것이 좋을 것 같다.