토비의 스프링 3.1 - 4장_예외

Roeniss Moon·2020년 6월 25일
0

토비의 스프링 3.1

목록 보기
5/6
post-thumbnail

모든 예외는 적절하게 복구되거나, 작업을 중단하고 개발자에게 통보되어야 한다.

예외의 종류

Error

'시스템'에 비정상적인 상황이 발생했을 때 사용된다. 여기서 시스템은 '애플리케이션'과 대척점에 서 있는 JVM 메모리, OS 레이어 등을 말한다. 에러는 애플리케이션 코드에서 잡으면 안된다. 바꿔 말하자면, 애플리케이션에서는 이런 에러에 대한 에러를 신경쓰지 않는다.

🤔 사견 : 여러 글을 봤는데, Error가 발생했을 때는 그냥 다 포기하고 앱이 다운되는 걸 수긍하라는 얘기만 보인다. 이에 대한 처리는 모니터링 인프라나 도커 등으로 극복해야 되는 것 같다.

Exception

Checked Exception

catch/throw가 의무적이다.

컴파일 단에서 확인이 가능하고, 복구를 시도해보는 것이 일차적인 관심사다. 바꿔 말하자면, 복구될 가능성이 있는 문제 상황을 checked exception이라 한다.

Unchecked Exception

(ex. NullPointException)

RuntimeException을 상속하고 있어서 뭉뚱그려 '런타임 예외'라고 부르기도 한다. catch/throw 하지 않아도 된다.

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

발생하지 않도록 설계하는게 우선이며, 발생했을 때는 복구하는 대신, 빨리 처리 해버리는게 주 관심사다.

예외처리 방법

예외 복구

다른 작업 흐름으로 자연스럽게 유도하기. (ex) "다시 시도해주십시오."

예외처리 회피

rethrow. 즉, caller에게 전달하고 책임을 회피한다.

예외 전환 (exception translation)

예외를 전환(변환)한 후에 rethrow한다.

두 가지 케이스(이유)가 있다.

  1. 의미가 분명해지도록 구체화. (ex) SQLException --> DuplicateUserIdException (custom exception)

  2. Wrapping. (ex) checked execption --> RuntimeException (unchecked exception)

2번 이유의 예시에 대한 부가 설명 : "어차피 복구하지 못할 예외라면 애플리케이션 코드에서는 런타임 예외로 포장해서 던져버리고, 기타 서비스를 이용해 자세한 로그를 남기면서 메일로 관리자에게 통보하고, 사용자에게 친절한 안내 메시지를 보이는 게 바람직하다"

2번 이유의 또다른 예시 : 애플리케이션 로직에서 발생하는 예외 상황은 chekced Exception을 일부러 만들어 '적절한 대응이나 복구 작업'을 하는 것이 좋다.

일관된 에러처리 전략을 제시하자면

대개의 경우 런타임 에러로 wrapping할 것

Unchekced/Checked Exception 분류에 대한 저자의 생각 :

"자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치는 점점 떨어지고 있다. (...) 대응이 불가능한 체크 예외라면 빨리 런타임 예외로 전환해서 던지는 게 낫다."

"예전에는 복구할 가능성이 조금이라도 있다면 체크 예외로 만든다고 생각했는데, 지금은 (라이브러리들이) '항상' 복구할 수 있는 예외가 아니라면 일단 언체크 예외로 만드는 경향이 있다. 언체크 예외라도 필요하다면 얼마든지 catch 블록으로 잡아서 복구하거나 처리할 수 있(기 때문이)다."

애플리케이션 예외의 처리는 custom error code 또는 custom exception

전자의 예시 : 잔액부족 == 775

후자의 예시 : throw LackOfBalanceException

JdbcTemplate의 경우

3장에서 완성한 UserDao.add()는 메소드 시그니처에 throws SQLException이 빠졌는데, 이는 JdbcTemplate 템플릿/콜백 안에서 발생하는 모든 SQLException이 RuntimeException을 상속한 DataAccessException으로 wrapping 되어있기 때문이다.

(기존) JDBC의 문제점

DB에 관계없이 자유롭게 사용할 수 없다. 여기엔 두 가지 이유가 있다.

  1. 특정 DB에만 있는 비표준 SQL들을 사용한다면, 다른 DB로 갈아끼울 때 SQL을 한바탕 뒤집어야 한다. (이에 대한 내용은 7장에서 자세히 다룸)

  2. 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 액세스 기술 교체가 클라이언트 코드에 영향을 끼치지 않는 것'으로 이해할 수 있다)

profile
기능이 아니라 버그예요

0개의 댓글