모든 예외는 적절하게 복구되거나, 작업을 중단하고 개발자에게 통보되어야 한다.
'시스템'에 비정상적인 상황이 발생했을 때 사용된다. 여기서 시스템은 '애플리케이션'과 대척점에 서 있는 JVM 메모리, OS 레이어 등을 말한다. 에러는 애플리케이션 코드에서 잡으면 안된다. 바꿔 말하자면, 애플리케이션에서는 이런 에러에 대한 에러를 신경쓰지 않는다.
🤔 사견 : 여러 글을 봤는데, Error가 발생했을 때는 그냥 다 포기하고 앱이 다운되는 걸 수긍하라는 얘기만 보인다. 이에 대한 처리는 모니터링 인프라나 도커 등으로 극복해야 되는 것 같다.
catch/throw가 의무적이다.
컴파일 단에서 확인이 가능하고, 복구를 시도해보는 것이 일차적인 관심사다. 바꿔 말하자면, 복구될 가능성이 있는 문제 상황을 checked exception이라 한다.
(ex. NullPointException)
RuntimeException을 상속하고 있어서 뭉뚱그려 '런타임 예외'라고 부르기도 한다. catch/throw 하지 않아도 된다.
이 예외들은 "코드에서 미리 조건을 체크하도록 주의 깊게 만든다면 피할 수 있다. 피할 수 있지만 개발자가 부주의해서 발생할 수 있는 경우에 발생하도록 만든 것"이기 때문이다.
발생하지 않도록 설계하는게 우선이며, 발생했을 때는 복구하는 대신, 빨리 처리 해버리는게 주 관심사다.
다른 작업 흐름으로 자연스럽게 유도하기. (ex) "다시 시도해주십시오."
rethrow. 즉, caller에게 전달하고 책임을 회피한다.
예외를 전환(변환)한 후에 rethrow한다.
두 가지 케이스(이유)가 있다.
의미가 분명해지도록 구체화. (ex) SQLException
--> DuplicateUserIdException
(custom exception)
Wrapping. (ex) checked execption --> RuntimeException (unchecked exception)
2번 이유의 예시에 대한 부가 설명 : "어차피 복구하지 못할 예외라면 애플리케이션 코드에서는 런타임 예외로 포장해서 던져버리고, 기타 서비스를 이용해 자세한 로그를 남기면서 메일로 관리자에게 통보하고, 사용자에게 친절한 안내 메시지를 보이는 게 바람직하다"
2번 이유의 또다른 예시 : 애플리케이션 로직에서 발생하는 예외 상황은 chekced Exception을 일부러 만들어 '적절한 대응이나 복구 작업'을 하는 것이 좋다.
Unchekced/Checked Exception 분류에 대한 저자의 생각 :
"자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치는 점점 떨어지고 있다. (...) 대응이 불가능한 체크 예외라면 빨리 런타임 예외로 전환해서 던지는 게 낫다."
"예전에는 복구할 가능성이 조금이라도 있다면 체크 예외로 만든다고 생각했는데, 지금은 (라이브러리들이) '항상' 복구할 수 있는 예외가 아니라면 일단 언체크 예외로 만드는 경향이 있다. 언체크 예외라도 필요하다면 얼마든지 catch 블록으로 잡아서 복구하거나 처리할 수 있(기 때문이)다."
전자의 예시 : 잔액부족 == 775
후자의 예시 : throw LackOfBalanceException
3장에서 완성한 UserDao.add()는 메소드 시그니처에 throws SQLException
이 빠졌는데, 이는 JdbcTemplate 템플릿/콜백 안에서 발생하는 모든 SQLException이 RuntimeException을 상속한 DataAccessException
으로 wrapping 되어있기 때문이다.
DB에 관계없이 자유롭게 사용할 수 없다. 여기엔 두 가지 이유가 있다.
특정 DB에만 있는 비표준 SQL들을 사용한다면, 다른 DB로 갈아끼울 때 SQL을 한바탕 뒤집어야 한다. (이에 대한 내용은 7장에서 자세히 다룸)
SQLException 하나로 모든 예외가 압축되기 때문에, e.gerErrorCode()
등으로 에러 코드를 확인해야만 정확한 에러를 확인할 수 있다. 그런데, 각 DB는 서로 다른 에러 코드를 사용한다.
본 4장에서는 2번에 대한 해결 방법만 언급한다.
위 2번 문제를 해결하기 위해, 스프링의 JdbcTemplate에서는 최종 예외로 DataAccessException
을 두고, 이 예외와 실제 DB단에서 발생하는 예외들 사이에 DataAccessExeption 계층구조
를 구축했다. 실제 DB 에러가 최종적으로 스프링의 예외로 튀어나오는 과정은 다음과 같다.
DB별 'DB 에러 코드 to 스프링 예외 클래스' 맵핑 테이블이 있어서, 얼추 각 DB의 같은 예외들이 같은 예외 클래스로 모이게 된다.
이 예외 클래스들은 DataAccessException을 상속하고 있다. 그래서 최종적으로 DataAccessException의 형태로 catch 문에서 잡을 수 있다.
"DataAccessException 계층구조는 '데이터 액세스 기술'의 종류와 상관없이 일관된 에러가 발생하도록 만들어준다."
기존에 만든 UserDao를 인터페이스로 변환해서, 다른 DAO로 갈아끼울 수 있게 만드는 시나리오를 생각해보자. 지금까지 만든 Dao는 UserDaoJdbc이라는 이름의 UserDao 인터페이스 구현체가 될 것이다.
그런데 인터페이스의public void add(User user)
메소드에throw SQLException
이라는 시그니처가 붙으면, hibernate를 이용한 DAO를 사용했을 때public void add(User user) throw HibernateException
이라는 또다른 시그니처가 필요하므로, 인터페이스를 제대로 활용할 수가 없다.
...같은 경우가 있을 수 있기 때문에 후발주자 기술들은 런타임 예외(DataAccessException의)를 사용한다고 한다.
🤔 사견 : 바로 위에서 언급한 부분은, 훨씬 앞에 언급한 RuntimeException의 장점을 말할 때 같이 얘기해도 좋았을 것 같다. 책에서는 대략 이 타이밍에 등장한다.
에러(Error)는 신경 쓰지 않는 거다
예외를 잡았으면 복구하거나, 전달(rethrow)하거나, 적절한 예외로 전환하라
예외 전환에는 의미를 분명하게 하는 방법과 런타임 에러로 포장하는 방법이 있다
복구할 수 없는 예외는 빨리 RuntimeException으로 전환하라
애플리케이션의 로직을 담는 예외는 Checked Exception으로 전환하라
throw Exception
같은거 쓰지 마라
스프링은 DataAccessException 계층 구조를 통해 DB에 독립적인, '추상화된 런타임 예외 계층'을 제공한다. 이는 JDBC의 SQLException에 달린 에러코드가 특정 DB에 종속되는 것과 대비되는 모습이다.
DAO를 '데이터 액세스 기술'에서 독립시키기 위해선 (1) 인터페이스 도입, (2) 런타임 예외로의 전환, (3) 기술에 독립적인 추상화된 예외로의 전환 이 필요하다. (여기서 말하는 독립은, '클라이언트 측에서 특정 기술과 관련된 예외를 의무적으로 작성하지 않아도 되는 것', 'DB 액세스 기술 교체가 클라이언트 코드에 영향을 끼치지 않는 것'으로 이해할 수 있다)